[MERGE] trunk
bzr revid: xmo@openerp.com-20110929132454-oweaht1oel4st1m8
12
MANIFEST.in
|
@ -1,18 +1,12 @@
|
|||
include rpminstall_sh.txt # TODO do we need this file ?
|
||||
include README
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include setup.nsi
|
||||
include setup.cfg
|
||||
#include openerp/server.cert
|
||||
#include openerp/server.pkey
|
||||
#include openerp/gpl.txt
|
||||
include man/openerp-server.1
|
||||
include man/openerp_serverrc.5
|
||||
recursive-include pixmaps *bmp *ico *png
|
||||
include setup_rpm.sh
|
||||
recursive-include win32 *.py *.bat
|
||||
recursive-include openerp *css *csv *html *png *po *pot
|
||||
recursive-include openerp *rml *rng *sql *sxw *xml *xsl *yml
|
||||
recursive-include openerp *css *csv *html *png *po *pot *rml *rng *sql *sxw *xml *xsl *yml
|
||||
graft install
|
||||
graft debian
|
||||
graft doc
|
||||
global-exclude *pyc *~ # Exclude possible garbage from previous graft.
|
||||
|
|
155
README
|
@ -1,17 +1,138 @@
|
|||
About OpenERP
|
||||
---------------
|
||||
|
||||
OpenERP is a free Enterprise Resource Planning and Customer Relationship
|
||||
Management software. It is mainly developed to meet changing needs.
|
||||
|
||||
The main functional features are: CRM & SRM, analytic and financial accounting,
|
||||
double-entry stock management, sales and purchases management, tasks automation,
|
||||
help desk, marketing campaign, ... and vertical modules for very specific
|
||||
businesses.
|
||||
|
||||
Technical features include a distributed server, flexible workflows, an object
|
||||
database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ...
|
||||
|
||||
For more information, please visit:
|
||||
http://www.openerp.com
|
||||
|
||||
About OpenERP
|
||||
-------------
|
||||
|
||||
OpenERP is a free Enterprise Resource Planning and Customer Relationship
|
||||
Management software. It is mainly developed to meet changing needs.
|
||||
|
||||
The main functional features are: CRM & SRM, analytic and financial accounting,
|
||||
double-entry stock management, sales and purchases management, tasks automation,
|
||||
help desk, marketing campaign, ... and vertical modules for very specific
|
||||
businesses.
|
||||
|
||||
Technical features include a distributed server, flexible workflows, an object
|
||||
database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ...
|
||||
|
||||
For more information, please visit:
|
||||
http://www.openerp.com
|
||||
|
||||
OpenERP Quick Installation Guide
|
||||
---------------------------------
|
||||
|
||||
This file contains a quick guide to configure and install the OpenERP server.
|
||||
|
||||
Required dependencies:
|
||||
---------------------
|
||||
|
||||
You need the following software installed:
|
||||
|
||||
* Python 2.5 or 2.6
|
||||
* Postgresql 8.2 or above
|
||||
* Psycopg2 python module
|
||||
* Reportlab pdf generation library for python
|
||||
* lxml python module
|
||||
* pytz python module
|
||||
* PyYaml python module (install with: easy_install PyYaml)
|
||||
|
||||
Some dependencies are only required for specific purposes:
|
||||
|
||||
for rendering workflows graphs, you need:
|
||||
* graphviz
|
||||
* pyparsing
|
||||
|
||||
For Luxembourg localization, you also need:
|
||||
* pdftk (http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/)
|
||||
|
||||
for generating reports using non .jpg images, you need:
|
||||
* Python Imaging Library for python
|
||||
|
||||
For Debian-based distributions, the required packages can be installed with the
|
||||
following command:
|
||||
|
||||
#> apt-get install -y postgresql graphviz python-psycopg2 python-lxml python-tz python-imaging
|
||||
|
||||
For Fedora
|
||||
if they are not installed, install:
|
||||
python and postgresql
|
||||
|
||||
uses yum or you can recover required packages on fedora web site in "core" or "extra" repository :
|
||||
postgresql-python
|
||||
python-lxml
|
||||
python-imaging
|
||||
python-psycopg2
|
||||
python-reportlab
|
||||
graphviz
|
||||
You can find pyparsing at http://pyparsing.sourceforge.net/
|
||||
|
||||
1. Check that all the required dependencies are installed.
|
||||
|
||||
2. Launch the program "python ./bin/openerp-server.py -r db_user -w db_password --db_host 127.0.0.1".
|
||||
See the man page for more information about options.
|
||||
|
||||
3. Connect to the server using the GUI client. And follow the instructions to create a new database.
|
||||
|
||||
Installation Steps
|
||||
------------------
|
||||
|
||||
1. Check that all the required dependencies are installed.
|
||||
|
||||
2. Create a postgresql database.
|
||||
|
||||
The default database name is "terp". If you want to use another name, you
|
||||
will need to provide it when launching the server (by using the commandline
|
||||
option --database).
|
||||
|
||||
To create a postgresql database named "terp" using the following command:
|
||||
$ createdb --encoding=UNICODE terp
|
||||
|
||||
If it is the first time you use postgresql you might need to create a new user
|
||||
to the postgres system using the following commands (where myusername is your
|
||||
unix user name):
|
||||
|
||||
$ su -
|
||||
# su - postgres
|
||||
$ createuser openerp
|
||||
Shall the new user be allowed to create databases? (y/n) y
|
||||
Shall the new user be allowed to create more new users? (y/n) y
|
||||
CREATE USER
|
||||
$ logout
|
||||
# logout
|
||||
|
||||
3. Launch service daemon by "service openerp-server start".
|
||||
|
||||
The first time it is run, the server will initialise the database with all the default values.
|
||||
|
||||
4. Connect to the server using the GUI client.
|
||||
|
||||
There are two accounts by default:
|
||||
* login: admin, password:admin
|
||||
* login: demo, password:demo
|
||||
|
||||
Some instructions to use setup.py for a user-install.
|
||||
This file should/will be moved on a proper documentation place later.
|
||||
|
||||
|
||||
- Possibly clean any left-over of the previous build.
|
||||
> rm -rf dist openerp_server.egg-info
|
||||
|
||||
- Possibly copy the addons in the server if we want them to be packaged
|
||||
together:
|
||||
> rsync -av --delete \
|
||||
--exclude .bzr/ \
|
||||
--exclude .bzrignore \
|
||||
--exclude /__init__.py \
|
||||
--exclude /base \
|
||||
--exclude /base_quality_interrogation.py \
|
||||
<path-to-addons> openerp/addons
|
||||
|
||||
- Create the user-local directory where we want the package to be installed:
|
||||
> mkdir -p /home/openerp/openerp-tmp/lib/python2.6/site-packages/
|
||||
|
||||
- Use --prefix to specify where the package is installed and include that
|
||||
place in PYTHONPATH:
|
||||
> PYTHONPATH=/home/openerp/openerp-tmp/lib/python2.6/site-packages/ \
|
||||
python setup.py install --prefix=/home/openerp/openerp-tmp
|
||||
|
||||
- Run the main script, again specifying the PYTHONPATH:
|
||||
> PYTHONPATH=/home/openerp/openerp-tmp/lib/python2.6/site-packages/ \
|
||||
/home/openerp/openerp-tmp/bin/openerp-server
|
||||
|
||||
|
|
|
@ -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
|
||||
GNU Public Licence.
|
||||
|
||||
(c) 2003-TODAY, Fabien Pinckaers - OpenERP s.a.
|
||||
(c) 2003-TODAY, Fabien Pinckaers - OpenERP SA
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
@ -88,19 +88,27 @@ def setup_pid_file():
|
|||
|
||||
def preload_registry(dbname):
|
||||
""" Preload a registry, and start the cron."""
|
||||
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
|
||||
pool.get('ir.cron').restart(db.dbname)
|
||||
try:
|
||||
db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
|
||||
|
||||
# jobs will start to be processed later, when openerp.cron.start_master_thread() is called by openerp.service.start_services()
|
||||
registry.schedule_cron_jobs()
|
||||
except Exception:
|
||||
logging.exception('Failed to initialize database `%s`.', dbname)
|
||||
|
||||
def run_test_file(dbname, test_file):
|
||||
""" Preload a registry, possibly run a test file, and start the cron."""
|
||||
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
|
||||
try:
|
||||
db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
|
||||
cr = db.cursor()
|
||||
logger = logging.getLogger('server')
|
||||
logger.info('loading test file %s', test_file)
|
||||
openerp.tools.convert_yaml_import(cr, 'base', file(test_file), {}, 'test', True)
|
||||
cr.rollback()
|
||||
cr.close()
|
||||
except Exception:
|
||||
logging.exception('Failed to initialize database `%s` and run test file `%s`.', dbname, test_file)
|
||||
|
||||
cr = db.cursor()
|
||||
logger = logging.getLogger('server')
|
||||
logger.info('loading test file %s', test_file)
|
||||
openerp.tools.convert_yaml_import(cr, 'base', file(test_file), {}, 'test', True)
|
||||
cr.rollback()
|
||||
cr.close()
|
||||
|
||||
def export_translation():
|
||||
config = openerp.tools.config
|
||||
|
@ -136,27 +144,6 @@ def import_translation():
|
|||
cr.commit()
|
||||
cr.close()
|
||||
|
||||
def start_services():
|
||||
http_server = openerp.service.http_server
|
||||
netrpc_server = openerp.service.netrpc_server
|
||||
|
||||
# Instantiate local services (this is a legacy design).
|
||||
openerp.osv.osv.start_object_proxy()
|
||||
# Export (for RPC) services.
|
||||
openerp.service.web_services.start_web_services()
|
||||
|
||||
# Initialize the HTTP stack.
|
||||
http_server.init_servers()
|
||||
http_server.init_xmlrpc()
|
||||
http_server.init_static_http()
|
||||
netrpc_server.init_servers()
|
||||
|
||||
# Start the main cron thread.
|
||||
openerp.netsvc.start_agent()
|
||||
|
||||
# Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC).
|
||||
openerp.netsvc.Server.startAll()
|
||||
|
||||
# Variable keeping track of the number of calls to the signal handler defined
|
||||
# below. This variable is monitored by ``quit_on_signals()``.
|
||||
quit_signals_received = 0
|
||||
|
@ -208,30 +195,16 @@ def quit_on_signals():
|
|||
while quit_signals_received == 0:
|
||||
time.sleep(60)
|
||||
|
||||
openerp.netsvc.Agent.quit()
|
||||
openerp.netsvc.Server.quitAll()
|
||||
config = openerp.tools.config
|
||||
if config['pidfile']:
|
||||
os.unlink(config['pidfile'])
|
||||
logger = logging.getLogger('server')
|
||||
logger.info("Initiating shutdown")
|
||||
logger.info("Hit CTRL-C again or send a second signal to force the shutdown.")
|
||||
logging.shutdown()
|
||||
|
||||
# manually join() all threads before calling sys.exit() to allow a second signal
|
||||
# to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
|
||||
# threading.Thread.join() should not mask signals (at least in python 2.5)
|
||||
for thread in threading.enumerate():
|
||||
if thread != threading.currentThread() and not thread.isDaemon():
|
||||
while thread.isAlive():
|
||||
# need a busyloop here as thread.join() masks signals
|
||||
# and would present the forced shutdown
|
||||
thread.join(0.05)
|
||||
time.sleep(0.05)
|
||||
openerp.service.stop_services()
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
os.environ["TZ"] = "UTC"
|
||||
|
||||
check_root_user()
|
||||
openerp.tools.config.parse_config(sys.argv[1:])
|
||||
check_postgres_user()
|
||||
|
@ -257,7 +230,7 @@ if __name__ == "__main__":
|
|||
if not config["stop_after_init"]:
|
||||
# Some module register themselves when they are loaded so we need the
|
||||
# services to be running before loading any registry.
|
||||
start_services()
|
||||
openerp.service.start_services()
|
||||
|
||||
if config['db_name']:
|
||||
for dbname in config['db_name'].split(','):
|
||||
|
@ -266,6 +239,16 @@ if __name__ == "__main__":
|
|||
if config["stop_after_init"]:
|
||||
sys.exit(0)
|
||||
|
||||
for m in openerp.conf.server_wide_modules:
|
||||
try:
|
||||
__import__(m)
|
||||
# Call any post_load hook.
|
||||
info = openerp.modules.module.load_information_from_description_file(m)
|
||||
if info['post_load']:
|
||||
getattr(sys.modules[m], info['post_load'])()
|
||||
except Exception:
|
||||
logging.exception('Failed to load server-wide module `%s`', m)
|
||||
|
||||
setup_pid_file()
|
||||
logger = logging.getLogger('server')
|
||||
logger.info('OpenERP server is running, waiting for connections...')
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
""" OpenERP core library.
|
||||
|
||||
"""
|
||||
# The hard-coded super-user id (a.k.a. administrator, or root user).
|
||||
SUPERUSER_ID = 1
|
||||
|
||||
import addons
|
||||
import conf
|
||||
|
@ -41,6 +43,7 @@ import tiny_socket
|
|||
import tools
|
||||
import wizard
|
||||
import workflow
|
||||
import wsgi
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -22,10 +22,15 @@
|
|||
|
||||
""" Addons module.
|
||||
|
||||
This module only serves to contain OpenERP addons. For the code to
|
||||
manage those addons, see openerp.modules. This module conveniently
|
||||
reexports some symbols from openerp.modules. Importing them from here
|
||||
is deprecated.
|
||||
This module serves to contain all OpenERP addons, across all configured addons
|
||||
paths. For the code to manage those addons, see openerp.modules.
|
||||
|
||||
Addons are made available here (i.e. under openerp.addons) after
|
||||
openerp.tools.config.parse_config() is called (so that the addons paths
|
||||
are known).
|
||||
|
||||
This module also conveniently reexports some symbols from openerp.modules.
|
||||
Importing them from here is deprecated.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import module
|
|||
import res
|
||||
import publisher_warranty
|
||||
import report
|
||||
import test
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -92,6 +92,10 @@
|
|||
'test/test_osv_expression.yml',
|
||||
'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules.
|
||||
'test/test_ir_values.yml',
|
||||
# Commented because this takes some time.
|
||||
# This must be (un)commented with the corresponding import statement
|
||||
# in test/__init__.py.
|
||||
# 'test/test_ir_cron.yml', # <-- These tests perform a roolback.
|
||||
],
|
||||
'installable': True,
|
||||
'active': True,
|
||||
|
|
|
@ -1002,7 +1002,7 @@
|
|||
</record>
|
||||
|
||||
<record id="main_partner" model="res.partner">
|
||||
<field name="name">OpenERP S.A.</field>
|
||||
<field name="name">Company Name</field>
|
||||
<!-- Address and Company ID will be set later -->
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="company_id" eval="None"/>
|
||||
|
@ -1010,13 +1010,13 @@
|
|||
</record>
|
||||
<record id="main_address" model="res.partner.address">
|
||||
<field name="partner_id" ref="main_partner"/>
|
||||
<field name="name">Fabien Pinckaers</field>
|
||||
<field name="street">Chaussee de Namur 40</field>
|
||||
<field name="zip">1367</field>
|
||||
<field name="city">Gerompont</field>
|
||||
<field name="phone">(+32).81.81.37.00</field>
|
||||
<field name="name">Company contact name</field>
|
||||
<field name="street">Company street, number</field>
|
||||
<field name="zip">Company zip</field>
|
||||
<field name="city">Company city</field>
|
||||
<field name="phone">+1-212-555-12345</field>
|
||||
<field name="type">default</field>
|
||||
<field model="res.country" name="country_id" ref="be"/>
|
||||
<field model="res.country" name="country_id" ref="us"/>
|
||||
<!-- Company ID will be set later -->
|
||||
<field name="company_id" eval="None"/>
|
||||
</record>
|
||||
|
@ -1038,19 +1038,14 @@
|
|||
|
||||
<!-- Basic Company -->
|
||||
<record id="main_company" model="res.company">
|
||||
<field name="name">OpenERP S.A.</field>
|
||||
<field name="name">Company Name</field>
|
||||
<field name="partner_id" ref="main_partner"/>
|
||||
<field name="rml_header1">Free Business Solutions</field>
|
||||
<field name="rml_footer1">Web: http://www.openerp.com - Tel: (+32).81.81.37.00 - Bank: CPH 126-2013269-07</field>
|
||||
<field name="rml_footer2">IBAN: BE74 1262 0132 6907 - SWIFT: CPHBBE75 - VAT: BE0477.472.701</field>
|
||||
<field name="rml_header1">Company business slogan</field>
|
||||
<field name="rml_footer1">Web: www.companyname.com - Tel: +1-212-555-12345</field>
|
||||
<field name="rml_footer2">IBAN: XX12 3456 7890 1234 5678 - SWIFT: SWIFTCODE - VAT: Company vat number</field>
|
||||
<field name="currency_id" ref="base.EUR"/>
|
||||
</record>
|
||||
|
||||
<assert id="main_company" model="res.company">
|
||||
<test expr="currency_id.name == 'eur'.upper()"/>
|
||||
<test expr="name">OpenERP S.A.</test>
|
||||
</assert>
|
||||
|
||||
<record model="res.users" id="base.user_root">
|
||||
<field name="signature">Administrator</field>
|
||||
<field name="company_id" ref="main_company"/>
|
||||
|
@ -1086,16 +1081,17 @@
|
|||
<field eval="time.strftime('%Y-01-01')" name="name"/>
|
||||
</record>
|
||||
|
||||
<record id="VEB" model="res.currency">
|
||||
<field name="name">VEB</field>
|
||||
<field name="symbol">Bs</field>
|
||||
<field name="rounding">2.95</field>
|
||||
<!-- VEF was previously VEB -->
|
||||
<record id="VEF" model="res.currency">
|
||||
<field name="name">VEF</field>
|
||||
<field name="symbol">Bs.F</field>
|
||||
<field name="rounding">0.0001</field>
|
||||
<field name="accuracy">4</field>
|
||||
<field name="company_id" ref="main_company"/>
|
||||
</record>
|
||||
<record id="rateVEB" model="res.currency.rate">
|
||||
<field name="rate">2768.45</field>
|
||||
<field name="currency_id" ref="VEB"/>
|
||||
<record id="rateVEF" model="res.currency.rate">
|
||||
<field name="rate">5.864</field>
|
||||
<field name="currency_id" ref="VEF"/>
|
||||
<field eval="time.strftime('%Y-01-01')" name="name"/>
|
||||
</record>
|
||||
|
||||
|
@ -1599,6 +1595,7 @@
|
|||
<field name="rounding">0.01</field>
|
||||
<field name="accuracy">4</field>
|
||||
<field name="symbol">¢</field>
|
||||
<field name="company_id" ref="main_company"/>
|
||||
</record>
|
||||
<record id="rateCRC" model="res.currency.rate">
|
||||
<field name="rate">691.3153</field>
|
||||
|
|
|
@ -75,29 +75,29 @@
|
|||
-->
|
||||
|
||||
<record id="view_users_form_simple_modif" model="ir.ui.view">
|
||||
<field name="name">res.users.form.modif</field>
|
||||
<field name="name">res.users.preferences.form</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="type">form</field>
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Users">
|
||||
<field name="name"/>
|
||||
<field name="name" readonly="1"/>
|
||||
<newline/>
|
||||
<group colspan="2" col="2">
|
||||
<separator string="Preferences" colspan="2"/>
|
||||
<field name="view"/>
|
||||
<field name="context_lang"/>
|
||||
<field name="context_tz"/>
|
||||
<field name="menu_tips"/>
|
||||
<field name="view" readonly="0"/>
|
||||
<field name="context_lang" readonly="0"/>
|
||||
<field name="context_tz" readonly="0"/>
|
||||
<field name="menu_tips" readonly="0"/>
|
||||
</group>
|
||||
<group name="default_filters" colspan="2" col="2">
|
||||
<separator string="Default Filters" colspan="2"/>
|
||||
<field name="company_id" widget="selection"
|
||||
<field name="company_id" widget="selection" readonly="0"
|
||||
groups="base.group_multi_company" on_change="on_change_company_id(company_id)"/>
|
||||
</group>
|
||||
<separator string="Email Preferences" colspan="4"/>
|
||||
<field colspan="4" name="user_email" widget="email"/>
|
||||
<field colspan="4" name="signature"/>
|
||||
<field colspan="4" name="user_email" widget="email" readonly="0"/>
|
||||
<field colspan="4" name="signature" readonly="0"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -147,7 +147,7 @@
|
|||
<page string="Access Rights">
|
||||
<field nolabel="1" name="groups_id"/>
|
||||
</page>
|
||||
<page string="Companies" groups="base.group_multi_company">
|
||||
<page string="Allowed Companies" groups="base.group_multi_company">
|
||||
<field colspan="4" nolabel="1" name="company_ids" select="1"/>
|
||||
</page>
|
||||
</notebook>
|
||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: openobject-server\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
|
||||
"PO-Revision-Date: 2011-09-13 11:47+0000\n"
|
||||
"PO-Revision-Date: 2011-09-16 16:25+0000\n"
|
||||
"Last-Translator: Jiří Hajda <robie@centrum.cz>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2011-09-14 04:40+0000\n"
|
||||
"X-Generator: Launchpad (build 13921)\n"
|
||||
"X-Launchpad-Export-Date: 2011-09-17 04:54+0000\n"
|
||||
"X-Generator: Launchpad (build 13955)\n"
|
||||
"X-Poedit-Language: Czech\n"
|
||||
|
||||
#. module: base
|
||||
|
@ -3812,7 +3812,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: view:publisher_warranty.contract.wizard:0
|
||||
msgid "Please enter the serial key provided in your contract document:"
|
||||
msgstr "Prosíme zadejte sériové číslo poskytnuté ve dokumentu vaší smlouvy:"
|
||||
msgstr "Prosíme zadejte sériové číslo poskytnuté v dokumentu vaší smlouvy:"
|
||||
|
||||
#. module: base
|
||||
#: view:workflow.activity:0
|
||||
|
|
|
@ -8,14 +8,14 @@ msgstr ""
|
|||
"Project-Id-Version: openobject-server\n"
|
||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
|
||||
"PO-Revision-Date: 2011-07-28 15:35+0000\n"
|
||||
"PO-Revision-Date: 2011-09-22 14:47+0000\n"
|
||||
"Last-Translator: John Bradshaw <Unknown>\n"
|
||||
"Language-Team: English (United Kingdom) <en_GB@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2011-09-01 04:44+0000\n"
|
||||
"X-Generator: Launchpad (build 13827)\n"
|
||||
"X-Launchpad-Export-Date: 2011-09-23 04:38+0000\n"
|
||||
"X-Generator: Launchpad (build 14012)\n"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.filters:0
|
||||
|
@ -1972,7 +1972,7 @@ msgstr "Iteration Actions"
|
|||
#. module: base
|
||||
#: help:multi_company.default,company_id:0
|
||||
msgid "Company where the user is connected"
|
||||
msgstr ""
|
||||
msgstr "Company where the user is connected"
|
||||
|
||||
#. module: base
|
||||
#: field:publisher_warranty.contract,date_stop:0
|
||||
|
@ -2583,7 +2583,7 @@ msgstr "Search Actions"
|
|||
#: model:ir.actions.act_window,name:base.action_view_partner_wizard_ean_check
|
||||
#: view:partner.wizard.ean.check:0
|
||||
msgid "Ean check"
|
||||
msgstr ""
|
||||
msgstr "Ean check"
|
||||
|
||||
#. module: base
|
||||
#: field:res.partner,vat:0
|
||||
|
@ -2623,7 +2623,7 @@ msgstr "GPL-2 or later version"
|
|||
#. module: base
|
||||
#: model:res.partner.title,shortcut:base.res_partner_title_sir
|
||||
msgid "M."
|
||||
msgstr ""
|
||||
msgstr "M."
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/module/module.py:429
|
||||
|
@ -3001,7 +3001,7 @@ msgstr "License"
|
|||
#. module: base
|
||||
#: field:ir.attachment,url:0
|
||||
msgid "Url"
|
||||
msgstr ""
|
||||
msgstr "Url"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.actions.todo,restart:0
|
||||
|
@ -3153,7 +3153,7 @@ msgstr "Workflows"
|
|||
#. module: base
|
||||
#: field:ir.translation,xml_id:0
|
||||
msgid "XML Id"
|
||||
msgstr ""
|
||||
msgstr "XML Id"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.action_config_user_form
|
||||
|
@ -3224,7 +3224,7 @@ msgstr "Abkhazian / аҧсуа"
|
|||
#. module: base
|
||||
#: view:base.module.configuration:0
|
||||
msgid "System Configuration Done"
|
||||
msgstr ""
|
||||
msgstr "System Configuration Done"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/orm.py:929
|
||||
|
@ -3271,7 +3271,7 @@ msgstr "That contract is already registered in the system."
|
|||
#. module: base
|
||||
#: help:ir.sequence,suffix:0
|
||||
msgid "Suffix value of the record for the sequence"
|
||||
msgstr ""
|
||||
msgstr "Suffix value of the record for the sequence"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -3343,7 +3343,7 @@ msgstr "Installed"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Ukrainian / українська"
|
||||
msgstr ""
|
||||
msgstr "Ukrainian / українська"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.action_translation
|
||||
|
@ -3389,7 +3389,7 @@ msgstr "Next Number"
|
|||
#. module: base
|
||||
#: help:workflow.transition,condition:0
|
||||
msgid "Expression to be satisfied if we want the transition done."
|
||||
msgstr ""
|
||||
msgstr "Expression to be satisfied if we want the transition done."
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -3518,6 +3518,12 @@ msgid ""
|
|||
"Would your payment have been carried out after this mail was sent, please "
|
||||
"consider the present one as void."
|
||||
msgstr ""
|
||||
"Please note that the following payments are now due. If your payment "
|
||||
" has been sent, kindly forward your payment details. If "
|
||||
"payment will be delayed, please contact us to "
|
||||
"discuss. \n"
|
||||
"If payment was performed after this mail was sent, please consider the "
|
||||
"present one as void."
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.mx
|
||||
|
@ -3673,6 +3679,8 @@ msgid ""
|
|||
"If set to true, the action will not be displayed on the right toolbar of a "
|
||||
"form view"
|
||||
msgstr ""
|
||||
"If set to true, the action will not be displayed on the right toolbar of a "
|
||||
"form view"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.ms
|
||||
|
@ -3717,7 +3725,7 @@ msgstr "English (UK)"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Japanese / 日本語"
|
||||
msgstr ""
|
||||
msgstr "Japanese / 日本語"
|
||||
|
||||
#. module: base
|
||||
#: help:workflow.transition,act_from:0
|
||||
|
@ -3725,6 +3733,8 @@ msgid ""
|
|||
"Source activity. When this activity is over, the condition is tested to "
|
||||
"determine if we can start the ACT_TO activity."
|
||||
msgstr ""
|
||||
"Source activity. When this activity is over, the condition is tested to "
|
||||
"determine if we can start the ACT_TO activity."
|
||||
|
||||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_3
|
||||
|
@ -3885,7 +3895,7 @@ msgstr "Init Date"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Gujarati / ગુજરાતી"
|
||||
msgstr ""
|
||||
msgstr "Gujarati / ગુજરાતી"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/module/module.py:257
|
||||
|
@ -3953,6 +3963,9 @@ msgid ""
|
|||
"form, signal tests the name of the pressed button. If signal is NULL, no "
|
||||
"button is necessary to validate this transition."
|
||||
msgstr ""
|
||||
"When the operation of transition comes from a button press in the client "
|
||||
"form, signal tests the name of the pressed button. If signal is NULL, no "
|
||||
"button is necessary to validate this transition."
|
||||
|
||||
#. module: base
|
||||
#: help:multi_company.default,object_id:0
|
||||
|
@ -3972,7 +3985,7 @@ msgstr "Menu Name"
|
|||
#. module: base
|
||||
#: view:ir.module.module:0
|
||||
msgid "Author Website"
|
||||
msgstr ""
|
||||
msgstr "Author Website"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.attachment:0
|
||||
|
@ -4012,6 +4025,8 @@ msgid ""
|
|||
"Whether values for this field can be translated (enables the translation "
|
||||
"mechanism for that field)"
|
||||
msgstr ""
|
||||
"Whether values for this field can be translated (enables the translation "
|
||||
"mechanism for that field)"
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
|
@ -4072,13 +4087,13 @@ msgstr "Price Accuracy"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Latvian / latviešu valoda"
|
||||
msgstr ""
|
||||
msgstr "Latvian / latviešu valoda"
|
||||
|
||||
#. module: base
|
||||
#: view:res.config:0
|
||||
#: view:res.config.installer:0
|
||||
msgid "vsep"
|
||||
msgstr ""
|
||||
msgstr "vsep"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -4099,7 +4114,7 @@ msgstr "Workitem"
|
|||
#. module: base
|
||||
#: view:ir.actions.todo:0
|
||||
msgid "Set as Todo"
|
||||
msgstr ""
|
||||
msgstr "Set as Todo"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.act_window.view,act_window_id:0
|
||||
|
@ -4177,7 +4192,7 @@ msgstr "Menus"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Serbian (Latin) / srpski"
|
||||
msgstr ""
|
||||
msgstr "Serbian (Latin) / srpski"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.il
|
||||
|
@ -4353,7 +4368,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: view:base.language.import:0
|
||||
msgid "- module,type,name,res_id,src,value"
|
||||
msgstr ""
|
||||
msgstr "- module,type,name,res_id,src,value"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -4372,7 +4387,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: help:ir.model.fields,relation:0
|
||||
msgid "For relationship fields, the technical name of the target model"
|
||||
msgstr ""
|
||||
msgstr "For relationship fields, the technical name of the target model"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -4387,7 +4402,7 @@ msgstr "Inherited View"
|
|||
#. module: base
|
||||
#: view:ir.translation:0
|
||||
msgid "Source Term"
|
||||
msgstr ""
|
||||
msgstr "Source Term"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.menu_main_pm
|
||||
|
@ -4397,7 +4412,7 @@ msgstr "Project"
|
|||
#. module: base
|
||||
#: field:ir.ui.menu,web_icon_hover_data:0
|
||||
msgid "Web Icon Image (hover)"
|
||||
msgstr ""
|
||||
msgstr "Web Icon Image (hover)"
|
||||
|
||||
#. module: base
|
||||
#: view:base.module.import:0
|
||||
|
@ -4417,7 +4432,7 @@ msgstr "Create User"
|
|||
#. module: base
|
||||
#: view:partner.clear.ids:0
|
||||
msgid "Want to Clear Ids ? "
|
||||
msgstr ""
|
||||
msgstr "Want to Clear Ids ? "
|
||||
|
||||
#. module: base
|
||||
#: field:publisher_warranty.contract,name:0
|
||||
|
@ -4469,17 +4484,17 @@ msgstr "Fed. State"
|
|||
#. module: base
|
||||
#: field:ir.actions.server,copy_object:0
|
||||
msgid "Copy Of"
|
||||
msgstr ""
|
||||
msgstr "Copy Of"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.model,osv_memory:0
|
||||
msgid "In-memory model"
|
||||
msgstr ""
|
||||
msgstr "In-memory model"
|
||||
|
||||
#. module: base
|
||||
#: view:partner.clear.ids:0
|
||||
msgid "Clear Ids"
|
||||
msgstr ""
|
||||
msgstr "Clear Ids"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.io
|
||||
|
@ -4501,7 +4516,7 @@ msgstr "Field Mapping"
|
|||
#. module: base
|
||||
#: view:publisher_warranty.contract:0
|
||||
msgid "Refresh Validation Dates"
|
||||
msgstr ""
|
||||
msgstr "Refresh Validation Dates"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.model:0
|
||||
|
@ -4572,7 +4587,7 @@ msgstr "_Ok"
|
|||
#. module: base
|
||||
#: help:ir.filters,user_id:0
|
||||
msgid "False means for every user"
|
||||
msgstr ""
|
||||
msgstr "False means for every user"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/module/module.py:198
|
||||
|
@ -4621,6 +4636,7 @@ msgstr "Contacts"
|
|||
msgid ""
|
||||
"Unable to delete this document because it is used as a default property"
|
||||
msgstr ""
|
||||
"Unable to delete this document because it is used as a default property"
|
||||
|
||||
#. module: base
|
||||
#: view:res.widget.wizard:0
|
||||
|
@ -4674,7 +4690,7 @@ msgstr ""
|
|||
#: code:addons/orm.py:1350
|
||||
#, python-format
|
||||
msgid "Insufficient fields for Calendar View!"
|
||||
msgstr ""
|
||||
msgstr "Insufficient fields for Calendar View!"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.property,type:0
|
||||
|
@ -4687,6 +4703,8 @@ msgid ""
|
|||
"The path to the main report file (depending on Report Type) or NULL if the "
|
||||
"content is in another data field"
|
||||
msgstr ""
|
||||
"The path to the main report file (depending on Report Type) or NULL if the "
|
||||
"content is in another data field"
|
||||
|
||||
#. module: base
|
||||
#: help:res.config.users,company_id:0
|
||||
|
@ -4748,7 +4766,7 @@ msgstr "Close"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Spanish (MX) / Español (MX)"
|
||||
msgstr ""
|
||||
msgstr "Spanish (MX) / Español (MX)"
|
||||
|
||||
#. module: base
|
||||
#: view:res.log:0
|
||||
|
@ -4783,7 +4801,7 @@ msgstr "Publisher Warranty Contracts"
|
|||
#. module: base
|
||||
#: help:res.log,name:0
|
||||
msgid "The logging message."
|
||||
msgstr ""
|
||||
msgstr "The logging message."
|
||||
|
||||
#. module: base
|
||||
#: field:base.language.export,format:0
|
||||
|
@ -5018,7 +5036,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: help:ir.cron,interval_number:0
|
||||
msgid "Repeat every x."
|
||||
msgstr ""
|
||||
msgstr "Repeat every x."
|
||||
|
||||
#. module: base
|
||||
#: wizard_view:server.action.create,step_1:0
|
||||
|
@ -5078,6 +5096,8 @@ msgid ""
|
|||
"If specified, this action will be opened at logon for this user, in addition "
|
||||
"to the standard menu."
|
||||
msgstr ""
|
||||
"If specified, this action will be opened at logon for this user, in addition "
|
||||
"to the standard menu."
|
||||
|
||||
#. module: base
|
||||
#: view:ir.values:0
|
||||
|
@ -5088,7 +5108,7 @@ msgstr "Client Actions"
|
|||
#: code:addons/orm.py:1806
|
||||
#, python-format
|
||||
msgid "The exists method is not implemented on this object !"
|
||||
msgstr ""
|
||||
msgstr "The exists method is not implemented on this object !"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/module/module.py:336
|
||||
|
@ -5113,7 +5133,7 @@ msgstr "Connect Events to Actions"
|
|||
#. module: base
|
||||
#: model:ir.model,name:base.model_base_update_translations
|
||||
msgid "base.update.translations"
|
||||
msgstr ""
|
||||
msgstr "base.update.translations"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.module.category,parent_id:0
|
||||
|
@ -5124,7 +5144,7 @@ msgstr "Parent Category"
|
|||
#. module: base
|
||||
#: selection:ir.property,type:0
|
||||
msgid "Integer Big"
|
||||
msgstr ""
|
||||
msgstr "Integer Big"
|
||||
|
||||
#. module: base
|
||||
#: selection:res.partner.address,type:0
|
||||
|
@ -5158,7 +5178,7 @@ msgstr "Communication"
|
|||
#. module: base
|
||||
#: view:ir.actions.report.xml:0
|
||||
msgid "RML Report"
|
||||
msgstr ""
|
||||
msgstr "RML Report"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_server_object_lines
|
||||
|
@ -5206,7 +5226,7 @@ msgstr "Nigeria"
|
|||
#: code:addons/base/ir/ir_model.py:250
|
||||
#, python-format
|
||||
msgid "For selection fields, the Selection Options must be given!"
|
||||
msgstr ""
|
||||
msgstr "For selection fields, the Selection Options must be given!"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.action_partner_sms_send
|
||||
|
@ -5254,6 +5274,13 @@ msgid ""
|
|||
"installed the CRM, with the history tab, you can track all the interactions "
|
||||
"with a partner such as opportunities, emails, or sales orders issued."
|
||||
msgstr ""
|
||||
"Customers (also called Partners in other areas of the system) helps you "
|
||||
"manage your address book of companies whether they are prospects, customers "
|
||||
"and/or suppliers. The partner form allows you to track and record all the "
|
||||
"necessary information to interact with your partners from the company "
|
||||
"address to their contacts as well as pricelists, and much more. If you "
|
||||
"installed the CRM, with the history tab, you can track all interactions with "
|
||||
"a partner such as opportunities, emails, or sales orders issued."
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.ph
|
||||
|
@ -5278,7 +5305,7 @@ msgstr "Content"
|
|||
#. module: base
|
||||
#: help:ir.rule,global:0
|
||||
msgid "If no group is specified the rule is global and applied to everyone"
|
||||
msgstr ""
|
||||
msgstr "If no group is specified the rule is global and applied to everyone"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.td
|
||||
|
@ -5355,6 +5382,9 @@ msgid ""
|
|||
"groups. If this field is empty, OpenERP will compute visibility based on the "
|
||||
"related object's read access."
|
||||
msgstr ""
|
||||
"If you have groups, the visibility of this menu will be based on these "
|
||||
"groups. If this field is empty, OpenERP will compute visibility based on the "
|
||||
"related object's read access."
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.action_ui_view_custom
|
||||
|
@ -5496,7 +5526,7 @@ msgstr "Spanish (EC) / Español (EC)"
|
|||
#. module: base
|
||||
#: help:ir.ui.view,xml_id:0
|
||||
msgid "ID of the view defined in xml file"
|
||||
msgstr ""
|
||||
msgstr "ID of the view defined in xml file"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_base_module_import
|
||||
|
@ -5512,7 +5542,7 @@ msgstr "American Samoa"
|
|||
#. module: base
|
||||
#: help:ir.actions.act_window,res_model:0
|
||||
msgid "Model name of the object to open in the view window"
|
||||
msgstr ""
|
||||
msgstr "Model name of the object to open in the view window"
|
||||
|
||||
#. module: base
|
||||
#: field:res.log,secondary:0
|
||||
|
@ -5692,11 +5722,15 @@ msgid ""
|
|||
"Warning: if \"email_from\" and \"smtp_server\" aren't configured, it won't "
|
||||
"be possible to email new users."
|
||||
msgstr ""
|
||||
"If an email is provided, the user will be sent a message welcoming them.\n"
|
||||
"\n"
|
||||
"Warning: if \"email_from\" and \"smtp_server\" aren't configured, it won't "
|
||||
"be possible to email new users."
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Flemish (BE) / Vlaams (BE)"
|
||||
msgstr ""
|
||||
msgstr "Flemish (BE) / Vlaams (BE)"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.cron,interval_number:0
|
||||
|
@ -5746,7 +5780,7 @@ msgstr "ir.actions.todo"
|
|||
#: code:addons/base/res/res_config.py:94
|
||||
#, python-format
|
||||
msgid "Couldn't find previous ir.actions.todo"
|
||||
msgstr ""
|
||||
msgstr "Couldn't find previous ir.actions.todo"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.actions.act_window:0
|
||||
|
@ -5761,7 +5795,7 @@ msgstr "Custom Shortcuts"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Vietnamese / Tiếng Việt"
|
||||
msgstr ""
|
||||
msgstr "Vietnamese / Tiếng Việt"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.dz
|
||||
|
@ -5776,7 +5810,7 @@ msgstr "Belgium"
|
|||
#. module: base
|
||||
#: model:ir.model,name:base.model_osv_memory_autovacuum
|
||||
msgid "osv_memory.autovacuum"
|
||||
msgstr ""
|
||||
msgstr "osv_memory.autovacuum"
|
||||
|
||||
#. module: base
|
||||
#: field:base.language.export,lang:0
|
||||
|
@ -5809,30 +5843,30 @@ msgstr "Companies"
|
|||
#. module: base
|
||||
#: view:res.lang:0
|
||||
msgid "%H - Hour (24-hour clock) [00,23]."
|
||||
msgstr ""
|
||||
msgstr "%H - Hour (24-hour clock) [00,23]."
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_res_widget
|
||||
msgid "res.widget"
|
||||
msgstr ""
|
||||
msgstr "res.widget"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_model.py:258
|
||||
#, python-format
|
||||
msgid "Model %s does not exist!"
|
||||
msgstr ""
|
||||
msgstr "Model %s does not exist!"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_lang.py:159
|
||||
#, python-format
|
||||
msgid "You cannot delete the language which is User's Preferred Language !"
|
||||
msgstr ""
|
||||
msgstr "You cannot delete the language which is User's Preferred Language !"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/fields.py:103
|
||||
#, python-format
|
||||
msgid "Not implemented get_memory method !"
|
||||
msgstr ""
|
||||
msgstr "Not implemented get_memory method !"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.actions.server:0
|
||||
|
@ -5879,7 +5913,7 @@ msgstr "Neutral Zone"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Hindi / हिंदी"
|
||||
msgstr ""
|
||||
msgstr "Hindi / हिंदी"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.model:0
|
||||
|
@ -5926,7 +5960,7 @@ msgstr "Window Actions"
|
|||
#. module: base
|
||||
#: view:res.lang:0
|
||||
msgid "%I - Hour (12-hour clock) [01,12]."
|
||||
msgstr ""
|
||||
msgstr "%I - Hour (12-hour clock) [01,12]."
|
||||
|
||||
#. module: base
|
||||
#: selection:publisher_warranty.contract.wizard,state:0
|
||||
|
@ -5964,12 +5998,14 @@ msgid ""
|
|||
"View type: set to 'tree' for a hierarchical tree view, or 'form' for other "
|
||||
"views"
|
||||
msgstr ""
|
||||
"View type: set to 'tree' for a hierarchical tree view, or 'form' for other "
|
||||
"views"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_config.py:421
|
||||
#, python-format
|
||||
msgid "Click 'Continue' to configure the next addon..."
|
||||
msgstr ""
|
||||
msgstr "Click 'Continue' to configure the next addon..."
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.server,record_id:0
|
||||
|
@ -6010,7 +6046,7 @@ msgstr ""
|
|||
#: code:addons/base/ir/ir_actions.py:629
|
||||
#, python-format
|
||||
msgid "Please specify server option --email-from !"
|
||||
msgstr ""
|
||||
msgstr "Please specify server option --email-from !"
|
||||
|
||||
#. module: base
|
||||
#: field:base.language.import,name:0
|
||||
|
@ -6070,6 +6106,7 @@ msgid ""
|
|||
"It gives the status if the tip has to be displayed or not when a user "
|
||||
"executes an action"
|
||||
msgstr ""
|
||||
"It shows if the tip is to be displayed or not when a user executes an action"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.model:0
|
||||
|
@ -6126,7 +6163,7 @@ msgstr "Code"
|
|||
#. module: base
|
||||
#: model:ir.model,name:base.model_res_config_installer
|
||||
msgid "res.config.installer"
|
||||
msgstr ""
|
||||
msgstr "res.config.installer"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.mc
|
||||
|
@ -6170,7 +6207,7 @@ msgstr "Sequence Codes"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Spanish (CO) / Español (CO)"
|
||||
msgstr ""
|
||||
msgstr "Spanish (CO) / Español (CO)"
|
||||
|
||||
#. module: base
|
||||
#: view:base.module.configuration:0
|
||||
|
@ -6178,6 +6215,8 @@ msgid ""
|
|||
"All pending configuration wizards have been executed. You may restart "
|
||||
"individual wizards via the list of configuration wizards."
|
||||
msgstr ""
|
||||
"All pending configuration wizards have been executed. You may restart "
|
||||
"individual wizards via the list of configuration wizards."
|
||||
|
||||
#. module: base
|
||||
#: wizard_button:server.action.create,step_1,create:0
|
||||
|
@ -6187,7 +6226,7 @@ msgstr "Create"
|
|||
#. module: base
|
||||
#: view:ir.sequence:0
|
||||
msgid "Current Year with Century: %(year)s"
|
||||
msgstr ""
|
||||
msgstr "Current Year with Century: %(year)s"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.exports,export_fields:0
|
||||
|
@ -6202,13 +6241,13 @@ msgstr "France"
|
|||
#. module: base
|
||||
#: model:ir.model,name:base.model_res_log
|
||||
msgid "res.log"
|
||||
msgstr ""
|
||||
msgstr "res.log"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.translation,module:0
|
||||
#: help:ir.translation,xml_id:0
|
||||
msgid "Maps to the ir_model_data for which this translation is provided."
|
||||
msgstr ""
|
||||
msgstr "Maps to the ir_model_data for which this translation is provided."
|
||||
|
||||
#. module: base
|
||||
#: view:workflow.activity:0
|
||||
|
@ -6302,7 +6341,7 @@ msgstr "Todo"
|
|||
#. module: base
|
||||
#: field:ir.attachment,datas:0
|
||||
msgid "File Content"
|
||||
msgstr ""
|
||||
msgstr "File Content"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.pa
|
||||
|
@ -6319,12 +6358,13 @@ msgstr "Ltd"
|
|||
msgid ""
|
||||
"The group that a user must have to be authorized to validate this transition."
|
||||
msgstr ""
|
||||
"The group that a user must have to be authorized to validate this transition."
|
||||
|
||||
#. module: base
|
||||
#: constraint:res.config.users:0
|
||||
#: constraint:res.users:0
|
||||
msgid "The chosen company is not in the allowed companies for this user"
|
||||
msgstr ""
|
||||
msgstr "The chosen company is not in the allowed companies for this user"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.gi
|
||||
|
@ -6346,6 +6386,7 @@ msgstr "Pitcairn Island"
|
|||
msgid ""
|
||||
"We suggest to reload the menu tab to see the new menus (Ctrl+T then Ctrl+R)."
|
||||
msgstr ""
|
||||
"We suggest reloading the menu tab to see the new menus (Ctrl+T then Ctrl+R)."
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.action_rule
|
||||
|
@ -6398,7 +6439,7 @@ msgstr "Search View"
|
|||
#. module: base
|
||||
#: sql_constraint:res.lang:0
|
||||
msgid "The code of the language must be unique !"
|
||||
msgstr ""
|
||||
msgstr "The code of the language must be unique !"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.action_attachment
|
||||
|
@ -6441,7 +6482,7 @@ msgstr "Write Access"
|
|||
#. module: base
|
||||
#: view:res.lang:0
|
||||
msgid "%m - Month number [01,12]."
|
||||
msgstr ""
|
||||
msgstr "%m - Month number [01,12]."
|
||||
|
||||
#. module: base
|
||||
#: field:res.bank,city:0
|
||||
|
@ -6499,7 +6540,7 @@ msgstr "English (US)"
|
|||
#: view:ir.model.data:0
|
||||
#: model:ir.ui.menu,name:base.ir_model_data_menu
|
||||
msgid "Object Identifiers"
|
||||
msgstr ""
|
||||
msgstr "Object Identifiers"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,help:base.action_partner_title_partner
|
||||
|
@ -6507,11 +6548,13 @@ msgid ""
|
|||
"Manage the partner titles you want to have available in your system. The "
|
||||
"partner titles is the legal status of the company: Private Limited, SA, etc."
|
||||
msgstr ""
|
||||
"Manage the partner titles you want to have available in your system. The "
|
||||
"partner title is the legal status of the company: Private Limited, SA, etc."
|
||||
|
||||
#. module: base
|
||||
#: view:base.language.export:0
|
||||
msgid "To browse official translations, you can start with these links:"
|
||||
msgstr ""
|
||||
msgstr "To browse official translations, you can start with these links:"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_model.py:484
|
||||
|
@ -6520,6 +6563,8 @@ msgid ""
|
|||
"You can not read this document (%s) ! Be sure your user belongs to one of "
|
||||
"these groups: %s."
|
||||
msgstr ""
|
||||
"You can not read this document (%s) ! Be sure your user belongs to one of "
|
||||
"these groups: %s."
|
||||
|
||||
#. module: base
|
||||
#: view:res.bank:0
|
||||
|
@ -6538,7 +6583,7 @@ msgstr "Installed version"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Mongolian / монгол"
|
||||
msgstr ""
|
||||
msgstr "Mongolian / монгол"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.mr
|
||||
|
@ -6553,7 +6598,7 @@ msgstr "ir.translation"
|
|||
#. module: base
|
||||
#: view:base.module.update:0
|
||||
msgid "Module update result"
|
||||
msgstr ""
|
||||
msgstr "Module update result"
|
||||
|
||||
#. module: base
|
||||
#: view:workflow.activity:0
|
||||
|
@ -6575,7 +6620,7 @@ msgstr "Parent Company"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Spanish (CR) / Español (CR)"
|
||||
msgstr ""
|
||||
msgstr "Spanish (CR) / Español (CR)"
|
||||
|
||||
#. module: base
|
||||
#: field:res.currency.rate,rate:0
|
||||
|
@ -6615,6 +6660,9 @@ msgid ""
|
|||
"for the currency: %s \n"
|
||||
"at the date: %s"
|
||||
msgstr ""
|
||||
"No rate found \n"
|
||||
"for the currency: %s \n"
|
||||
"at the date: %s"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,help:base.action_ui_view_custom
|
||||
|
@ -6622,6 +6670,8 @@ msgid ""
|
|||
"Customized views are used when users reorganize the content of their "
|
||||
"dashboard views (via web client)"
|
||||
msgstr ""
|
||||
"Customised views are used when users reorganise the content of their "
|
||||
"dashboard views (via web client)"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.model,name:0
|
||||
|
@ -6660,7 +6710,7 @@ msgstr "Icon"
|
|||
#. module: base
|
||||
#: help:ir.model.fields,model_id:0
|
||||
msgid "The model this field belongs to"
|
||||
msgstr ""
|
||||
msgstr "The model this field belongs to"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.mq
|
||||
|
@ -6670,7 +6720,7 @@ msgstr "Martinique (French)"
|
|||
#. module: base
|
||||
#: view:ir.sequence.type:0
|
||||
msgid "Sequences Type"
|
||||
msgstr ""
|
||||
msgstr "Sequences Type"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.res_request-act
|
||||
|
@ -6694,7 +6744,7 @@ msgstr "Or"
|
|||
#: model:ir.actions.act_window,name:base.res_log_act_window
|
||||
#: model:ir.ui.menu,name:base.menu_res_log_act_window
|
||||
msgid "Client Logs"
|
||||
msgstr ""
|
||||
msgstr "Client Logs"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.al
|
||||
|
@ -6713,6 +6763,8 @@ msgid ""
|
|||
"You cannot delete the language which is Active !\n"
|
||||
"Please de-activate the language first."
|
||||
msgstr ""
|
||||
"You cannot delete a language which is active !\n"
|
||||
"Please de-activate the language first."
|
||||
|
||||
#. module: base
|
||||
#: view:base.language.install:0
|
||||
|
@ -6721,6 +6773,8 @@ msgid ""
|
|||
"Please be patient, this operation may take a few minutes (depending on the "
|
||||
"number of modules currently installed)..."
|
||||
msgstr ""
|
||||
"Please be patient, this operation may take a few minutes (depending on the "
|
||||
"number of modules currently installed)..."
|
||||
|
||||
#. module: base
|
||||
#: field:ir.ui.menu,child_id:0
|
||||
|
@ -6739,18 +6793,18 @@ msgstr "Problem in configuration `Record Id` in Server Action!"
|
|||
#: code:addons/orm.py:2316
|
||||
#, python-format
|
||||
msgid "ValidateError"
|
||||
msgstr ""
|
||||
msgstr "ValidateError"
|
||||
|
||||
#. module: base
|
||||
#: view:base.module.import:0
|
||||
#: view:base.module.update:0
|
||||
msgid "Open Modules"
|
||||
msgstr ""
|
||||
msgstr "Open Modules"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,help:base.action_res_bank_form
|
||||
msgid "Manage bank records you want to be used in the system."
|
||||
msgstr ""
|
||||
msgstr "Manage bank records you want to be used in the system."
|
||||
|
||||
#. module: base
|
||||
#: view:base.module.import:0
|
||||
|
@ -6768,6 +6822,8 @@ msgid ""
|
|||
"The path to the main report file (depending on Report Type) or NULL if the "
|
||||
"content is in another field"
|
||||
msgstr ""
|
||||
"The path to the main report file (depending on Report Type) or NULL if the "
|
||||
"content is in another field"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.la
|
||||
|
@ -6794,6 +6850,8 @@ msgid ""
|
|||
"The sum of the data (2nd field) is null.\n"
|
||||
"We can't draw a pie chart !"
|
||||
msgstr ""
|
||||
"The sum of the data (2nd field) is null.\n"
|
||||
"We can't draw a pie chart !"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.menu_lunch_reporting
|
||||
|
@ -6815,7 +6873,7 @@ msgstr "Togo"
|
|||
#. module: base
|
||||
#: selection:ir.module.module,license:0
|
||||
msgid "Other Proprietary"
|
||||
msgstr ""
|
||||
msgstr "Other Proprietary"
|
||||
|
||||
#. module: base
|
||||
#: selection:workflow.activity,kind:0
|
||||
|
@ -6826,7 +6884,7 @@ msgstr "Stop All"
|
|||
#: code:addons/orm.py:412
|
||||
#, python-format
|
||||
msgid "The read_group method is not implemented on this object !"
|
||||
msgstr ""
|
||||
msgstr "The read_group method is not implemented on this object !"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.model.data:0
|
||||
|
@ -6846,7 +6904,7 @@ msgstr "Cascade"
|
|||
#. module: base
|
||||
#: field:workflow.transition,group_id:0
|
||||
msgid "Group Required"
|
||||
msgstr ""
|
||||
msgstr "Group Required"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.actions.configuration.wizard:0
|
||||
|
@ -6869,17 +6927,19 @@ msgid ""
|
|||
"Enable this if you want to execute missed occurences as soon as the server "
|
||||
"restarts."
|
||||
msgstr ""
|
||||
"Enable this if you want to execute missed occurences as soon as the server "
|
||||
"restarts."
|
||||
|
||||
#. module: base
|
||||
#: view:base.module.upgrade:0
|
||||
msgid "Start update"
|
||||
msgstr ""
|
||||
msgstr "Start update"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/publisher_warranty/publisher_warranty.py:144
|
||||
#, python-format
|
||||
msgid "Contract validation error"
|
||||
msgstr ""
|
||||
msgstr "Contract validation error"
|
||||
|
||||
#. module: base
|
||||
#: field:res.country.state,name:0
|
||||
|
@ -6906,7 +6966,7 @@ msgstr "ir.actions.report.xml"
|
|||
#. module: base
|
||||
#: model:res.partner.title,shortcut:base.res_partner_title_miss
|
||||
msgid "Mss"
|
||||
msgstr ""
|
||||
msgstr "Mss"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_ui_view
|
||||
|
@ -6916,7 +6976,7 @@ msgstr "ir.ui.view"
|
|||
#. module: base
|
||||
#: constraint:res.partner:0
|
||||
msgid "Error ! You can not create recursive associated members."
|
||||
msgstr ""
|
||||
msgstr "Error ! You can not create recursive associated members."
|
||||
|
||||
#. module: base
|
||||
#: help:res.lang,code:0
|
||||
|
@ -6931,7 +6991,7 @@ msgstr "OpenERP Partners"
|
|||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.menu_hr_manager
|
||||
msgid "HR Manager Dashboard"
|
||||
msgstr ""
|
||||
msgstr "HR Manager Dashboard"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/module/module.py:253
|
||||
|
@ -6939,11 +6999,12 @@ msgstr ""
|
|||
msgid ""
|
||||
"Unable to install module \"%s\" because an external dependency is not met: %s"
|
||||
msgstr ""
|
||||
"Unable to install module \"%s\" because an external dependency is not met: %s"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.module.module:0
|
||||
msgid "Search modules"
|
||||
msgstr ""
|
||||
msgstr "Search modules"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.by
|
||||
|
@ -6968,6 +7029,10 @@ msgid ""
|
|||
"not connect to the system. You can assign them groups in order to give them "
|
||||
"specific access to the applications they need to use in the system."
|
||||
msgstr ""
|
||||
"Create and manage users that will connect to the system. Users can be "
|
||||
"deactivated should there be a period of time during which they will/should "
|
||||
"not connect to the system. You can assign them groups to give them specific "
|
||||
"access to the applications they need to use."
|
||||
|
||||
#. module: base
|
||||
#: selection:res.request,priority:0
|
||||
|
@ -6983,13 +7048,13 @@ msgstr "Street2"
|
|||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.action_view_base_module_update
|
||||
msgid "Module Update"
|
||||
msgstr ""
|
||||
msgstr "Module Update"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/module/wizard/base_module_upgrade.py:95
|
||||
#, python-format
|
||||
msgid "Following modules are not installed or unknown: %s"
|
||||
msgstr ""
|
||||
msgstr "Following modules are not installed or unknown: %s"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.cron:0
|
||||
|
@ -7018,7 +7083,7 @@ msgstr "Open Window"
|
|||
#. module: base
|
||||
#: field:ir.actions.act_window,auto_search:0
|
||||
msgid "Auto Search"
|
||||
msgstr ""
|
||||
msgstr "Auto Search"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.act_window,filter:0
|
||||
|
@ -7064,25 +7129,25 @@ msgstr "Load"
|
|||
#: help:res.config.users,name:0
|
||||
#: help:res.users,name:0
|
||||
msgid "The new user's real name, used for searching and most listings"
|
||||
msgstr ""
|
||||
msgstr "The new user's real name, used for searching and most listings"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/osv.py:154
|
||||
#: code:addons/osv.py:156
|
||||
#, python-format
|
||||
msgid "Integrity Error"
|
||||
msgstr ""
|
||||
msgstr "Integrity Error"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_wizard_screen
|
||||
msgid "ir.wizard.screen"
|
||||
msgstr ""
|
||||
msgstr "ir.wizard.screen"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_model.py:223
|
||||
#, python-format
|
||||
msgid "Size of the field can never be less than 1 !"
|
||||
msgstr ""
|
||||
msgstr "Size of the field can never be less than 1 !"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.so
|
||||
|
@ -7092,7 +7157,7 @@ msgstr "Somalia"
|
|||
#. module: base
|
||||
#: selection:publisher_warranty.contract,state:0
|
||||
msgid "Terminated"
|
||||
msgstr ""
|
||||
msgstr "Terminated"
|
||||
|
||||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_13
|
||||
|
@ -7102,7 +7167,7 @@ msgstr "Important customers"
|
|||
#. module: base
|
||||
#: view:res.lang:0
|
||||
msgid "Update Terms"
|
||||
msgstr ""
|
||||
msgstr "Update Terms"
|
||||
|
||||
#. module: base
|
||||
#: field:partner.sms.send,mobile_to:0
|
||||
|
@ -7121,7 +7186,7 @@ msgstr "Arguments"
|
|||
#: code:addons/orm.py:716
|
||||
#, python-format
|
||||
msgid "Database ID doesn't exist: %s : %s"
|
||||
msgstr ""
|
||||
msgstr "Database ID doesn't exist: %s : %s"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.module.module,license:0
|
||||
|
@ -7137,7 +7202,7 @@ msgstr "GPL Version 3"
|
|||
#: code:addons/orm.py:836
|
||||
#, python-format
|
||||
msgid "key '%s' not found in selection field '%s'"
|
||||
msgstr ""
|
||||
msgstr "key '%s' not found in selection field '%s'"
|
||||
|
||||
#. module: base
|
||||
#: view:partner.wizard.ean.check:0
|
||||
|
@ -7148,7 +7213,7 @@ msgstr "Correct EAN13"
|
|||
#: code:addons/orm.py:2317
|
||||
#, python-format
|
||||
msgid "The value \"%s\" for the field \"%s\" is not in the selection"
|
||||
msgstr ""
|
||||
msgstr "The value \"%s\" for the field \"%s\" is not in the selection"
|
||||
|
||||
#. module: base
|
||||
#: field:res.partner,customer:0
|
||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: OpenERP Server 5.0.0\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
|
||||
"PO-Revision-Date: 2011-01-28 02:14+0000\n"
|
||||
"PO-Revision-Date: 2011-09-28 13:58+0000\n"
|
||||
"Last-Translator: Walter Cheuk <wwycheuk@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2011-09-01 04:45+0000\n"
|
||||
"X-Generator: Launchpad (build 13827)\n"
|
||||
"X-Launchpad-Export-Date: 2011-09-29 04:36+0000\n"
|
||||
"X-Generator: Launchpad (build 14049)\n"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.filters:0
|
||||
|
@ -46,7 +46,7 @@ msgstr "日期時間"
|
|||
msgid ""
|
||||
"The second argument of the many2many field %s must be a SQL table !You used "
|
||||
"%s, which is not a valid SQL table name."
|
||||
msgstr "many2many 欄位 %s 之第二個引數須為 SQL 表格!您用了並非有效 SQL 表格名稱之 %s。"
|
||||
msgstr "「多對多(many2many)」欄位 %s 之第二個引數須為 SQL 表格!您用了並非有效 SQL 表格名稱之 %s。"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.values:0
|
||||
|
@ -103,7 +103,7 @@ msgstr "工作流程作用於"
|
|||
#. module: base
|
||||
#: field:ir.actions.act_window,display_menu_tip:0
|
||||
msgid "Display Menu Tips"
|
||||
msgstr "顯示選表提示"
|
||||
msgstr "顯示選單提示"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.module.module:0
|
||||
|
@ -129,7 +129,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: field:res.partner,ref:0
|
||||
msgid "Reference"
|
||||
msgstr "參照"
|
||||
msgstr "參考"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.act_window,target:0
|
||||
|
@ -154,7 +154,7 @@ msgstr ""
|
|||
#: code:addons/osv.py:133
|
||||
#, python-format
|
||||
msgid "Constraint Error"
|
||||
msgstr ""
|
||||
msgstr "約束錯誤"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_ui_view_custom
|
||||
|
@ -171,12 +171,12 @@ msgstr "史瓦濟蘭"
|
|||
#: code:addons/orm.py:3653
|
||||
#, python-format
|
||||
msgid "created."
|
||||
msgstr ""
|
||||
msgstr "已建立。"
|
||||
|
||||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_woodsuppliers0
|
||||
msgid "Wood Suppliers"
|
||||
msgstr ""
|
||||
msgstr "木材供應商"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/module/module.py:303
|
||||
|
@ -224,7 +224,7 @@ msgstr "新"
|
|||
#. module: base
|
||||
#: field:ir.actions.report.xml,multi:0
|
||||
msgid "On multiple doc."
|
||||
msgstr "作用於多重檔案。"
|
||||
msgstr "作用於多重文件。"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.module.category,module_nr:0
|
||||
|
@ -252,12 +252,12 @@ msgstr "聯絡人名稱"
|
|||
msgid ""
|
||||
"Save this document to a %s file and edit it with a specific software or a "
|
||||
"text editor. The file encoding is UTF-8."
|
||||
msgstr "將此檔案儲存為%s檔案後可以特定軟體或文字編輯程式修改。檔案編碼為 UTF-8。"
|
||||
msgstr "將此檔案存為 %s 檔案,並以特定軟體或文字編輯程式修改。檔案編碼為 UTF-8。"
|
||||
|
||||
#. module: base
|
||||
#: sql_constraint:res.lang:0
|
||||
msgid "The name of the language must be unique !"
|
||||
msgstr ""
|
||||
msgstr "語言名稱須與其他不同 !"
|
||||
|
||||
#. module: base
|
||||
#: selection:res.request,state:0
|
||||
|
@ -329,7 +329,7 @@ msgstr "欄位名稱"
|
|||
#: wizard_view:server.action.create,init:0
|
||||
#: wizard_field:server.action.create,init,type:0
|
||||
msgid "Select Action Type"
|
||||
msgstr "選擇動作類型"
|
||||
msgstr "選取動作類型"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.tv
|
||||
|
@ -422,7 +422,7 @@ msgstr "哥倫比亞"
|
|||
#. module: base
|
||||
#: view:ir.module.module:0
|
||||
msgid "Schedule Upgrade"
|
||||
msgstr "排程升級"
|
||||
msgstr "安排升級"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/orm.py:838
|
||||
|
@ -436,7 +436,7 @@ msgid ""
|
|||
"The ISO country code in two chars.\n"
|
||||
"You can use this field for quick search."
|
||||
msgstr ""
|
||||
"ISO 國家代碼使用兩個字符。\n"
|
||||
"ISO 國家代碼使用兩個字元。\n"
|
||||
"您可以使用該欄位來快速搜索。"
|
||||
|
||||
#. module: base
|
||||
|
@ -447,7 +447,7 @@ msgstr "帛琉"
|
|||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Sales & Purchases"
|
||||
msgstr "銷售&採購"
|
||||
msgstr "銷售 及 購貨"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.translation:0
|
||||
|
@ -481,7 +481,7 @@ msgstr "自訂欄位名稱開頭必須是 'x_' !"
|
|||
#. module: base
|
||||
#: help:ir.actions.server,action_id:0
|
||||
msgid "Select the Action Window, Report, Wizard to be executed."
|
||||
msgstr "選擇要執行的動作視窗、報表或精靈。"
|
||||
msgstr "選取要執行之動作視窗、報表或精靈。"
|
||||
|
||||
#. module: base
|
||||
#: view:res.config.users:0
|
||||
|
@ -502,7 +502,7 @@ msgstr "模型說明"
|
|||
#: help:ir.actions.act_window,src_model:0
|
||||
msgid ""
|
||||
"Optional model name of the objects on which this action should be visible"
|
||||
msgstr ""
|
||||
msgstr "要可見動作的物件之模型名稱(可有可無)"
|
||||
|
||||
#. module: base
|
||||
#: field:workflow.transition,trigger_expr_id:0
|
||||
|
@ -548,7 +548,7 @@ msgstr "是否檢查 Ean? "
|
|||
#. module: base
|
||||
#: field:ir.values,key2:0
|
||||
msgid "Event Type"
|
||||
msgstr "事件類型"
|
||||
msgstr "活動類型"
|
||||
|
||||
#. module: base
|
||||
#: view:base.language.export:0
|
||||
|
@ -684,7 +684,7 @@ msgstr "西班牙文 (UY) / Español (UY)"
|
|||
#: field:res.partner,mobile:0
|
||||
#: field:res.partner.address,mobile:0
|
||||
msgid "Mobile"
|
||||
msgstr "手機"
|
||||
msgstr "手提電話"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.om
|
||||
|
@ -741,7 +741,7 @@ msgstr "印度"
|
|||
#: model:ir.actions.act_window,name:base.res_request_link-act
|
||||
#: model:ir.ui.menu,name:base.menu_res_request_link_act
|
||||
msgid "Request Reference Types"
|
||||
msgstr "請求參照類型"
|
||||
msgstr "請求參考類型"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.values:0
|
||||
|
@ -812,7 +812,7 @@ msgstr "人力資源儀錶板"
|
|||
#: code:addons/base/res/res_user.py:507
|
||||
#, python-format
|
||||
msgid "Setting empty passwords is not allowed for security reasons!"
|
||||
msgstr "因保安理由密碼不能留空!"
|
||||
msgstr "因安全理由密碼不能留空!"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.actions.server,state:0
|
||||
|
@ -882,7 +882,7 @@ msgstr "刪除存取"
|
|||
#. module: base
|
||||
#: model:res.country,name:base.ne
|
||||
msgid "Niger"
|
||||
msgstr "尼日爾"
|
||||
msgstr "尼日"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -998,7 +998,7 @@ msgstr "儀錶板"
|
|||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.menu_purchase_root
|
||||
msgid "Purchases"
|
||||
msgstr "採購"
|
||||
msgstr "購貨"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.md
|
||||
|
@ -1120,7 +1120,7 @@ msgstr "報表"
|
|||
msgid ""
|
||||
"If set to true, the action will not be displayed on the right toolbar of a "
|
||||
"form view."
|
||||
msgstr "如設為是,該動作不會顯示於表單檢視右側工具欄。"
|
||||
msgstr "如設為真,該動作不會顯示於表單檢視右側工具欄。"
|
||||
|
||||
#. module: base
|
||||
#: field:workflow,on_create:0
|
||||
|
@ -1239,7 +1239,7 @@ msgstr "馬爾地夫"
|
|||
#. module: base
|
||||
#: help:ir.values,res_id:0
|
||||
msgid "Keep 0 if the action must appear on all resources."
|
||||
msgstr "如果該動作要顯示於所有資源上的話請保持為「0」。"
|
||||
msgstr "如該動作要顯示於所有資源的話請保持為「0」。"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_rule
|
||||
|
@ -1325,7 +1325,7 @@ msgstr "優先次序"
|
|||
#. module: base
|
||||
#: field:workflow.transition,act_from:0
|
||||
msgid "Source Activity"
|
||||
msgstr "來源活動"
|
||||
msgstr "來源地動態"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.sequence:0
|
||||
|
@ -1368,7 +1368,7 @@ msgstr "完整路徑"
|
|||
#. module: base
|
||||
#: view:res.request:0
|
||||
msgid "References"
|
||||
msgstr "參照"
|
||||
msgstr "參考"
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
|
@ -1510,7 +1510,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: view:workflow.activity:0
|
||||
msgid "Workflow Activity"
|
||||
msgstr "工作流程活動"
|
||||
msgstr "工作流程動態"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
|
@ -1694,7 +1694,7 @@ msgstr "原始檢視"
|
|||
#. module: base
|
||||
#: view:ir.values:0
|
||||
msgid "Action To Launch"
|
||||
msgstr "要啟動之動作"
|
||||
msgstr "要執行之動作"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.url,target:0
|
||||
|
@ -1736,7 +1736,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: help:ir.values,action_id:0
|
||||
msgid "This field is not used, it only helps you to select the right action."
|
||||
msgstr "該欄位並未使用,只是為了幫您選擇正確的動作。"
|
||||
msgstr "此欄位並未使用,只是為了幫您選擇正確動作。"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.server,email:0
|
||||
|
@ -1916,7 +1916,7 @@ msgstr "諾福克島"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Korean (KR) / 한국어 (KR)"
|
||||
msgstr "韓文 (KR) / 한국어 (KR)"
|
||||
msgstr "韓文 (北韓) / 한국어 (KR)"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.model.fields,model:0
|
||||
|
@ -1927,7 +1927,7 @@ msgstr ""
|
|||
#: field:ir.actions.server,action_id:0
|
||||
#: selection:ir.actions.server,state:0
|
||||
msgid "Client Action"
|
||||
msgstr "客戶端動作"
|
||||
msgstr "用戶端動作"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.bd
|
||||
|
@ -2007,7 +2007,7 @@ msgstr "屬性"
|
|||
#: model:ir.model,name:base.model_res_partner_bank_type
|
||||
#: view:res.partner.bank.type:0
|
||||
msgid "Bank Account Type"
|
||||
msgstr "銀行帳戶類型"
|
||||
msgstr "銀行帳號類型"
|
||||
|
||||
#. module: base
|
||||
#: field:base.language.export,config_logo:0
|
||||
|
@ -2171,7 +2171,7 @@ msgstr "西班牙文 (DO) / Español (DO)"
|
|||
#. module: base
|
||||
#: model:ir.model,name:base.model_workflow_activity
|
||||
msgid "workflow.activity"
|
||||
msgstr "工作流程.活動"
|
||||
msgstr "工作流程.動態"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.ui.view_sc,res_id:0
|
||||
|
@ -2239,7 +2239,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: field:ir.default,ref_id:0
|
||||
msgid "ID Ref."
|
||||
msgstr "ID參照"
|
||||
msgstr "ID參考"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.server,name:base.action_start_configurator
|
||||
|
@ -2313,7 +2313,7 @@ msgstr "分格格式"
|
|||
#. module: base
|
||||
#: selection:publisher_warranty.contract,state:0
|
||||
msgid "Unvalidated"
|
||||
msgstr "未檢驗"
|
||||
msgstr "未驗證"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.next_id_9
|
||||
|
@ -2336,7 +2336,7 @@ msgstr "馬約特"
|
|||
#: code:addons/base/ir/ir_actions.py:597
|
||||
#, python-format
|
||||
msgid "Please specify an action to launch !"
|
||||
msgstr "請指定執行動作!"
|
||||
msgstr "請指定要執行之動作!"
|
||||
|
||||
#. module: base
|
||||
#: view:res.payterm:0
|
||||
|
@ -2368,7 +2368,7 @@ msgstr "請檢查所有行數皆有%d個欄位。"
|
|||
#: view:ir.cron:0
|
||||
#: model:ir.ui.menu,name:base.menu_ir_cron_act
|
||||
msgid "Scheduled Actions"
|
||||
msgstr "計劃的動作"
|
||||
msgstr "已安排動作"
|
||||
|
||||
#. module: base
|
||||
#: field:res.partner.address,title:0
|
||||
|
@ -2412,7 +2412,7 @@ msgstr "建立選單"
|
|||
msgid ""
|
||||
"Value Added Tax number. Check the box if the partner is subjected to the "
|
||||
"VAT. Used by the VAT legal statement."
|
||||
msgstr "增值稅編號。如該伙伴適用於增值稅,請選擇。用於增值稅申報。"
|
||||
msgstr "增值稅編號。如該伙伴需要繳交增值稅,請選擇。用於申報增值稅。"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_maintenance_contract
|
||||
|
@ -2710,7 +2710,7 @@ msgstr "基礎欄位"
|
|||
#. module: base
|
||||
#: view:publisher_warranty.contract:0
|
||||
msgid "Validate"
|
||||
msgstr "檢驗"
|
||||
msgstr "驗證"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.todo,restart:0
|
||||
|
@ -2810,7 +2810,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_16
|
||||
msgid "Telecom sector"
|
||||
msgstr "電訊範疇"
|
||||
msgstr "電信範疇"
|
||||
|
||||
#. module: base
|
||||
#: field:workflow.transition,trigger_model:0
|
||||
|
@ -2820,7 +2820,7 @@ msgstr "觸發器物件"
|
|||
#. module: base
|
||||
#: view:res.users:0
|
||||
msgid "Current Activity"
|
||||
msgstr "目前活動"
|
||||
msgstr "當前動態"
|
||||
|
||||
#. module: base
|
||||
#: view:workflow.activity:0
|
||||
|
@ -2842,7 +2842,7 @@ msgstr "行銷"
|
|||
#: view:res.partner.bank:0
|
||||
#: model:res.partner.bank.type,name:base.bank_normal
|
||||
msgid "Bank account"
|
||||
msgstr "銀行帳戶"
|
||||
msgstr "銀行帳號"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -2857,7 +2857,7 @@ msgstr "序列類型"
|
|||
#. module: base
|
||||
#: view:ir.ui.view.custom:0
|
||||
msgid "Customized Architecture"
|
||||
msgstr "自訂架構"
|
||||
msgstr "自訂化架構"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.module.module,license:0
|
||||
|
@ -2877,7 +2877,7 @@ msgstr "必定"
|
|||
#. module: base
|
||||
#: selection:ir.translation,type:0
|
||||
msgid "SQL Constraint"
|
||||
msgstr "SQL 限制"
|
||||
msgstr "SQL 約束"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.server,srcmodel_id:0
|
||||
|
@ -3070,7 +3070,7 @@ msgstr "肯亞"
|
|||
#. module: base
|
||||
#: view:res.partner.event:0
|
||||
msgid "Event"
|
||||
msgstr "事件"
|
||||
msgstr "活動"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.menu_custom_reports
|
||||
|
@ -3080,7 +3080,7 @@ msgstr "自訂報表"
|
|||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
msgid "Abkhazian / аҧсуа"
|
||||
msgstr "阿布哈茲文 / аҧсуа"
|
||||
msgstr "阿布哈兹文 / аҧсуа"
|
||||
|
||||
#. module: base
|
||||
#: view:base.module.configuration:0
|
||||
|
@ -3106,7 +3106,7 @@ msgstr "聖馬利諾"
|
|||
#. module: base
|
||||
#: model:res.country,name:base.bm
|
||||
msgid "Bermuda"
|
||||
msgstr "百慕達"
|
||||
msgstr "百慕大"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.pe
|
||||
|
@ -3180,7 +3180,7 @@ msgstr "完整存取"
|
|||
#: view:ir.model.fields:0
|
||||
#: model:ir.ui.menu,name:base.menu_security
|
||||
msgid "Security"
|
||||
msgstr "保安"
|
||||
msgstr "安全"
|
||||
|
||||
#. module: base
|
||||
#: model:res.widget,title:base.openerp_favorites_twitter_widget
|
||||
|
@ -3481,8 +3481,8 @@ msgid ""
|
|||
"plugin, don't forget to register emails to each contact so that the gateway "
|
||||
"will automatically attach incoming emails to the right partner."
|
||||
msgstr ""
|
||||
"客戶是指您與其做生意者,例如公司或機構。客戶可有多個聯絡人或地址,均屬於其員工。您可用「歷史」分頁追蹤所有有關交易:訂單、電郵、機會、退款要求等等。如您用"
|
||||
"電郵閘道和 Outlook 或 Thunderbird 外掛程式,別忘了為聯絡人登記電郵,好讓閘道自動為合適伙伴寄送收到之電郵。"
|
||||
"客戶是指您與其做生意者,例如公司或機構。客戶可有多個聯絡人或地址,均屬於其員工。您可用「歷史」分頁追蹤所有有關交易:訂單、電郵、商機、退款要求等等。如您用"
|
||||
"電郵閘道、Outlook 或 Thunderbird 外掛程式,別忘了為聯絡人登記電郵,好讓閘道自動為合適伙伴寄送收到之電郵。"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.report.xml,name:0
|
||||
|
@ -3517,7 +3517,7 @@ msgstr "名稱"
|
|||
msgid ""
|
||||
"If set to true, the action will not be displayed on the right toolbar of a "
|
||||
"form view"
|
||||
msgstr "如設為是,該動作不會顯示於表單檢視右側工具欄"
|
||||
msgstr "如設為真,該動作不會顯示於表單檢視右側工具欄"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.ms
|
||||
|
@ -3543,7 +3543,7 @@ msgstr "應用程式詞彙"
|
|||
msgid ""
|
||||
"The user's timezone, used to perform timezone conversions between the server "
|
||||
"and the client."
|
||||
msgstr ""
|
||||
msgstr "用戶之時區,用以為伺服器及用戶端進行時區轉換。"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.module.module,demo:0
|
||||
|
@ -3565,7 +3565,7 @@ msgstr "日文 / 日本語"
|
|||
msgid ""
|
||||
"Source activity. When this activity is over, the condition is tested to "
|
||||
"determine if we can start the ACT_TO activity."
|
||||
msgstr ""
|
||||
msgstr "來源地動態。當再無動態,會測試條件以決定是否開始 ACT_TO 動態。"
|
||||
|
||||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_3
|
||||
|
@ -3618,7 +3618,7 @@ msgstr "冷岸及央麥恩群島"
|
|||
#: model:ir.model,name:base.model_ir_actions_wizard
|
||||
#: selection:ir.ui.menu,action:0
|
||||
msgid "ir.actions.wizard"
|
||||
msgstr ""
|
||||
msgstr "ir.actions.wizard"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.actions.act_window:0
|
||||
|
@ -3753,12 +3753,12 @@ msgstr "無法載入模組基礎!(提示:檢查附加元件路徑)"
|
|||
#. module: base
|
||||
#: view:res.partner.bank:0
|
||||
msgid "Bank Account Owner"
|
||||
msgstr "銀行帳戶所有者"
|
||||
msgstr "銀行帳號所有者"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.act_values_form
|
||||
msgid "Client Actions Connections"
|
||||
msgstr "客戶端動作連接"
|
||||
msgstr "用戶端動作連線"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.attachment,res_name:0
|
||||
|
@ -3836,7 +3836,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: view:ir.actions.server:0
|
||||
msgid "Client Action Configuration"
|
||||
msgstr "客戶端動作設置"
|
||||
msgstr "用戶端動作設定"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_res_partner_address
|
||||
|
@ -3872,13 +3872,13 @@ msgstr "選取要匯入之模組套件 (.zip 檔):"
|
|||
#: field:res.partner.event,name:0
|
||||
#: model:res.widget,title:base.events_widget
|
||||
msgid "Events"
|
||||
msgstr "事件"
|
||||
msgstr "活動"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_actions_url
|
||||
#: selection:ir.ui.menu,action:0
|
||||
msgid "ir.actions.url"
|
||||
msgstr ""
|
||||
msgstr "ir.actions.url"
|
||||
|
||||
#. module: base
|
||||
#: model:res.widget,title:base.currency_converter_widget
|
||||
|
@ -4098,7 +4098,7 @@ msgstr "重複錯過的"
|
|||
#. module: base
|
||||
#: help:ir.actions.server,state:0
|
||||
msgid "Type of the Action that is to be executed"
|
||||
msgstr "將要執行動作之類型"
|
||||
msgstr "要執行動作之類型"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.server.object.lines,server_id:0
|
||||
|
@ -4706,7 +4706,7 @@ msgid ""
|
|||
"Track from where is coming your leads and opportunities by creating specific "
|
||||
"channels that will be maintained at the creation of a document in the "
|
||||
"system. Some examples of channels can be: Website, Phone Call, Reseller, etc."
|
||||
msgstr ""
|
||||
msgstr "於系統建立文件,以保持特定渠道追蹤潛在客戶及商機之來源。渠道例子有網站、電話查詢、零售商等等。"
|
||||
|
||||
#. module: base
|
||||
#: model:res.partner.bank.type.field,name:base.bank_normal_field
|
||||
|
@ -4765,7 +4765,7 @@ msgstr "變改我的偏好設定"
|
|||
#: code:addons/base/ir/ir_actions.py:164
|
||||
#, python-format
|
||||
msgid "Invalid model name in the action definition."
|
||||
msgstr "動作定義之模型名無效。"
|
||||
msgstr "動作定義之模型名稱無效。"
|
||||
|
||||
#. module: base
|
||||
#: field:partner.sms.send,text:0
|
||||
|
@ -4900,7 +4900,7 @@ msgstr "如指定,此動作會於此用戶登入時於標準選單以外額外
|
|||
#. module: base
|
||||
#: view:ir.values:0
|
||||
msgid "Client Actions"
|
||||
msgstr "客戶端動作"
|
||||
msgstr "用戶端動作"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/orm.py:1806
|
||||
|
@ -4921,12 +4921,12 @@ msgstr ""
|
|||
#. module: base
|
||||
#: field:workflow.transition,act_to:0
|
||||
msgid "Destination Activity"
|
||||
msgstr "目的地活動"
|
||||
msgstr "目的地動態"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.values:0
|
||||
msgid "Connect Events to Actions"
|
||||
msgstr "把事件連接動作"
|
||||
msgstr "把活動關聯到動作"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_base_update_translations
|
||||
|
@ -5041,7 +5041,7 @@ msgstr "網頁圖示影像"
|
|||
#. module: base
|
||||
#: view:ir.values:0
|
||||
msgid "Values for Event Type"
|
||||
msgstr "事件類型值"
|
||||
msgstr "活動類型值"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.model.fields,select_level:0
|
||||
|
@ -5069,6 +5069,8 @@ msgid ""
|
|||
"installed the CRM, with the history tab, you can track all the interactions "
|
||||
"with a partner such as opportunities, emails, or sales orders issued."
|
||||
msgstr ""
|
||||
"客戶(於系統其他地方又稱「伙伴」)助您管理其他公司,包括潛在客戶、客戶及/或供應商,之通訊錄。「伙伴表單」讓您追蹤及紀錄所有所需資訊,以讓您處理公司地址、"
|
||||
"聯絡人、報價單等等。如您安裝了客戶關係管理(CRM)模組,以歷史分頁您可追蹤與伙伴有關之所有來往,如商機、電郵或銷售訂單等。"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.ph
|
||||
|
@ -5552,7 +5554,7 @@ msgstr ""
|
|||
#: code:addons/base/res/res_config.py:94
|
||||
#, python-format
|
||||
msgid "Couldn't find previous ir.actions.todo"
|
||||
msgstr ""
|
||||
msgstr "找不到之前的 ir.actions.todo"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.actions.act_window:0
|
||||
|
@ -5752,7 +5754,7 @@ msgstr "一年中的星期:%(woy)s"
|
|||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_14
|
||||
msgid "Bad customers"
|
||||
msgstr "差客戶"
|
||||
msgstr "壞客戶"
|
||||
|
||||
#. module: base
|
||||
#: report:ir.module.reference.graph:0
|
||||
|
@ -5792,7 +5794,7 @@ msgstr "宏都拉斯"
|
|||
#: help:res.users,menu_tips:0
|
||||
msgid ""
|
||||
"Check out this box if you want to always display tips on each menu action"
|
||||
msgstr ""
|
||||
msgstr "如想於每個選單動作顯示提示,勾選此框"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.eg
|
||||
|
@ -6078,7 +6080,7 @@ msgstr "建立日期"
|
|||
msgid ""
|
||||
"Select the action that will be executed. Loop action will not be avaliable "
|
||||
"inside loop."
|
||||
msgstr "選擇將要執行的動作。循環動作在循環內不可用。"
|
||||
msgstr "選取要執行之動作。循環動作在循環內不可用。"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -6358,7 +6360,7 @@ msgstr "模組更新結果"
|
|||
#: view:workflow.activity:0
|
||||
#: field:workflow.workitem,act_id:0
|
||||
msgid "Activity"
|
||||
msgstr "活動"
|
||||
msgstr "動態"
|
||||
|
||||
#. module: base
|
||||
#: view:res.partner:0
|
||||
|
@ -6423,7 +6425,7 @@ msgstr ""
|
|||
msgid ""
|
||||
"Customized views are used when users reorganize the content of their "
|
||||
"dashboard views (via web client)"
|
||||
msgstr ""
|
||||
msgstr "當用戶(以 web client)重組其 dashboard 檢視即會使用自訂化檢視"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.model,name:0
|
||||
|
@ -6494,7 +6496,7 @@ msgstr ""
|
|||
#: model:ir.actions.act_window,name:base.res_log_act_window
|
||||
#: model:ir.ui.menu,name:base.menu_res_log_act_window
|
||||
msgid "Client Logs"
|
||||
msgstr "客戶端日誌"
|
||||
msgstr "用戶端日誌"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.al
|
||||
|
@ -6534,7 +6536,7 @@ msgstr ""
|
|||
#: code:addons/base/ir/ir_actions.py:716
|
||||
#, python-format
|
||||
msgid "Problem in configuration `Record Id` in Server Action!"
|
||||
msgstr "於伺服器動作之「Record Id」配置錯誤!"
|
||||
msgstr "伺服器動作之「紀錄 Id」配置有問題!"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/orm.py:2306
|
||||
|
@ -6587,7 +6589,7 @@ msgstr "電郵"
|
|||
#: field:res.config.users,action_id:0
|
||||
#: field:res.users,action_id:0
|
||||
msgid "Home Action"
|
||||
msgstr "家動作(Home Action)"
|
||||
msgstr "家動作(Home Action)"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/custom.py:558
|
||||
|
@ -7603,7 +7605,7 @@ msgstr "要更新模組"
|
|||
msgid ""
|
||||
"Important when you deal with multiple actions, the execution order will be "
|
||||
"decided based on this, low number is higher priority."
|
||||
msgstr "對於處理多個動作很重要,其決定動作執行順序;小的數字具較高優先次序。"
|
||||
msgstr "對於處理多重動作很重要,其決定動作執行次序;小的數字具較高優先次序。"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.report.xml,header:0
|
||||
|
@ -7756,7 +7758,7 @@ msgstr "格陵蘭"
|
|||
#. module: base
|
||||
#: field:res.partner.bank,acc_number:0
|
||||
msgid "Account Number"
|
||||
msgstr "帳戶號碼"
|
||||
msgstr "帳號"
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
|
@ -7969,7 +7971,7 @@ msgstr "斯洛伐克文 / Slovenský jazyk"
|
|||
#: field:ir.ui.menu,icon_pict:0
|
||||
#: field:publisher_warranty.contract.wizard,state:0
|
||||
msgid "unknown"
|
||||
msgstr "不明"
|
||||
msgstr "不詳"
|
||||
|
||||
#. module: base
|
||||
#: field:res.currency,symbol:0
|
||||
|
@ -8032,7 +8034,7 @@ msgstr "CSV 檔"
|
|||
#. module: base
|
||||
#: field:res.company,account_no:0
|
||||
msgid "Account No."
|
||||
msgstr "帳戶號碼"
|
||||
msgstr "帳號"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_lang.py:157
|
||||
|
@ -8128,7 +8130,7 @@ msgstr "土耳其文 / Türkçe"
|
|||
#: view:workflow:0
|
||||
#: field:workflow,activities:0
|
||||
msgid "Activities"
|
||||
msgstr "活動"
|
||||
msgstr "動態"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.act_window,auto_refresh:0
|
||||
|
@ -8166,7 +8168,7 @@ msgstr ""
|
|||
#: model:ir.ui.menu,name:base.menu_event_association
|
||||
#: model:ir.ui.menu,name:base.menu_event_main
|
||||
msgid "Events Organisation"
|
||||
msgstr "事件組織"
|
||||
msgstr "活動組織"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.ir_sequence_actions
|
||||
|
@ -8247,7 +8249,7 @@ msgstr "添加公司 RML 頁首與否"
|
|||
#. module: base
|
||||
#: help:workflow.transition,act_to:0
|
||||
msgid "The destination activity."
|
||||
msgstr "目的地活地。"
|
||||
msgstr "目的地動態。"
|
||||
|
||||
#. module: base
|
||||
#: view:base.module.update:0
|
||||
|
@ -8283,7 +8285,7 @@ msgstr "聖誕島"
|
|||
#. module: base
|
||||
#: view:ir.actions.server:0
|
||||
msgid "Other Actions Configuration"
|
||||
msgstr "其他動作設置"
|
||||
msgstr "其他動作配置"
|
||||
|
||||
#. module: base
|
||||
#: view:res.config.installer:0
|
||||
|
@ -8307,7 +8309,7 @@ msgstr "額外資訊"
|
|||
#: model:ir.actions.act_window,name:base.act_values_form_action
|
||||
#: model:ir.ui.menu,name:base.menu_values_form_action
|
||||
msgid "Client Events"
|
||||
msgstr "客戶端事件"
|
||||
msgstr "用戶端活動"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.module.module:0
|
||||
|
@ -8429,7 +8431,7 @@ msgstr "關聯欄位"
|
|||
#. module: base
|
||||
#: view:res.partner.event:0
|
||||
msgid "Event Logs"
|
||||
msgstr "事件日誌"
|
||||
msgstr "活動日誌"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/module/wizard/base_module_configuration.py:37
|
||||
|
@ -8446,7 +8448,7 @@ msgstr "目的地實例"
|
|||
#: field:ir.actions.act_window,multi:0
|
||||
#: field:ir.actions.wizard,multi:0
|
||||
msgid "Action on Multiple Doc."
|
||||
msgstr "動作作用於多個文件"
|
||||
msgstr "多重文件之動作。"
|
||||
|
||||
#. module: base
|
||||
#: view:base.language.export:0
|
||||
|
@ -8477,7 +8479,7 @@ msgstr "盧森堡"
|
|||
#: help:ir.values,key2:0
|
||||
msgid ""
|
||||
"The kind of action or button in the client side that will trigger the action."
|
||||
msgstr "客戶端的該類動作或按鈕將觸發此動作。"
|
||||
msgstr "用戶端的該類動作或按鈕會觸發此動作。"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_ui_menu.py:285
|
||||
|
@ -8899,7 +8901,7 @@ msgstr "目前視窗"
|
|||
#. module: base
|
||||
#: view:ir.values:0
|
||||
msgid "Action Source"
|
||||
msgstr "動作來源"
|
||||
msgstr "動作來源地"
|
||||
|
||||
#. module: base
|
||||
#: view:res.config.view:0
|
||||
|
@ -9048,7 +9050,7 @@ msgstr "本地化"
|
|||
#. module: base
|
||||
#: view:ir.actions.server:0
|
||||
msgid "Action to Launch"
|
||||
msgstr "要啟動動作"
|
||||
msgstr "要執行之動作"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.cron:0
|
||||
|
@ -9086,7 +9088,7 @@ msgstr "另存為附件前綴"
|
|||
msgid ""
|
||||
"Only one client action will be executed, last client action will be "
|
||||
"considered in case of multiple client actions."
|
||||
msgstr "只能執行一個客戶端動作,如有多重動作只考慮最後一個。"
|
||||
msgstr "只會執行一個用戶端動作,如有多重用戶端動作只考慮最後一個。"
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
|
@ -9131,7 +9133,7 @@ msgstr "塞席爾"
|
|||
#: model:ir.model,name:base.model_res_partner_bank
|
||||
#: view:res.partner.bank:0
|
||||
msgid "Bank Accounts"
|
||||
msgstr "銀行帳戶"
|
||||
msgstr "銀行帳號"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.sl
|
||||
|
@ -9152,7 +9154,7 @@ msgstr "土克斯及開科斯群島"
|
|||
#. module: base
|
||||
#: field:res.partner.bank,owner_name:0
|
||||
msgid "Account Owner"
|
||||
msgstr "帳戶所有者"
|
||||
msgstr "帳號所有者"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_user.py:256
|
||||
|
|
|
@ -1274,7 +1274,7 @@
|
|||
|
||||
|
||||
<record id="action_model_model" model="ir.actions.act_window">
|
||||
<field name="name">Objects</field>
|
||||
<field name="name">Models</field>
|
||||
<field name="res_model">ir.model</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="context">{'manual':True}</field>
|
||||
|
@ -1740,7 +1740,7 @@
|
|||
<field name="name">Property multi-company</field>
|
||||
<field model="ir.model" name="model_id" ref="model_ir_property"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
|
||||
<field name="domain_force">['|',('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<!--server action view-->
|
||||
|
@ -1766,7 +1766,9 @@
|
|||
<page string="Trigger" attrs="{'invisible':[('state','!=','trigger')]}">
|
||||
<separator colspan="4" string="Trigger Configuration"/>
|
||||
<field name="wkf_model_id" attrs="{'required':[('state','=','trigger')]}"/>
|
||||
<field name="trigger_obj_id" context="{'key':''}" domain="[('model_id','=',model_id)]" attrs="{'required':[('state','=','trigger')]}"/>
|
||||
<field name="trigger_obj_id" context="{'key':''}"
|
||||
domain="[('model_id','=',model_id),('ttype','in',['many2one','int'])]"
|
||||
attrs="{'required':[('state','=','trigger')]}"/>
|
||||
<field name="trigger_name" attrs="{'required':[('state','=','trigger')]}"/>
|
||||
</page>
|
||||
<page string="Action to Launch" attrs="{'invisible':[('state','!=','client_action')]}">
|
||||
|
|
|
@ -438,14 +438,15 @@ server_object_lines()
|
|||
class actions_server(osv.osv):
|
||||
|
||||
def _select_signals(self, cr, uid, context=None):
|
||||
cr.execute("SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t \
|
||||
WHERE w.id = a.wkf_id AND t.act_from = a.id OR t.act_to = a.id AND t.signal!='' \
|
||||
AND t.signal NOT IN (null, NULL)")
|
||||
cr.execute("""SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t
|
||||
WHERE w.id = a.wkf_id AND
|
||||
(t.act_from = a.id OR t.act_to = a.id) AND
|
||||
t.signal IS NOT NULL""")
|
||||
result = cr.fetchall() or []
|
||||
res = []
|
||||
for rs in result:
|
||||
if rs[0] is not None and rs[1] is not None:
|
||||
line = rs[0], "%s - (%s)" % (rs[1], rs[0])
|
||||
line = rs[1], "%s - (%s)" % (rs[1], rs[0])
|
||||
res.append(line)
|
||||
return res
|
||||
|
||||
|
@ -456,13 +457,15 @@ class actions_server(osv.osv):
|
|||
return [(r['model'], r['name']) for r in res] + [('','')]
|
||||
|
||||
def change_object(self, cr, uid, ids, copy_object, state, context=None):
|
||||
if state == 'object_copy':
|
||||
if state == 'object_copy' and copy_object:
|
||||
if context is None:
|
||||
context = {}
|
||||
model_pool = self.pool.get('ir.model')
|
||||
model = copy_object.split(',')[0]
|
||||
mid = model_pool.search(cr, uid, [('model','=',model)])
|
||||
return {
|
||||
'value':{'srcmodel_id':mid[0]},
|
||||
'context':context
|
||||
'value': {'srcmodel_id': mid[0]},
|
||||
'context': context
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
@ -503,9 +506,9 @@ class actions_server(osv.osv):
|
|||
'sequence': fields.integer('Sequence', help="Important when you deal with multiple actions, the execution order will be decided based on this, low number is higher priority."),
|
||||
'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create)."),
|
||||
'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."),
|
||||
'trigger_name': fields.selection(_select_signals, string='Trigger Name', size=128, help="Select the Signal name that is to be used as the trigger."),
|
||||
'wkf_model_id': fields.many2one('ir.model', 'Workflow On', help="Workflow to be executed on this model."),
|
||||
'trigger_obj_id': fields.many2one('ir.model.fields','Trigger On', help="Select the object from the model on which the workflow will executed."),
|
||||
'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
|
||||
'wkf_model_id': fields.many2one('ir.model', 'Target Object', help="The object that should receive the workflow signal (must have an associated workflow)"),
|
||||
'trigger_obj_id': fields.many2one('ir.model.fields','Relation Field', help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)"),
|
||||
'email': fields.char('Email Address', size=512, help="Expression that returns the email address to send to. Can be based on the same values as for the condition field.\n"
|
||||
"Example: object.invoice_address_id.email, or 'me@example.com'"),
|
||||
'subject': fields.char('Subject', size=1024, translate=True, help="Email subject, may contain expressions enclosed in double brackets based on the same values as those "
|
||||
|
@ -532,7 +535,7 @@ class actions_server(osv.osv):
|
|||
'sequence': lambda *a: 5,
|
||||
'code': lambda *a: """# You can use the following variables:
|
||||
# - self: ORM model of the record on which the action is triggered
|
||||
# - object or obj: browse_record of the record on which the action is triggered
|
||||
# - object: browse_record of the record on which the action is triggered if there is one, otherwise None
|
||||
# - pool: ORM model pool (i.e. self.pool)
|
||||
# - time: Python time module
|
||||
# - cr: database cursor
|
||||
|
@ -685,9 +688,10 @@ class actions_server(osv.osv):
|
|||
if action.state == 'trigger':
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
model = action.wkf_model_id.model
|
||||
res_id = obj_pool.read(cr, uid, [context.get('active_id')], [action.trigger_obj_id.name])
|
||||
id = res_id [0][action.trigger_obj_id.name]
|
||||
wf_service.trg_validate(uid, model, int(id), action.trigger_name, cr)
|
||||
m2o_field_name = action.trigger_obj_id.name
|
||||
target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
|
||||
target_id = target_id[0] if isinstance(target_id,tuple) else target_id
|
||||
wf_service.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
|
||||
|
||||
if action.state == 'sms':
|
||||
#TODO: set the user and password from the system
|
||||
|
|
|
@ -21,13 +21,20 @@
|
|||
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
import psycopg2
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
import netsvc
|
||||
import tools
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
import openerp
|
||||
import pooler
|
||||
import tools
|
||||
from openerp.cron import WAKE_UP_NOW
|
||||
from osv import fields, osv
|
||||
from tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
from tools.translate import _
|
||||
|
||||
def str2tuple(s):
|
||||
return eval('tuple(%s)' % (s or ''))
|
||||
|
@ -41,10 +48,15 @@ _intervalTypes = {
|
|||
'minutes': lambda interval: relativedelta(minutes=interval),
|
||||
}
|
||||
|
||||
class ir_cron(osv.osv, netsvc.Agent):
|
||||
""" This is the ORM object that periodically executes actions.
|
||||
Note that we use the netsvc.Agent()._logger member.
|
||||
class ir_cron(osv.osv):
|
||||
""" Model describing cron jobs (also called actions or tasks).
|
||||
"""
|
||||
|
||||
# TODO: perhaps in the future we could consider a flag on ir.cron jobs
|
||||
# that would cause database wake-up even if the database has not been
|
||||
# loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
|
||||
# See also openerp.cron
|
||||
|
||||
_name = "ir.cron"
|
||||
_order = 'name'
|
||||
_columns = {
|
||||
|
@ -54,17 +66,17 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
'interval_number': fields.integer('Interval Number',help="Repeat every x."),
|
||||
'interval_type': fields.selection( [('minutes', 'Minutes'),
|
||||
('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'),
|
||||
'numbercall': fields.integer('Number of Calls', help='Number of time the function is called,\na negative number indicates no limit'),
|
||||
'doall' : fields.boolean('Repeat Missed', help="Enable this if you want to execute missed occurences as soon as the server restarts."),
|
||||
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this scheduler"),
|
||||
'model': fields.char('Object', size=64, help="Name of object whose function will be called when this scheduler will run. e.g. 'res.partener'"),
|
||||
'function': fields.char('Function', size=64, help="Name of the method to be called on the object when this scheduler is executed."),
|
||||
'args': fields.text('Arguments', help="Arguments to be passed to the method. e.g. (uid,)"),
|
||||
'priority': fields.integer('Priority', help='0=Very Urgent\n10=Not urgent')
|
||||
'numbercall': fields.integer('Number of Calls', help='How many times the method is called,\na negative number indicates no limit.'),
|
||||
'doall' : fields.boolean('Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts."),
|
||||
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this job."),
|
||||
'model': fields.char('Object', size=64, help="Model name on which the method to be called is located, e.g. 'res.partner'."),
|
||||
'function': fields.char('Method', size=64, help="Name of the method to be called when this job is processed."),
|
||||
'args': fields.text('Arguments', help="Arguments to be passed to the method, e.g. (uid,)."),
|
||||
'priority': fields.integer('Priority', help='The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.')
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'nextcall' : lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'nextcall' : lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
'priority' : lambda *a: 5,
|
||||
'user_id' : lambda obj,cr,uid,context: uid,
|
||||
'interval_number' : lambda *a: 1,
|
||||
|
@ -74,6 +86,8 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
'doall' : lambda *a: 1
|
||||
}
|
||||
|
||||
_logger = logging.getLogger('cron')
|
||||
|
||||
def _check_args(self, cr, uid, ids, context=None):
|
||||
try:
|
||||
for this in self.browse(cr, uid, ids, context):
|
||||
|
@ -86,68 +100,164 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
(_check_args, 'Invalid arguments', ['args']),
|
||||
]
|
||||
|
||||
def _handle_callback_exception(self, cr, uid, model, func, args, job_id, job_exception):
|
||||
cr.rollback()
|
||||
logger=logging.getLogger('cron')
|
||||
logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model, func, args, job_id))
|
||||
def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception):
|
||||
""" Method called when an exception is raised by a job.
|
||||
|
||||
def _callback(self, cr, uid, model, func, args, job_id):
|
||||
Simply logs the exception and rollback the transaction.
|
||||
|
||||
:param model_name: model name on which the job method is located.
|
||||
:param method_name: name of the method to call when this job is processed.
|
||||
:param args: arguments of the method (without the usual self, cr, uid).
|
||||
:param job_id: job id.
|
||||
:param job_exception: exception raised by the job.
|
||||
|
||||
"""
|
||||
cr.rollback()
|
||||
self._logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model_name, method_name, args, job_id))
|
||||
|
||||
def _callback(self, cr, uid, model_name, method_name, args, job_id):
|
||||
""" Run the method associated to a given job
|
||||
|
||||
It takes care of logging and exception handling.
|
||||
|
||||
:param model_name: model name on which the job method is located.
|
||||
:param method_name: name of the method to call when this job is processed.
|
||||
:param args: arguments of the method (without the usual self, cr, uid).
|
||||
:param job_id: job id.
|
||||
"""
|
||||
args = str2tuple(args)
|
||||
m = self.pool.get(model)
|
||||
if m and hasattr(m, func):
|
||||
f = getattr(m, func)
|
||||
model = self.pool.get(model_name)
|
||||
if model and hasattr(model, method_name):
|
||||
method = getattr(model, method_name)
|
||||
try:
|
||||
netsvc.log('cron', (cr.dbname,uid,'*',model,func)+tuple(args), channel=logging.DEBUG,
|
||||
netsvc.log('cron', (cr.dbname,uid,'*',model_name,method_name)+tuple(args), channel=logging.DEBUG,
|
||||
depth=(None if self._logger.isEnabledFor(logging.DEBUG_RPC_ANSWER) else 1), fn='object.execute')
|
||||
logger = logging.getLogger('execution time')
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
start_time = time.time()
|
||||
f(cr, uid, *args)
|
||||
method(cr, uid, *args)
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
end_time = time.time()
|
||||
logger.log(logging.DEBUG, '%.3fs (%s, %s)' % (end_time - start_time, model, func))
|
||||
logger.log(logging.DEBUG, '%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name))
|
||||
except Exception, e:
|
||||
self._handle_callback_exception(cr, uid, model, func, args, job_id, e)
|
||||
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
|
||||
|
||||
def _poolJobs(self, db_name, check=False):
|
||||
def _run_job(self, cr, job, now):
|
||||
""" Run a given job taking care of the repetition.
|
||||
|
||||
The cursor has a lock on the job (aquired by _run_jobs_multithread()) and this
|
||||
method is run in a worker thread (spawned by _run_jobs_multithread())).
|
||||
|
||||
:param job: job to be run (as a dictionary).
|
||||
:param now: timestamp (result of datetime.now(), no need to call it multiple time).
|
||||
|
||||
"""
|
||||
try:
|
||||
db, pool = pooler.get_db_and_pool(db_name)
|
||||
except:
|
||||
return False
|
||||
nextcall = datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
numbercall = job['numbercall']
|
||||
|
||||
ok = False
|
||||
while nextcall < now and numbercall:
|
||||
if numbercall > 0:
|
||||
numbercall -= 1
|
||||
if not ok or job['doall']:
|
||||
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
||||
if numbercall:
|
||||
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
|
||||
ok = True
|
||||
addsql = ''
|
||||
if not numbercall:
|
||||
addsql = ', active=False'
|
||||
cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
|
||||
(nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
|
||||
|
||||
if numbercall:
|
||||
# Reschedule our own main cron thread if necessary.
|
||||
# This is really needed if this job runs longer than its rescheduling period.
|
||||
nextcall = time.mktime(nextcall.timetuple())
|
||||
openerp.cron.schedule_wakeup(nextcall, cr.dbname)
|
||||
finally:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
openerp.cron.release_thread_slot()
|
||||
|
||||
def _run_jobs_multithread(self):
|
||||
# TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py
|
||||
""" Process the cron jobs by spawning worker threads.
|
||||
|
||||
This selects in database all the jobs that should be processed. It then
|
||||
tries to lock each of them and, if it succeeds, spawns a thread to run
|
||||
the cron job (if it doesn't succeed, it means the job was already
|
||||
locked to be taken care of by another thread).
|
||||
|
||||
The cursor used to lock the job in database is given to the worker
|
||||
thread (which has to close it itself).
|
||||
|
||||
"""
|
||||
db = self.pool.db
|
||||
cr = db.cursor()
|
||||
db_name = db.dbname
|
||||
try:
|
||||
if not pool._init:
|
||||
now = datetime.now()
|
||||
cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority')
|
||||
for job in cr.dictfetchall():
|
||||
nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S')
|
||||
numbercall = job['numbercall']
|
||||
jobs = {} # mapping job ids to jobs for all jobs being processed.
|
||||
now = datetime.now()
|
||||
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
|
||||
cr.execute("""SELECT * FROM ir_cron
|
||||
WHERE numbercall != 0
|
||||
AND active AND nextcall <= (now() at time zone 'UTC')
|
||||
ORDER BY priority""")
|
||||
for job in cr.dictfetchall():
|
||||
if not openerp.cron.get_thread_slots():
|
||||
break
|
||||
jobs[job['id']] = job
|
||||
|
||||
ok = False
|
||||
while nextcall < now and numbercall:
|
||||
if numbercall > 0:
|
||||
numbercall -= 1
|
||||
if not ok or job['doall']:
|
||||
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
||||
if numbercall:
|
||||
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
|
||||
ok = True
|
||||
addsql = ''
|
||||
if not numbercall:
|
||||
addsql = ', active=False'
|
||||
cr.execute("update ir_cron set nextcall=%s, numbercall=%s"+addsql+" where id=%s", (nextcall.strftime('%Y-%m-%d %H:%M:%S'), numbercall, job['id']))
|
||||
cr.commit()
|
||||
task_cr = db.cursor()
|
||||
try:
|
||||
# Try to grab an exclusive lock on the job row from within the task transaction
|
||||
acquired_lock = False
|
||||
task_cr.execute("""SELECT *
|
||||
FROM ir_cron
|
||||
WHERE id=%s
|
||||
FOR UPDATE NOWAIT""",
|
||||
(job['id'],), log_exceptions=False)
|
||||
acquired_lock = True
|
||||
except psycopg2.OperationalError, e:
|
||||
if e.pgcode == '55P03':
|
||||
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
||||
self._logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
|
||||
continue
|
||||
else:
|
||||
# Unexpected OperationalError
|
||||
raise
|
||||
finally:
|
||||
if not acquired_lock:
|
||||
# we're exiting due to an exception while acquiring the lot
|
||||
task_cr.close()
|
||||
|
||||
# Got the lock on the job row, now spawn a thread to execute it in the transaction with the lock
|
||||
task_thread = threading.Thread(target=self._run_job, name=job['name'], args=(task_cr, job, now))
|
||||
# force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
|
||||
task_thread.setDaemon(False)
|
||||
openerp.cron.take_thread_slot()
|
||||
task_thread.start()
|
||||
self._logger.debug('Cron execution thread for job `%s` spawned', job['name'])
|
||||
|
||||
cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active')
|
||||
next_call = cr.dictfetchone()['min_next_call']
|
||||
if next_call:
|
||||
next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S'))
|
||||
# Find next earliest job ignoring currently processed jobs (by this and other cron threads)
|
||||
find_next_time_query = """SELECT min(nextcall) AS min_next_call
|
||||
FROM ir_cron WHERE numbercall != 0 AND active"""
|
||||
if jobs:
|
||||
cr.execute(find_next_time_query + " AND id NOT IN %s", (tuple(jobs.keys()),))
|
||||
else:
|
||||
next_call = int(time.time()) + 3600 # if do not find active cron job from database, it will run again after 1 day
|
||||
cr.execute(find_next_time_query)
|
||||
next_call = cr.dictfetchone()['min_next_call']
|
||||
|
||||
if not check:
|
||||
self.setAlarm(self._poolJobs, next_call, db_name, db_name)
|
||||
if next_call:
|
||||
next_call = time.mktime(time.strptime(next_call, DEFAULT_SERVER_DATETIME_FORMAT))
|
||||
else:
|
||||
# no matching cron job found in database, re-schedule arbitrarily in 1 day,
|
||||
# this delay will likely be modified when running jobs complete their tasks
|
||||
next_call = time.time() + (24*3600)
|
||||
|
||||
openerp.cron.schedule_wakeup(next_call, db_name)
|
||||
|
||||
except Exception, ex:
|
||||
self._logger.warning('Exception in cron:', exc_info=True)
|
||||
|
@ -156,12 +266,8 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
cr.commit()
|
||||
cr.close()
|
||||
|
||||
def restart(self, dbname):
|
||||
self.cancel(dbname)
|
||||
# Reschedule cron processing job asap, but not in the current thread
|
||||
self.setAlarm(self._poolJobs, time.time(), dbname, dbname)
|
||||
|
||||
def update_running_cron(self, cr):
|
||||
""" Schedule as soon as possible a wake-up for this database. """
|
||||
# Verify whether the server is already started and thus whether we need to commit
|
||||
# immediately our changes and restart the cron agent in order to apply the change
|
||||
# immediately. The commit() is needed because as soon as the cron is (re)started it
|
||||
|
@ -171,23 +277,37 @@ class ir_cron(osv.osv, netsvc.Agent):
|
|||
# when the server is only starting or loading modules (hence the test on pool._init).
|
||||
if not self.pool._init:
|
||||
cr.commit()
|
||||
self.restart(cr.dbname)
|
||||
openerp.cron.schedule_wakeup(WAKE_UP_NOW, self.pool.db.dbname)
|
||||
|
||||
def _try_lock(self, cr, uid, ids, context=None):
|
||||
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
|
||||
to make sure a following write() or unlink() will not block due
|
||||
to a process currently executing those cron tasks"""
|
||||
try:
|
||||
cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
|
||||
(tuple(ids),), log_exceptions=False)
|
||||
except psycopg2.OperationalError:
|
||||
cr.rollback() # early rollback to allow translations to work for the user feedback
|
||||
raise osv.except_osv(_("Record cannot be modified right now"),
|
||||
_("This cron task is currently being executed and may not be modified, "
|
||||
"please try again in a few minutes"))
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
res = super(ir_cron, self).create(cr, uid, vals, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
|
||||
def write(self, cr, user, ids, vals, context=None):
|
||||
res = super(ir_cron, self).write(cr, user, ids, vals, context=context)
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
self._try_lock(cr, uid, ids, context)
|
||||
res = super(ir_cron, self).write(cr, uid, ids, vals, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
self._try_lock(cr, uid, ids, context)
|
||||
res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
ir_cron()
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -56,14 +56,14 @@ def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
|
|||
|
||||
class ir_model(osv.osv):
|
||||
_name = 'ir.model'
|
||||
_description = "Objects"
|
||||
_description = "Models"
|
||||
_order = 'model'
|
||||
|
||||
def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
|
||||
models = self.browse(cr, uid, ids, context=context)
|
||||
res = dict.fromkeys(ids)
|
||||
for model in models:
|
||||
res[model.id] = isinstance(self.pool.get(model.model), osv.osv_memory)
|
||||
res[model.id] = self.pool.get(model.model).is_transient()
|
||||
return res
|
||||
|
||||
def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
|
||||
|
@ -85,8 +85,8 @@ class ir_model(osv.osv):
|
|||
return res
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Object Name', size=64, translate=True, required=True),
|
||||
'model': fields.char('Object', size=64, required=True, select=1),
|
||||
'name': fields.char('Model Description', size=64, translate=True, required=True),
|
||||
'model': fields.char('Model', size=64, required=True, select=1),
|
||||
'info': fields.text('Information'),
|
||||
'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
|
||||
'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
|
||||
|
@ -97,12 +97,12 @@ class ir_model(osv.osv):
|
|||
'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the object is defined or inherited'),
|
||||
'view_ids': fields.function(_view_ids, method=True, type='one2many', obj='ir.ui.view', string='Views'),
|
||||
}
|
||||
|
||||
|
||||
_defaults = {
|
||||
'model': lambda *a: 'x_',
|
||||
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
|
||||
}
|
||||
|
||||
|
||||
def _check_model_name(self, cr, uid, ids, context=None):
|
||||
for model in self.browse(cr, uid, ids, context=context):
|
||||
if model.state=='manual':
|
||||
|
@ -114,9 +114,13 @@ class ir_model(osv.osv):
|
|||
|
||||
def _model_name_msg(self, cr, uid, ids, context=None):
|
||||
return _('The Object name must start with x_ and not contain any special character !')
|
||||
|
||||
_constraints = [
|
||||
(_check_model_name, _model_name_msg, ['model']),
|
||||
]
|
||||
_sql_constraints = [
|
||||
('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
|
||||
]
|
||||
|
||||
# overridden to allow searching both on model name (model field)
|
||||
# and model description (name field)
|
||||
|
@ -161,7 +165,7 @@ class ir_model(osv.osv):
|
|||
pass
|
||||
x_custom_model._name = model
|
||||
x_custom_model._module = False
|
||||
a = x_custom_model.createInstance(self.pool, cr)
|
||||
a = x_custom_model.create_instance(self.pool, cr)
|
||||
if (not a._columns) or ('x_name' in a._columns.keys()):
|
||||
x_name = 'x_name'
|
||||
else:
|
||||
|
@ -477,14 +481,12 @@ class ir_model_access(osv.osv):
|
|||
|
||||
if isinstance(model, browse_record):
|
||||
assert model._table_name == 'ir.model', 'Invalid model object'
|
||||
model_name = model.name
|
||||
model_name = model.model
|
||||
else:
|
||||
model_name = model
|
||||
|
||||
# osv_memory objects can be read by everyone, as they only return
|
||||
# results that belong to the current user (except for superuser)
|
||||
model_obj = self.pool.get(model_name)
|
||||
if isinstance(model_obj, osv.osv_memory):
|
||||
# TransientModel records have no access rights, only an implicit access rule
|
||||
if self.pool.get(model_name).is_transient():
|
||||
return True
|
||||
|
||||
# We check if a specific rule exists
|
||||
|
@ -519,7 +521,7 @@ class ir_model_access(osv.osv):
|
|||
}
|
||||
|
||||
raise except_orm(_('AccessError'), msgs[mode] % (model_name, groups) )
|
||||
return r
|
||||
return r or False
|
||||
|
||||
__cache_clearing_methods = []
|
||||
|
||||
|
|
|
@ -26,8 +26,7 @@ from functools import partial
|
|||
import tools
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
from tools.misc import unquote as unquote
|
||||
|
||||
SUPERUSER_UID = 1
|
||||
from openerp import SUPERUSER_ID
|
||||
|
||||
class ir_rule(osv.osv):
|
||||
_name = 'ir.rule'
|
||||
|
@ -68,7 +67,7 @@ class ir_rule(osv.osv):
|
|||
return res
|
||||
|
||||
def _check_model_obj(self, cr, uid, ids, context=None):
|
||||
return not any(isinstance(self.pool.get(rule.model_id.model), osv.osv_memory) for rule in self.browse(cr, uid, ids, context))
|
||||
return not any(self.pool.get(rule.model_id.model).is_transient() for rule in self.browse(cr, uid, ids, context))
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=128, select=1),
|
||||
|
@ -104,7 +103,7 @@ class ir_rule(osv.osv):
|
|||
if mode not in self._MODES:
|
||||
raise ValueError('Invalid mode: %r' % (mode,))
|
||||
|
||||
if uid == SUPERUSER_UID:
|
||||
if uid == SUPERUSER_ID:
|
||||
return None
|
||||
cr.execute("""SELECT r.id
|
||||
FROM ir_rule r
|
||||
|
@ -117,10 +116,10 @@ class ir_rule(osv.osv):
|
|||
rule_ids = [x[0] for x in cr.fetchall()]
|
||||
if rule_ids:
|
||||
# browse user as super-admin root to avoid access errors!
|
||||
user = self.pool.get('res.users').browse(cr, SUPERUSER_UID, uid)
|
||||
user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid)
|
||||
global_domains = [] # list of domains
|
||||
group_domains = {} # map: group -> list of domains
|
||||
for rule in self.browse(cr, SUPERUSER_UID, rule_ids):
|
||||
for rule in self.browse(cr, SUPERUSER_ID, rule_ids):
|
||||
# read 'domain' as UID to have the correct eval context for the rule.
|
||||
rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain']
|
||||
dom = expression.normalize(rule_domain)
|
||||
|
|
|
@ -96,6 +96,19 @@ class view(osv.osv):
|
|||
if not cr.fetchone():
|
||||
cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, type, inherit_id)')
|
||||
|
||||
def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
|
||||
"""Retrieves the architecture of views that inherit from the given view.
|
||||
|
||||
:param int view_id: id of the view whose inheriting views should be retrieved
|
||||
:param str model: model identifier of the view's related model (for double-checking)
|
||||
:rtype: list of tuples
|
||||
:return: [(view_arch,view_id), ...]
|
||||
"""
|
||||
cr.execute("""SELECT arch, id FROM ir_ui_view WHERE inherit_id=%s AND model=%s
|
||||
ORDER BY priority""",
|
||||
(view_id, model))
|
||||
return cr.fetchall()
|
||||
|
||||
def write(self, cr, uid, ids, vals, context={}):
|
||||
if not isinstance(ids, (list, tuple)):
|
||||
ids = [ids]
|
||||
|
@ -159,10 +172,10 @@ class view(osv.osv):
|
|||
label_string = ""
|
||||
if label:
|
||||
for lbl in eval(label):
|
||||
if t.has_key(str(lbl)) and str(t[lbl])=='False':
|
||||
if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
|
||||
label_string = label_string + ' '
|
||||
else:
|
||||
label_string = label_string + " " + t[lbl]
|
||||
label_string = label_string + " " + tools.ustr(t[lbl])
|
||||
labels[str(t['id'])] = (a['id'],label_string)
|
||||
g = graph(nodes, transitions, no_ancester)
|
||||
g.process(start)
|
||||
|
|
|
@ -19,18 +19,15 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
from osv import osv
|
||||
from osv.orm import orm_memory
|
||||
import openerp
|
||||
|
||||
class osv_memory_autovacuum(osv.osv_memory):
|
||||
class osv_memory_autovacuum(openerp.osv.osv.osv_memory):
|
||||
""" Expose the osv_memory.vacuum() method to the cron jobs mechanism. """
|
||||
_name = 'osv_memory.autovacuum'
|
||||
|
||||
def power_on(self, cr, uid, context=None):
|
||||
for model in self.pool.obj_list():
|
||||
obj = self.pool.get(model)
|
||||
if isinstance(obj, orm_memory):
|
||||
obj.vaccum(cr, uid)
|
||||
for model in self.pool.models.values():
|
||||
if model.is_transient():
|
||||
model._transient_vacuum(cr, uid)
|
||||
return True
|
||||
|
||||
osv_memory_autovacuum()
|
||||
|
||||
|
|
|
@ -20,6 +20,5 @@
|
|||
##############################################################################
|
||||
import wizard_menu
|
||||
import wizard_screen
|
||||
import create_action
|
||||
# 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>
|
||||
</record>
|
||||
<act_window context="{'model_id': active_id}" id="act_menu_create" name="Create Menu" res_model="wizard.ir.model.menu.create" target="new" view_mode="form"/>
|
||||
<wizard
|
||||
id="wizard_server_action_create"
|
||||
model="ir.actions.server"
|
||||
name="server.action.create"
|
||||
string="Create Action"
|
||||
menu="False"
|
||||
/>
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -107,48 +107,59 @@ class module(osv.osv):
|
|||
view_obj = self.pool.get('ir.ui.view')
|
||||
report_obj = self.pool.get('ir.actions.report.xml')
|
||||
menu_obj = self.pool.get('ir.ui.menu')
|
||||
mlist = self.browse(cr, uid, ids, context=context)
|
||||
mnames = {}
|
||||
for m in mlist:
|
||||
# skip uninstalled modules below,
|
||||
# no data to find anyway
|
||||
if m.state in ('installed', 'to upgrade', 'to remove'):
|
||||
mnames[m.name] = m.id
|
||||
res[m.id] = {
|
||||
'menus_by_module':[],
|
||||
'reports_by_module':[],
|
||||
|
||||
dmodels = []
|
||||
if field_name is None or 'views_by_module' in field_name:
|
||||
dmodels.append('ir.ui.view')
|
||||
if field_name is None or 'reports_by_module' in field_name:
|
||||
dmodels.append('ir.actions.report.xml')
|
||||
if field_name is None or 'menus_by_module' in field_name:
|
||||
dmodels.append('ir.ui.menu')
|
||||
assert dmodels, "no models for %s" % field_name
|
||||
|
||||
for module_rec in self.browse(cr, uid, ids, context=context):
|
||||
res[module_rec.id] = {
|
||||
'menus_by_module': [],
|
||||
'reports_by_module': [],
|
||||
'views_by_module': []
|
||||
}
|
||||
|
||||
if not mnames:
|
||||
return res
|
||||
# Skip uninstalled modules below, no data to find anyway.
|
||||
if module_rec.state not in ('installed', 'to upgrade', 'to remove'):
|
||||
continue
|
||||
|
||||
view_id = model_data_obj.search(cr,uid,[('module','in', mnames.keys()),
|
||||
('model','in',('ir.ui.view','ir.actions.report.xml','ir.ui.menu'))])
|
||||
for data_id in model_data_obj.browse(cr,uid,view_id,context):
|
||||
# We use try except, because views or menus may not exist
|
||||
# then, search and group ir.model.data records
|
||||
imd_models = dict( [(m,[]) for m in dmodels])
|
||||
imd_ids = model_data_obj.search(cr,uid,[('module','=', module_rec.name),
|
||||
('model','in',tuple(dmodels))])
|
||||
|
||||
for imd_res in model_data_obj.read(cr, uid, imd_ids, ['model', 'res_id'], context=context):
|
||||
imd_models[imd_res['model']].append(imd_res['res_id'])
|
||||
|
||||
# For each one of the models, get the names of these ids.
|
||||
# We use try except, because views or menus may not exist.
|
||||
try:
|
||||
key = data_id.model
|
||||
res_mod_dic = res[mnames[data_id.module]]
|
||||
if key=='ir.ui.view':
|
||||
v = view_obj.browse(cr,uid,data_id.res_id)
|
||||
res_mod_dic = res[module_rec.id]
|
||||
for v in view_obj.browse(cr, uid, imd_models.get('ir.ui.view', []), context=context):
|
||||
aa = v.inherit_id and '* INHERIT ' or ''
|
||||
res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
|
||||
elif key=='ir.actions.report.xml':
|
||||
res_mod_dic['reports_by_module'].append(report_obj.browse(cr,uid,data_id.res_id).name)
|
||||
elif key=='ir.ui.menu':
|
||||
res_mod_dic['menus_by_module'].append(menu_obj.browse(cr,uid,data_id.res_id).complete_name)
|
||||
|
||||
for rx in report_obj.browse(cr, uid, imd_models.get('ir.actions.report.xml', []), context=context):
|
||||
res_mod_dic['reports_by_module'].append(rx.name)
|
||||
|
||||
for um in menu_obj.browse(cr, uid, imd_models.get('ir.ui.menu', []), context=context):
|
||||
res_mod_dic['menus_by_module'].append(um.complete_name)
|
||||
except KeyError, e:
|
||||
self.__logger.warning(
|
||||
'Data not found for reference %s[%s:%s.%s]', data_id.model,
|
||||
data_id.res_id, data_id.model, data_id.name, exc_info=True)
|
||||
pass
|
||||
'Data not found for items of %s', module_rec.name)
|
||||
except AttributeError, e:
|
||||
self.__logger.warning(
|
||||
'Data not found for items of %s %s', module_rec.name, str(e))
|
||||
except Exception, e:
|
||||
self.__logger.warning('Unknown error while browsing %s[%s]',
|
||||
data_id.model, data_id.res_id, exc_info=True)
|
||||
pass
|
||||
self.__logger.warning('Unknown error while fetching data of %s',
|
||||
module_rec.name, exc_info=True)
|
||||
for key, value in res.iteritems():
|
||||
for k, v in res[key].iteritems() :
|
||||
for k, v in res[key].iteritems():
|
||||
res[key][k] = "\n".join(sorted(v))
|
||||
return res
|
||||
|
||||
|
|
|
@ -35,9 +35,12 @@ class base_language_import(osv.osv_memory):
|
|||
'name': fields.char('Language Name',size=64 , required=True),
|
||||
'code': fields.char('Code (eg:en__US)',size=5 , required=True),
|
||||
'data': fields.binary('File', required=True),
|
||||
'overwrite': fields.boolean('Overwrite Existing Terms',
|
||||
help="If you enable this option, existing translations (including custom ones) "
|
||||
"will be overwritten and replaced by those in this file"),
|
||||
}
|
||||
|
||||
def import_lang(self, cr, uid, ids, context):
|
||||
def import_lang(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Import Language
|
||||
@param cr: the current row, from the database cursor.
|
||||
|
@ -45,8 +48,11 @@ class base_language_import(osv.osv_memory):
|
|||
@param ids: the ID or list of IDs
|
||||
@param context: A standard dictionary
|
||||
"""
|
||||
|
||||
if context is None:
|
||||
context = {}
|
||||
import_data = self.browse(cr, uid, ids)[0]
|
||||
if import_data.overwrite:
|
||||
context.update(overwrite=True)
|
||||
fileobj = TemporaryFile('w+')
|
||||
fileobj.write(base64.decodestring(import_data.data))
|
||||
|
||||
|
@ -56,7 +62,7 @@ class base_language_import(osv.osv_memory):
|
|||
fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po'
|
||||
fileobj.seek(0)
|
||||
|
||||
tools.trans_load_data(cr, fileobj, fileformat, import_data.code, lang_name=import_data.name)
|
||||
tools.trans_load_data(cr, fileobj, fileformat, import_data.code, lang_name=import_data.name, context=context)
|
||||
tools.trans_update_res_ids(cr)
|
||||
fileobj.close()
|
||||
return {}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<field name="name" width="200"/>
|
||||
<field name="code"/>
|
||||
<field name="data" colspan="4"/>
|
||||
<field name="overwrite"/>
|
||||
</group>
|
||||
<group colspan="8" col="8">
|
||||
<separator string="" colspan="8"/>
|
||||
|
|
|
@ -28,6 +28,8 @@ import base64
|
|||
from tools.translate import _
|
||||
from osv import osv, fields
|
||||
|
||||
ADDONS_PATH = tools.config['addons_path'].split(",")[-1]
|
||||
|
||||
class base_module_import(osv.osv_memory):
|
||||
""" Import Module """
|
||||
|
||||
|
@ -37,7 +39,8 @@ class base_module_import(osv.osv_memory):
|
|||
|
||||
_columns = {
|
||||
'module_file': fields.binary('Module .ZIP file', required=True),
|
||||
'state':fields.selection([('init','init'),('done','done')], 'state', readonly=True),
|
||||
'state':fields.selection([('init','init'),('done','done')],
|
||||
'state', readonly=True),
|
||||
'module_name': fields.char('Module Name', size=128),
|
||||
}
|
||||
|
||||
|
@ -48,26 +51,30 @@ class base_module_import(osv.osv_memory):
|
|||
def importzip(self, cr, uid, ids, context):
|
||||
(data,) = self.browse(cr, uid, ids , context=context)
|
||||
module_data = data.module_file
|
||||
|
||||
val = base64.decodestring(module_data)
|
||||
zip_data = base64.decodestring(module_data)
|
||||
fp = StringIO()
|
||||
fp.write(val)
|
||||
fdata = zipfile.ZipFile(fp, 'r')
|
||||
fname = fdata.namelist()[0]
|
||||
module_name = os.path.split(fname)[0]
|
||||
|
||||
ad = tools.config['addons_path'].split(",")[-1]
|
||||
|
||||
fname = os.path.join(ad, module_name+'.zip')
|
||||
fp.write(zip_data)
|
||||
try:
|
||||
fp = file(fname, 'wb')
|
||||
fp.write(val)
|
||||
fp.close()
|
||||
except IOError:
|
||||
raise osv.except_osv(_('Error !'), _('Can not create the module file: %s !') % (fname,) )
|
||||
file_data = zipfile.ZipFile(fp, 'r')
|
||||
except zipfile.BadZipfile:
|
||||
raise osv.except_osv(_('Error !'), _('File is not a zip file!'))
|
||||
init_file_name = sorted(file_data.namelist())[0]
|
||||
module_name = os.path.split(init_file_name)[0]
|
||||
|
||||
self.pool.get('ir.module.module').update_list(cr, uid, {'module_name': module_name,})
|
||||
self.write(cr, uid, ids, {'state':'done', 'module_name': module_name}, context)
|
||||
file_path = os.path.join(ADDONS_PATH, '%s.zip' % module_name)
|
||||
try:
|
||||
zip_file = open(file_path, 'wb')
|
||||
except IOError:
|
||||
raise osv.except_osv(_('Error !'),
|
||||
_('Can not create the module file: %s !') % \
|
||||
(file_path,) )
|
||||
zip_file.write(zip_data)
|
||||
zip_file.close()
|
||||
|
||||
self.pool.get('ir.module.module').update_list(cr, uid,
|
||||
{'module_name': module_name,})
|
||||
self.write(cr, uid, ids, {'state':'done', 'module_name': module_name},
|
||||
context)
|
||||
return False
|
||||
|
||||
def action_module_open(self, cr, uid, ids, context):
|
||||
|
@ -84,4 +91,4 @@ class base_module_import(osv.osv_memory):
|
|||
base_module_import()
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -12,9 +12,10 @@
|
|||
<group colspan="3" col="1">
|
||||
<field name="config_logo" widget="image" width="220" height="130" nolabel="1" colspan="1"/>
|
||||
<newline/>
|
||||
<label width="220" string="This wizard helps you add a new language to you OpenERP system. After loading a new language it becomes available as default interface language for users and partners."/>
|
||||
<label width="220" string='This wizard helps you to import a new module to your OpenERP system.
|
||||
After importing a new module you can install it by clicking on the button "Install" from the form view.'/>
|
||||
<label width="220"/>
|
||||
<label width="220" string="Please be patient, this operation may take a few minutes (depending on the number of modules currently installed)..."/>
|
||||
<label width="220" string="Please be patient, this operation may take a few minutes..."/>
|
||||
<field name="state" invisible="1"/>
|
||||
</group>
|
||||
<separator orientation="vertical" rowspan="5"/>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
help="Parameters that are used by all resources."
|
||||
domain="[('res_id','=',False)]"/>
|
||||
<separator orientation="vertical"/>
|
||||
<field name="fields_id" />
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</search>
|
||||
|
|
|
@ -143,6 +143,9 @@ class res_company(osv.osv):
|
|||
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
|
||||
'company_registry': fields.char('Company Registry', size=64),
|
||||
}
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', 'The company name must be unique !')
|
||||
]
|
||||
|
||||
def _search(self, cr, uid, args, offset=0, limit=None, order=None,
|
||||
context=None, count=False, access_rights_uid=None):
|
||||
|
@ -242,9 +245,7 @@ class res_company(osv.osv):
|
|||
return False
|
||||
|
||||
def _get_logo(self, cr, uid, ids):
|
||||
return open(os.path.join(
|
||||
tools.config['root_path'], '..', 'pixmaps', 'your_logo.png'),
|
||||
'rb') .read().encode('base64')
|
||||
return open(os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'res_company_logo.png'), 'rb') .read().encode('base64')
|
||||
|
||||
_header = """
|
||||
<header>
|
||||
|
|
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'),
|
||||
'company_id':fields.many2one('res.company', 'Company'),
|
||||
'date': fields.date('Date'),
|
||||
'base': fields.boolean('Base')
|
||||
|
||||
'base': fields.boolean('Base'),
|
||||
'position': fields.selection([('after','After Amount'),('before','Before Amount')], 'Symbol position', help="Determines where the currency symbol should be placed after or before the amount.")
|
||||
}
|
||||
_defaults = {
|
||||
'active': lambda *a: 1,
|
||||
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'res.currency', context=c)
|
||||
'position' : 'after',
|
||||
}
|
||||
_sql_constraints = [
|
||||
# this constraint does not cover all cases due to SQL NULL handling for company_id,
|
||||
# so it is complemented with a unique index (see below). The constraint and index
|
||||
# share the same prefix so that IntegrityError triggered by the index will be caught
|
||||
# and reported to the user with the constraint's error message.
|
||||
('unique_name_company_id', 'unique (name, company_id)', 'The currency code must be unique per company!'),
|
||||
]
|
||||
_order = "name"
|
||||
|
||||
def init(self, cr):
|
||||
# CONSTRAINT/UNIQUE INDEX on (name,company_id)
|
||||
# /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
|
||||
# only support field names in constraint definitions, and we need a function here:
|
||||
# we need to special-case company_id to treat all NULL company_id as equal, otherwise
|
||||
# we would allow duplicate "global" currencies (all having company_id == NULL)
|
||||
cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'res_currency_unique_name_company_id_idx'""")
|
||||
if not cr.fetchone():
|
||||
cr.execute("""CREATE UNIQUE INDEX res_currency_unique_name_company_id_idx
|
||||
ON res_currency
|
||||
(name, (COALESCE(company_id,-1)))""")
|
||||
|
||||
def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
|
||||
res = super(osv.osv, self).read(cr, user, ids, fields, context, load)
|
||||
res = super(res_currency, self).read(cr, user, ids, fields, context, load)
|
||||
currency_rate_obj = self.pool.get('res.currency.rate')
|
||||
for r in res:
|
||||
if r.__contains__('rate_ids'):
|
||||
|
@ -150,7 +169,7 @@ res_currency()
|
|||
|
||||
class res_currency_rate_type(osv.osv):
|
||||
_name = "res.currency.rate.type"
|
||||
_description = "Used to define the type of Currency Rates"
|
||||
_description = "Currency Rate Type"
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=64, required=True, translate=True),
|
||||
}
|
||||
|
|
|
@ -2,6 +2,18 @@
|
|||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_currency_search" model="ir.ui.view">
|
||||
<field name="name">res.currency.search</field>
|
||||
<field name="model">res.currency</field>
|
||||
<field name="type">search</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Currencies">
|
||||
<field name="name"/>
|
||||
<field name="active"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_currency_tree" model="ir.ui.view">
|
||||
<field name="name">res.currency.tree</field>
|
||||
<field name="model">res.currency</field>
|
||||
|
@ -9,12 +21,13 @@
|
|||
<field name="arch" type="xml">
|
||||
<tree string="Currencies">
|
||||
<field name="name"/>
|
||||
<field name="company_id" select="2" />
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="rate_ids" invisible="1"/>
|
||||
<field name="date"/>
|
||||
<field name="rate"/>
|
||||
<field name="rounding"/>
|
||||
<field name="accuracy"/>
|
||||
<field name="position"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
@ -25,23 +38,30 @@
|
|||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Currency">
|
||||
<group col="6" colspan="6">
|
||||
<field name="name" select="1"/>
|
||||
<group col="6" colspan="4">
|
||||
<field name="name"/>
|
||||
<field name="rate"/>
|
||||
<field name="company_id" select="2" groups="base.group_multi_company" />
|
||||
<field name="symbol"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
|
||||
<group col="2" colspan="2">
|
||||
<separator string="Price Accuracy" colspan="2"/>
|
||||
<field name="rounding"/>
|
||||
<field name="accuracy"/>
|
||||
</group>
|
||||
<group col="6" colspan="4">
|
||||
<group col="2" colspan="2">
|
||||
<separator string="Price Accuracy" colspan="2"/>
|
||||
<field name="rounding"/>
|
||||
<field name="accuracy"/>
|
||||
</group>
|
||||
|
||||
<group col="2" colspan="2">
|
||||
<separator string="Miscelleanous" colspan="2"/>
|
||||
<field name="base"/>
|
||||
<field name="active" select="1"/>
|
||||
<group col="2" colspan="2">
|
||||
<separator string="Display" colspan="2"/>
|
||||
<field name="symbol"/>
|
||||
<field name="position"/>
|
||||
</group>
|
||||
|
||||
<group col="2" colspan="2">
|
||||
<separator string="Miscelleanous" colspan="2"/>
|
||||
<field name="base"/>
|
||||
<field name="active" select="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<field colspan="4" mode="tree,form" name="rate_ids" nolabel="1" attrs="{'readonly':[('base','=',True)]}">
|
||||
|
@ -62,6 +82,7 @@
|
|||
<field name="res_model">res.currency</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_currency_search"/>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_currency_form" id="menu_action_currency_form" parent="menu_localisation" sequence="3"/>
|
||||
|
|
|
@ -131,12 +131,15 @@ class res_partner(osv.osv):
|
|||
'customer': fields.boolean('Customer', help="Check this box if the partner is a customer."),
|
||||
'supplier': fields.boolean('Supplier', help="Check this box if the partner is a supplier. If it's not checked, purchase people will not see it when encoding a purchase order."),
|
||||
'city': fields.related('address', 'city', type='char', string='City'),
|
||||
'function': fields.related('address', 'function', type='char', string='function'),
|
||||
'subname': fields.related('address', 'name', type='char', string='Contact Name'),
|
||||
'phone': fields.related('address', 'phone', type='char', string='Phone'),
|
||||
'mobile': fields.related('address', 'mobile', type='char', string='Mobile'),
|
||||
'country': fields.related('address', 'country_id', type='many2one', relation='res.country', string='Country'),
|
||||
'employee': fields.boolean('Employee', help="Check this box if the partner is an Employee."),
|
||||
'email': fields.related('address', 'email', type='char', size=240, string='E-mail'),
|
||||
'company_id': fields.many2one('res.company', 'Company', select=1),
|
||||
'color': fields.integer('Color Index'),
|
||||
}
|
||||
|
||||
def _default_category(self, cr, uid, context={}):
|
||||
|
@ -150,6 +153,7 @@ class res_partner(osv.osv):
|
|||
'address': [{'type': 'default'}],
|
||||
'category_id': _default_category,
|
||||
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'res.partner', context=c),
|
||||
'color': 0,
|
||||
}
|
||||
|
||||
def copy(self, cr, uid, id, default={}, context={}):
|
||||
|
@ -300,6 +304,7 @@ class res_partner_address(osv.osv):
|
|||
'active': fields.boolean('Active', help="Uncheck the active field to hide the contact."),
|
||||
# 'company_id': fields.related('partner_id','company_id',type='many2one',relation='res.company',string='Company', store=True),
|
||||
'company_id': fields.many2one('res.company', 'Company',select=1),
|
||||
'color': fields.integer('Color Index'),
|
||||
}
|
||||
_defaults = {
|
||||
'active': lambda *a: 1,
|
||||
|
|
|
@ -90,67 +90,88 @@
|
|||
|
||||
<record id="res_partner_asus" model="res.partner">
|
||||
<field name="name">ASUStek</field>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="website">www.asustek.com</field>
|
||||
</record>
|
||||
<record id="res_partner_agrolait" model="res.partner">
|
||||
<field name="name">Agrolait</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_8')])]" name="category_id"/>
|
||||
<field eval="[(6, 0, [ref('base.res_partner_category_0')])]" name="category_id"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="website">www.agrolait.com</field>
|
||||
</record>
|
||||
<record id="res_partner_c2c" model="res.partner">
|
||||
<field name="name">Camptocamp</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_10'), ref('res_partner_category_5')])]" name="category_id"/>
|
||||
<field name="supplier">1</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="website">www.camptocamp.com</field>
|
||||
</record>
|
||||
<record id="res_partner_sednacom" model="res.partner">
|
||||
<field name="website">http://www.syleam.fr</field>
|
||||
<field name="website">www.syleam.fr</field>
|
||||
<field name="name">Syleam</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
</record>
|
||||
<record id="res_partner_thymbra" model="res.partner">
|
||||
<field name="name">Thymbra</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/>
|
||||
<field name="website">www.thymbra.com/</field>
|
||||
</record>
|
||||
<record id="res_partner_desertic_hispafuentes" model="res.partner">
|
||||
<field name="name">Axelor</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/>
|
||||
<field name="supplier">1</field>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
<field name="website">www.axelor.com/</field>
|
||||
</record>
|
||||
<record id="res_partner_tinyatwork" model="res.partner">
|
||||
<field name="name">Tiny AT Work</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_5'), ref('res_partner_category_10')])]" name="category_id"/>
|
||||
<field name="website">www.tinyatwork.com/</field>
|
||||
</record>
|
||||
<record id="res_partner_2" model="res.partner">
|
||||
<field name="name">Bank Wealthy and sons</field>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="website">www.wealthyandsons.com/</field>
|
||||
</record>
|
||||
<record id="res_partner_3" model="res.partner">
|
||||
<field name="name">China Export</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="website">www.chinaexport.com/</field>
|
||||
</record>
|
||||
<record id="res_partner_4" model="res.partner">
|
||||
<field name="name">Distrib PC</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="website">www.distribpc.com/</field>
|
||||
</record>
|
||||
<record id="res_partner_5" model="res.partner">
|
||||
<field name="name">Ecole de Commerce de Liege</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
<field name="website">www.eci-liege.info//</field>
|
||||
</record>
|
||||
<record id="res_partner_6" model="res.partner">
|
||||
<field name="name">Elec Import</field>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
<field name="address" eval="[]"/>
|
||||
</record>
|
||||
<record id="res_partner_maxtor" model="res.partner">
|
||||
|
@ -159,6 +180,7 @@
|
|||
<field name="user_id" ref="user_demo"/>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
<field name="address" eval="[]"/>
|
||||
</record>
|
||||
<record id="res_partner_seagate" model="res.partner">
|
||||
|
@ -177,11 +199,11 @@
|
|||
<field name="address" eval="[]"/>
|
||||
</record>
|
||||
<record id="res_partner_9" model="res.partner">
|
||||
<field name="website">http://balmerinc.com</field>
|
||||
<field name="website">www.balmerinc.com</field>
|
||||
<field name="name">BalmerInc S.A.</field>
|
||||
<field eval="12000.00" name="credit_limit"/>
|
||||
<field name="ref">or</field>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
</record>
|
||||
|
@ -190,6 +212,7 @@
|
|||
<field name="ean13">3020170000003</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
</record>
|
||||
<record id="res_partner_11" model="res.partner">
|
||||
<field name="name">Leclerc</field>
|
||||
|
@ -205,6 +228,7 @@
|
|||
<field name="parent_id" ref="res_partner_10"/>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_11')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
</record>
|
||||
<record id="res_partner_15" model="res.partner">
|
||||
<field name="name">Magazin BML 1</field>
|
||||
|
@ -219,6 +243,8 @@
|
|||
<field name="name">Université de Liège</field>
|
||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
<field name="website">http://www.ulg.ac.be/</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
|
@ -230,16 +256,19 @@
|
|||
<field model="res.users" name="user_id" search="[('name', '=', u'Thomas Lebrun')]"/>
|
||||
<field name="name">Dubois sprl</field>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="website">http://www.dubois.be/</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_ericdubois0" model="res.partner">
|
||||
<field name="name">Eric Dubois</field>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="user_demo"/>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_fabiendupont0" model="res.partner">
|
||||
<field name="name">Fabien Dupont</field>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_lucievonck0" model="res.partner">
|
||||
|
@ -250,32 +279,41 @@
|
|||
<record id="res_partner_notsotinysarl0" model="res.partner">
|
||||
<field name="name">NotSoTiny SARL</field>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="website">notsotiny.be</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_theshelvehouse0" model="res.partner">
|
||||
<field name="name">The Shelve House</field>
|
||||
<field eval="[(6,0,[ref('res_partner_category_retailers0')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_vickingdirect0" model="res.partner">
|
||||
<field name="name">Vicking Direct</field>
|
||||
<field eval="[(6,0,[ref('res_partner_category_miscellaneoussuppliers0')])]" name="category_id"/>
|
||||
<field name="supplier">1</field>
|
||||
<field name="customer">0</field>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="website">vicking-direct.be</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_woodywoodpecker0" model="res.partner">
|
||||
<field name="name">Wood y Wood Pecker</field>
|
||||
<field eval="[(6,0,[ref('res_partner_category_woodsuppliers0')])]" name="category_id"/>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="website">woodywoodpecker.com</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_zerooneinc0" model="res.partner">
|
||||
<field name="name">ZeroOne Inc</field>
|
||||
<field eval="[(6,0,[ref('res_partner_category_consumers0')])]" name="category_id"/>
|
||||
<field name="address" eval="[]"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="website">http://www.zerooneinc.com/</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
|
@ -312,6 +350,7 @@
|
|||
<field name="email">info@axelor.com</field>
|
||||
<field name="phone">+33 1 64 61 04 01</field>
|
||||
<field name="street">12 rue Albert Einstein</field>
|
||||
<field name="type">default</field>
|
||||
<field name="partner_id" ref="res_partner_desertic_hispafuentes"/>
|
||||
</record>
|
||||
<record id="res_partner_address_3" model="res.partner.address">
|
||||
|
@ -329,6 +368,8 @@
|
|||
<field name="zip">23410</field>
|
||||
<field model="res.country" name="country_id" search="[('name','=','Taiwan')]"/>
|
||||
<field name="street">31 Hong Kong street</field>
|
||||
<field name="email">info@asustek.com</field>
|
||||
<field name="phone">+ 1 64 61 04 01</field>
|
||||
<field name="type">default</field>
|
||||
<field name="partner_id" ref="res_partner_asus"/>
|
||||
</record>
|
||||
|
@ -338,6 +379,8 @@
|
|||
<field name="zip">23540</field>
|
||||
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
|
||||
<field name="street">56 Beijing street</field>
|
||||
<field name="email">info@maxtor.com</field>
|
||||
<field name="phone">+ 11 8528 456 789</field>
|
||||
<field name="type">default</field>
|
||||
<field name="partner_id" ref="res_partner_maxtor"/>
|
||||
</record>
|
||||
|
@ -348,6 +391,8 @@
|
|||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||
<field name="street">23 rue du Vieux Bruges</field>
|
||||
<field name="type">default</field>
|
||||
<field name="email">info@elecimport.com</field>
|
||||
<field name="phone">+ 32 025 897 456</field>
|
||||
<field name="partner_id" ref="res_partner_6"/>
|
||||
</record>
|
||||
<record id="res_partner_address_7" model="res.partner.address">
|
||||
|
@ -357,6 +402,8 @@
|
|||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||
<field name="street">42 rue de la Lesse</field>
|
||||
<field name="type">default</field>
|
||||
<field name="email">info@distribpc.com</field>
|
||||
<field name="phone">+ 32 081256987</field>
|
||||
<field name="partner_id" ref="res_partner_4"/>
|
||||
</record>
|
||||
<record id="res_partner_address_8" model="res.partner.address">
|
||||
|
@ -366,7 +413,10 @@
|
|||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||
<field name="street">69 rue de Chimay</field>
|
||||
<field name="type">default</field>
|
||||
<field name="email">s.l@agrolait.be</field>
|
||||
<field name="phone">003281588558</field>
|
||||
<field name="partner_id" ref="res_partner_agrolait"/>
|
||||
<field name="title" ref="base.res_partner_title_madam"/>
|
||||
</record>
|
||||
<record id="res_partner_address_8delivery" model="res.partner.address">
|
||||
<field name="city">Wavre</field>
|
||||
|
@ -375,7 +425,10 @@
|
|||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||
<field name="street">71 rue de Chimay</field>
|
||||
<field name="type">delivery</field>
|
||||
<field name="email">p.l@agrolait.be</field>
|
||||
<field name="phone">003281588557</field>
|
||||
<field name="partner_id" ref="res_partner_agrolait"/>
|
||||
<field name="title" ref="base.res_partner_title_sir"/>
|
||||
</record>
|
||||
<record id="res_partner_address_8invoice" model="res.partner.address">
|
||||
<field name="city">Wavre</field>
|
||||
|
@ -384,7 +437,10 @@
|
|||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||
<field name="street">69 rue de Chimay</field>
|
||||
<field name="type">invoice</field>
|
||||
<field name="email">serge.l@agrolait.be</field>
|
||||
<field name="phone">003281588556</field>
|
||||
<field name="partner_id" ref="res_partner_agrolait"/>
|
||||
<field name="title" ref="base.res_partner_title_sir"/>
|
||||
</record>
|
||||
<record id="res_partner_address_9" model="res.partner.address">
|
||||
<field name="city">Paris</field>
|
||||
|
@ -393,7 +449,10 @@
|
|||
<field model="res.country" name="country_id" search="[('name','=','France')]"/>
|
||||
<field name="street">1 rue Rockfeller</field>
|
||||
<field name="type">default</field>
|
||||
<field name="email">a.g@wealthyandsons.com</field>
|
||||
<field name="phone">003368978776</field>
|
||||
<field name="partner_id" ref="res_partner_2"/>
|
||||
<field name="title" ref="base.res_partner_title_sir"/>
|
||||
</record>
|
||||
<record id="res_partner_address_11" model="res.partner.address">
|
||||
<field name="city">Alencon</field>
|
||||
|
@ -412,48 +471,84 @@
|
|||
<field name="zip">6985</field>
|
||||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||
<field name="street">2 Impasse de la Soif</field>
|
||||
<field name="email">k.lesbrouffe@eci-liege.info</field>
|
||||
<field name="phone">+32 421 52571</field>
|
||||
<field name="type">default</field>
|
||||
<field name="partner_id" ref="res_partner_5"/>
|
||||
</record>
|
||||
<record id="res_partner_address_zen" model="res.partner.address">
|
||||
<field name="city">Shanghai</field>
|
||||
<field name="name">Zen</field>
|
||||
<field name="zip">4785552</field>
|
||||
<field name="zip">478552</field>
|
||||
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
|
||||
<field name="street">52 Chop Suey street</field>
|
||||
<field name="type">default</field>
|
||||
<field name="email">zen@chinaexport.com</field>
|
||||
<field name="phone">+86-751-64845671</field>
|
||||
<field name="partner_id" ref="res_partner_3"/>
|
||||
</record>
|
||||
<record id="res_partner_address_12" model="res.partner.address">
|
||||
<field name="type">default</field>
|
||||
<field name="name">Centrale</field>
|
||||
<field name="city">Grenoble</field>
|
||||
<field name="name">Loïc Dupont</field>
|
||||
<field name="zip">38100</field>
|
||||
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
|
||||
<field name="street">Rue Lavoisier 145</field>
|
||||
<field name="type">default</field>
|
||||
<field name="email">l.dupont@tecsas.fr</field>
|
||||
<field name="phone">+33-658-256545</field>
|
||||
<field name="partner_id" ref="res_partner_10"/>
|
||||
</record>
|
||||
<record id="res_partner_address_13" model="res.partner.address">
|
||||
<field name="type">default</field>
|
||||
<field name="name">Centrale d'achats 1</field>
|
||||
<field name="name">Carl François</field>
|
||||
<field name="city">Bruxelles</field>
|
||||
<field name="zip">1000</field>
|
||||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||
<field name="street">89 Chaussée de Waterloo</field>
|
||||
<field name="email">carl.françois@bml.be</field>
|
||||
<field name="phone">+32-258-256545</field>
|
||||
<field name="partner_id" ref="res_partner_14"/>
|
||||
</record>
|
||||
<record id="res_partner_address_14" model="res.partner.address">
|
||||
<field name="type">default</field>
|
||||
<field name="name">Shop 1</field>
|
||||
<field name="name">Lucien Ferguson</field>
|
||||
<field name="street">89 Chaussée de Liège</field>
|
||||
<field name="city">Namur</field>
|
||||
<field name="zip">5000</field>
|
||||
<field name="email">lucien.ferguson@bml.be</field>
|
||||
<field name="phone">+32-621-568978</field>
|
||||
<field name="partner_id" ref="res_partner_15"/>
|
||||
</record>
|
||||
<record id="res_partner_address_15" model="res.partner.address">
|
||||
<field name="type">default</field>
|
||||
<field name="name">Shop 2</field>
|
||||
<field name="name">Marine Leclerc</field>
|
||||
<field name="street">rue Grande</field>
|
||||
<field name="city">Brest</field>
|
||||
<field name="zip">29200</field>
|
||||
<field name="email">marine@leclerc.fr</field>
|
||||
<field name="phone">+33-298.334558</field>
|
||||
<field name="partner_id" ref="res_partner_11"/>
|
||||
</record>
|
||||
<record id="res_partner_address_16" model="res.partner.address">
|
||||
<field name="type">default</field>
|
||||
<field name="name">Shop 3</field>
|
||||
<field name="type">invoice</field>
|
||||
<field name="name">Claude Leclerc</field>
|
||||
<field name="street">rue Grande</field>
|
||||
<field name="city">Brest</field>
|
||||
<field name="zip">29200</field>
|
||||
<field name="email">claude@leclerc.fr</field>
|
||||
<field name="phone">+33-298.334598</field>
|
||||
<field name="partner_id" ref="res_partner_11"/>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_address_accent" model="res.partner.address">
|
||||
<field name="type">default</field>
|
||||
<field name="city">Liège</field>
|
||||
<field name="street">Université de Liège</field>
|
||||
<field name="name">Martine Ohio</field>
|
||||
<field name="street">Place du 20Août</field>
|
||||
<field name="city">Liège</field>
|
||||
<field name="zip">4000</field>
|
||||
<field name="email">martine.ohio@ulg.ac.be</field>
|
||||
<field name="phone">+32-45895245</field>
|
||||
<field name="partner_id" ref="res_partner_accent"/>
|
||||
</record>
|
||||
<record id="res_partner_address_Camptocamp" model="res.partner.address">
|
||||
|
@ -472,6 +567,8 @@
|
|||
<field name="zip">95014</field>
|
||||
<field model="res.country" name="country_id" search="[('name','=','United States')]"/>
|
||||
<field name="street">10200 S. De Anza Blvd</field>
|
||||
<field name="email">info@seagate.com</field>
|
||||
<field name="phone">+1 408 256987</field>
|
||||
<field name="type">default</field>
|
||||
<field name="partner_id" ref="res_partner_seagate"/>
|
||||
</record>
|
||||
|
@ -552,7 +649,12 @@
|
|||
|
||||
<record id="res_partner_address_brussels0" model="res.partner.address">
|
||||
<field eval="'Brussels'" name="city"/>
|
||||
<field eval="'Brussels'" name="name"/>
|
||||
<field eval="'Leen Vandenloep'" name="name"/>
|
||||
<field eval="'Puurs'" name="city"/>
|
||||
<field eval="'2870'" name="zip"/>
|
||||
<field name="country_id" ref="base.be"/>
|
||||
<field eval="'(+32).70.12.85.00'" name="phone"/>
|
||||
<field eval="'Schoonmansveld 28'" name="street"/>
|
||||
<field name="partner_id" ref="res_partner_vickingdirect0"/>
|
||||
<field name="country_id" ref="base.be"/>
|
||||
</record>
|
||||
|
@ -562,6 +664,7 @@
|
|||
<field eval="'Kainuu'" name="city"/>
|
||||
<field eval="'Roger Pecker'" name="name"/>
|
||||
<field name="partner_id" ref="res_partner_woodywoodpecker0"/>
|
||||
<field eval="'(+358).9.589 689'" name="phone"/>
|
||||
<field name="country_id" ref="base.fi"/>
|
||||
</record>
|
||||
|
||||
|
@ -597,10 +700,13 @@
|
|||
|
||||
<record id="res_partner_address_ericdubois0" model="res.partner.address">
|
||||
<field eval="'Mons'" name="city"/>
|
||||
<field eval="'Eric Dubois'" name="name"/>
|
||||
<field eval="'7000'" name="zip"/>
|
||||
<field name="partner_id" ref="res_partner_ericdubois0"/>
|
||||
<field name="country_id" ref="base.be"/>
|
||||
<field eval="'Chaussée de Binche, 27'" name="street"/>
|
||||
<field eval="'e.dubois@gmail.com'" name="email"/>
|
||||
<field eval="'(+32).758 958 789'" name="phone"/>
|
||||
</record>
|
||||
|
||||
|
||||
|
|
|
@ -105,11 +105,80 @@
|
|||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="contacts_kanban_view">
|
||||
<field name="name">res.partner.address.kanban</field>
|
||||
<field name="model">res.partner.address</field>
|
||||
<field name="type">kanban</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban >
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<t t-set="color" t-value="kanban_color(record.color.raw_value || record.name.raw_value)"/>
|
||||
<div t-att-class="color + (record.color.raw_value == 1 ? ' oe_kanban_color_alert' : '')">
|
||||
<div class="oe_kanban_box oe_kanban_color_border">
|
||||
<div class="oe_kanban_box_header oe_kanban_color_bgdark oe_kanban_color_border oe_kanban_draghandle">
|
||||
<table class="oe_kanban_table">
|
||||
<tr>
|
||||
<td class="oe_kanban_title1" align="left" valign="middle">
|
||||
<field name="name"/>
|
||||
</td>
|
||||
<td valign="top" width="22">
|
||||
<img t-att-src="kanban_gravatar(record.email.value, 22)" class="oe_kanban_gravatar"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_box_show_onclick_trigger oe_kanban_color_border">
|
||||
<table class="oe_kanban_table">
|
||||
<tr>
|
||||
<td valign="top" width="22" align="left">
|
||||
<img src="/web/static/src/img/persons.png"/>
|
||||
</td>
|
||||
<td valign="top" align="left">
|
||||
<div class="oe_kanban_title2">
|
||||
<field name="title"/>
|
||||
<t t-if="record.title.raw_value && record.function.raw_value">,</t>
|
||||
<field name="function"/>
|
||||
</div>
|
||||
<div class="oe_kanban_title3">
|
||||
<field name="partner_id"/>
|
||||
<t t-if="record.partner_id.raw_value && record.country_id.raw_value">,</t>
|
||||
<field name="country_id"/>
|
||||
</div>
|
||||
<div class="oe_kanban_title3">
|
||||
<i><field name="email"/>
|
||||
<t t-if="record.phone.raw_value && record.email.raw_value">,</t>
|
||||
<field name="phone"/></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="oe_kanban_buttons_set oe_kanban_color_border oe_kanban_color_bglight oe_kanban_box_show_onclick">
|
||||
<div class="oe_kanban_left">
|
||||
<a string="Edit" icon="gtk-edit" type="edit"/>
|
||||
<a string="Change Color" icon="color-picker" type="color" name="color"/>
|
||||
<a title="Mail" t-att-href="'mailto:'+record.email.value" style="text-decoration: none;" >
|
||||
<img src="/web/static/src/img/icons/terp-mail-message-new.png" border="0" width="16" height="16"/>
|
||||
</a>
|
||||
</div>
|
||||
<br class="oe_kanban_clear"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_partner_address_form" model="ir.actions.act_window">
|
||||
<field name="name">Addresses</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.partner.address</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field name="context">{"search_default_customer":1}</field>
|
||||
<field name="search_view_id" ref="view_res_partner_address_filter"/>
|
||||
<field name="help">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 the interactions with a partner such as opportunities, emails, or sales orders issued.</field>
|
||||
|
@ -353,12 +422,81 @@
|
|||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Partner Kanban View -->
|
||||
<record model="ir.ui.view" id="res_partner_kanban_view">
|
||||
<field name="name">RES - PARTNER KANBAN</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="type">kanban</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<t t-set="color" t-value="kanban_color(record.color.raw_value || record.name.raw_value)"/>
|
||||
<div t-att-class="color + (record.color.raw_value == 1 ? ' oe_kanban_color_alert' : '')">
|
||||
<div class="oe_kanban_box oe_kanban_color_border">
|
||||
<div class="oe_kanban_box_header oe_kanban_color_bgdark oe_kanban_color_border oe_kanban_draghandle">
|
||||
<table class="oe_kanban_table">
|
||||
<tr>
|
||||
<td class="oe_kanban_title1" align="left" valign="middle">
|
||||
<field name="name"/>
|
||||
</td>
|
||||
<td valign="top" width="22">
|
||||
<img t-att-src="kanban_gravatar(record.email.value, 22)" class="oe_kanban_gravatar"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_box_show_onclick_trigger oe_kanban_color_border">
|
||||
<table class="oe_kanban_table">
|
||||
<tr>
|
||||
<td valign="top" width="22" align="left">
|
||||
<img src="/web/static/src/img/partner.png"/>
|
||||
</td>
|
||||
<td valign="top" align="left">
|
||||
<div class="oe_kanban_title2">
|
||||
<field name="title"/>
|
||||
<t t-if="record.title.raw_value && record.country.raw_value">,</t>
|
||||
<field name="country"/>
|
||||
</div>
|
||||
<div class="oe_kanban_title3">
|
||||
<field name="subname"/>
|
||||
<t t-if="record.subname.raw_value && record.function.raw_value">,</t>
|
||||
<field name="function"/>
|
||||
</div>
|
||||
<div class="oe_kanban_title3">
|
||||
<i><field name="email"/>
|
||||
<t t-if="record.phone.raw_value && record.email.raw_value">,</t>
|
||||
<field name="phone"/></i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="oe_kanban_buttons_set oe_kanban_color_border oe_kanban_color_bglight oe_kanban_box_show_onclick">
|
||||
<div class="oe_kanban_left">
|
||||
<a string="Edit" icon="gtk-edit" type="edit"/>
|
||||
<a string="Change Color" icon="color-picker" type="color" name="color"/>
|
||||
<a title="Mail" t-att-href="'mailto:'+record.email.value" style="text-decoration: none;" >
|
||||
<img src="/web/static/src/img/icons/terp-mail-message-new.png" border="0" width="16" height="16"/>
|
||||
</a>
|
||||
</div>
|
||||
<br class="oe_kanban_clear"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_partner_form" model="ir.actions.act_window">
|
||||
<field name="name">Customers</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.partner</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="context">{"search_default_customer":1}</field>
|
||||
<field name="search_view_id" ref="view_res_partner_filter"/>
|
||||
<field name="help">A customer is an entity you do business with, like a company or an organization. A customer can have several contacts or addresses which are the people working for this company. You can use the history tab, to follow all transactions related to a customer: sales order, emails, opportunities, claims, etc. If you use the email gateway, the Outlook or the Thunderbird plugin, don't forget to register emails to each contact so that the gateway will automatically attach incoming emails to the right partner.</field>
|
||||
|
|
|
@ -35,6 +35,7 @@ from osv import fields,osv
|
|||
from osv.orm import browse_record
|
||||
from service import security
|
||||
from tools.translate import _
|
||||
import openerp.exceptions
|
||||
|
||||
class groups(osv.osv):
|
||||
_name = "res.groups"
|
||||
|
@ -338,7 +339,7 @@ class users(osv.osv):
|
|||
}
|
||||
|
||||
# User can write to a few of her own fields (but not her groups for example)
|
||||
SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
|
||||
SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
|
||||
|
||||
def write(self, cr, uid, ids, values, context=None):
|
||||
if not hasattr(ids, '__iter__'):
|
||||
|
@ -437,14 +438,14 @@ class users(osv.osv):
|
|||
if passwd == tools.config['admin_passwd']:
|
||||
return True
|
||||
else:
|
||||
raise security.ExceptionNoTb('AccessDenied')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
|
||||
def check(self, db, uid, passwd):
|
||||
"""Verifies that the given (uid, password) pair is authorized for the database ``db`` and
|
||||
raise an exception if it is not."""
|
||||
if not passwd:
|
||||
# empty passwords disallowed for obvious security reasons
|
||||
raise security.ExceptionNoTb('AccessDenied')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
if self._uid_cache.get(db, {}).get(uid) == passwd:
|
||||
return
|
||||
cr = pooler.get_db(db).cursor()
|
||||
|
@ -453,7 +454,7 @@ class users(osv.osv):
|
|||
(int(uid), passwd, True))
|
||||
res = cr.fetchone()[0]
|
||||
if not res:
|
||||
raise security.ExceptionNoTb('AccessDenied')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
if self._uid_cache.has_key(db):
|
||||
ulist = self._uid_cache[db]
|
||||
ulist[uid] = passwd
|
||||
|
@ -470,7 +471,7 @@ class users(osv.osv):
|
|||
cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
|
||||
res = cr.fetchone()
|
||||
if not res:
|
||||
raise security.ExceptionNoTb('Bad username or password')
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
return res[0]
|
||||
finally:
|
||||
cr.close()
|
||||
|
@ -481,7 +482,7 @@ class users(osv.osv):
|
|||
password is not used to authenticate requests.
|
||||
|
||||
:return: True
|
||||
:raise: security.ExceptionNoTb when old password is wrong
|
||||
:raise: openerp.exceptions.AccessDenied when old password is wrong
|
||||
:raise: except_osv when new password is not set or empty
|
||||
"""
|
||||
self.check(cr.dbname, uid, old_passwd)
|
||||
|
@ -553,7 +554,7 @@ class users_implied(osv.osv):
|
|||
_inherit = 'res.users'
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
groups = values.pop('groups_id')
|
||||
groups = values.pop('groups_id', None)
|
||||
user_id = super(users_implied, self).create(cr, uid, values, context)
|
||||
if groups:
|
||||
# delegate addition of groups to add implied groups
|
||||
|
@ -700,7 +701,7 @@ class users_view(osv.osv):
|
|||
self._process_values_groups(cr, uid, values, context)
|
||||
return super(users_view, self).write(cr, uid, ids, values, context)
|
||||
|
||||
def read(self, cr, uid, ids, fields, context=None, load='_classic_read'):
|
||||
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
|
||||
if not fields:
|
||||
group_fields, fields = [], self.fields_get(cr, uid, context).keys()
|
||||
else:
|
||||
|
|
|
@ -42,28 +42,32 @@
|
|||
<field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="res_widget_user_rule">
|
||||
<field name="name">res.widget.user rule</field>
|
||||
<field name="model_id" ref="model_res_widget_user"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('user_id','=',user.id),('user_id','=',False)]</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<record model="ir.rule" id="res_partner_rule">
|
||||
<field name="name">res.partner company</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="global" eval="True"/>
|
||||
<!-- Show partners from ancestors and descendants companies (or company-less), this is usually a better
|
||||
default for multicompany setups. -->
|
||||
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
||||
</record>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="ir.rule" id="multi_company_default_rule">
|
||||
<field name="name">Multi_company_default company</field>
|
||||
<field name="model_id" ref="model_multi_company_default"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="res_widget_user_rule">
|
||||
<field name="name">res.widget.user rule</field>
|
||||
<field name="model_id" ref="model_res_widget_user"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('user_id','=',user.id),('user_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="res_partner_rule">
|
||||
<field name="name">res.partner company</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="global" eval="True"/>
|
||||
<!-- Show partners from ancestors and descendants companies (or company-less), this is usually a better
|
||||
default for multicompany setups. -->
|
||||
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="multi_company_default_rule">
|
||||
<field name="name">Multi_company_default company</field>
|
||||
<field name="model_id" ref="model_multi_company_default"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
"access_res_country_state_group_user","res_country_state group_user","model_res_country_state","group_partner_manager",1,1,1,1
|
||||
"access_res_currency_group_all","res_currency group_all","model_res_currency",,1,0,0,0
|
||||
"access_res_currency_rate_group_all","res_currency_rate group_all","model_res_currency_rate",,1,0,0,0
|
||||
"access_res_currency_rate_type_group_all","res_currency_rate_type group_all","model_res_currency_rate_type",,1,0,0,0
|
||||
"access_res_currency_group_system","res_currency group_system","model_res_currency","group_system",1,1,1,1
|
||||
"access_res_currency_rate_group_system","res_currency_rate group_system","model_res_currency_rate","group_system",1,1,1,1
|
||||
"access_res_groups_group_erp_manager","res_groups group_erp_manager","model_res_groups","group_erp_manager",1,1,1,1
|
||||
|
@ -125,3 +126,4 @@
|
|||
"access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0
|
||||
"access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0
|
||||
"access_ir_actions_todo_category","ir_actions_todo_category","model_ir_actions_todo_category","group_system",1,1,1,1
|
||||
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
|
||||
|
|
|
|
@ -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}: |
|
||||
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.sort()
|
||||
assert res_ids == all_ids, "Searching against empty set failed, returns %r" % res_ids
|
||||
-
|
||||
Test the '(not) like/in' behavior. res.partner and its parent_id column are used because
|
||||
parent_id is a many2one, allowing to test the Null value, and there are actually some
|
||||
null and non-null values in the demo data.
|
||||
-
|
||||
!python {model: res.partner }: |
|
||||
partner_ids = self.search(cr, uid, [])
|
||||
partner_ids.sort()
|
||||
max_partner_id = max(partner_ids)
|
||||
|
||||
# Grab test sample data without using a normal
|
||||
# search domain, because we want to test these later,
|
||||
# so we can't rely on them!
|
||||
partners = self.browse(cr, uid, partner_ids)
|
||||
with_parent = []
|
||||
without_parent = []
|
||||
with_website = []
|
||||
for x in partners:
|
||||
if x.parent_id:
|
||||
with_parent.append(x.id)
|
||||
else:
|
||||
without_parent.append(x.id)
|
||||
if x.website:
|
||||
with_website.append(x.id)
|
||||
with_parent.sort()
|
||||
without_parent.sort()
|
||||
with_website.sort()
|
||||
|
||||
# We treat null values differently than in SQL. For instance in SQL:
|
||||
# SELECT id FROM res_partner WHERE parent_id NOT IN (0)
|
||||
# will return only the records with non-null parent_id.
|
||||
# SELECT id FROM res_partner WHERE parent_id IN (0)
|
||||
# will return expectedly nothing (our ids always begin at 1).
|
||||
# This means the union of those two results will give only some
|
||||
# records, but not all present in database.
|
||||
#
|
||||
# When using domains and the ORM's search method, we think it is
|
||||
# more intuitive that the union returns all the records, and that
|
||||
# a domain like ('parent_id', 'not in', [0]) will return all
|
||||
# the records. For instance, if you perform a search for the companies
|
||||
# that don't have OpenERP has a parent company, you expect to find,
|
||||
# among others, the companies that don't have parent company.
|
||||
#
|
||||
# ('parent_id', 'not in', [0]) must give the same result than
|
||||
# ('parent_id', 'not in', []), i.e. a empty set or a set with non-
|
||||
# existing values be treated similarly if we simply check that some
|
||||
# existing value belongs to them.
|
||||
|
||||
res_0 = self.search(cr, uid, [('parent_id', 'not like', 'probably_unexisting_name')]) # get all rows, included null parent_id
|
||||
res_0.sort()
|
||||
res_1 = self.search(cr, uid, [('parent_id', 'not in', [max_partner_id + 1])]) # get all rows, included null parent_id
|
||||
res_1.sort()
|
||||
res_2 = self.search(cr, uid, [('parent_id', 'not in', False)]) # get rows with not null parent_id, deprecated syntax
|
||||
res_2.sort()
|
||||
res_3 = self.search(cr, uid, [('parent_id', 'not in', [])]) # get all rows, included null parent_id
|
||||
res_3.sort()
|
||||
res_4 = self.search(cr, uid, [('parent_id', 'not in', [False])]) # get rows with not null parent_id
|
||||
res_4.sort()
|
||||
assert res_0 == partner_ids
|
||||
assert res_1 == partner_ids
|
||||
assert res_2 == with_parent
|
||||
assert res_3 == partner_ids
|
||||
assert res_4 == with_parent
|
||||
# The results of these queries, when combined with queries 0..4 must
|
||||
# give the whole set of ids.
|
||||
res_5 = self.search(cr, uid, [('parent_id', 'like', 'probably_unexisting_name')])
|
||||
res_5.sort()
|
||||
res_6 = self.search(cr, uid, [('parent_id', 'in', [max_partner_id + 1])])
|
||||
res_6.sort()
|
||||
res_7 = self.search(cr, uid, [('parent_id', 'in', False)])
|
||||
res_7.sort()
|
||||
res_8 = self.search(cr, uid, [('parent_id', 'in', [])])
|
||||
res_8.sort()
|
||||
res_9 = self.search(cr, uid, [('parent_id', 'in', [False])])
|
||||
res_9.sort()
|
||||
assert res_5 == []
|
||||
assert res_6 == []
|
||||
assert res_7 == without_parent
|
||||
assert res_8 == []
|
||||
assert res_9 == without_parent
|
||||
# These queries must return exactly the results than the queries 0..4,
|
||||
# i.e. not ... in ... must be the same as ... not in ... .
|
||||
res_10 = self.search(cr, uid, ['!', ('parent_id', 'like', 'probably_unexisting_name')])
|
||||
res_10.sort()
|
||||
res_11 = self.search(cr, uid, ['!', ('parent_id', 'in', [max_partner_id + 1])])
|
||||
res_11.sort()
|
||||
res_12 = self.search(cr, uid, ['!', ('parent_id', 'in', False)])
|
||||
res_12.sort()
|
||||
res_13 = self.search(cr, uid, ['!', ('parent_id', 'in', [])])
|
||||
res_13.sort()
|
||||
res_14 = self.search(cr, uid, ['!', ('parent_id', 'in', [False])])
|
||||
res_14.sort()
|
||||
assert res_0 == res_10
|
||||
assert res_1 == res_11
|
||||
assert res_2 == res_12
|
||||
assert res_3 == res_13
|
||||
assert res_4 == res_14
|
||||
|
||||
# Testing many2one field is not enough, a regular char field is tested
|
||||
# with in [] and must not return any result.
|
||||
res_15 = self.search(cr, uid, [('website', 'in', [])])
|
||||
assert res_15 == []
|
||||
# not in [] must return everything.
|
||||
res_16 = self.search(cr, uid, [('website', 'not in', [])])
|
||||
res_16.sort()
|
||||
assert res_16 == partner_ids
|
||||
|
||||
res_17 = self.search(cr, uid, [('website', 'not in', False)])
|
||||
res_17.sort()
|
||||
assert res_17 == with_website
|
||||
-
|
||||
Property of the query (one2many not in False).
|
||||
-
|
||||
!python {model: res.currency }: |
|
||||
ids = self.search(cr, uid, [])
|
||||
referenced_companies = set([x.company_id.id for x in self.browse(cr, uid, ids)])
|
||||
companies = set(self.pool.get('res.company').search(cr, uid, [('currency_ids', 'not in', False)]))
|
||||
assert referenced_companies == companies
|
||||
-
|
||||
Property of the query (one2many in False).
|
||||
-
|
||||
!python {model: res.currency }: |
|
||||
ids = self.search(cr, uid, [])
|
||||
referenced_companies = set([x.company_id.id for x in self.browse(cr, uid, ids)])
|
||||
unreferenced_companies = set(self.pool.get('res.company').search(cr, uid, [])).difference(referenced_companies)
|
||||
companies = set(self.pool.get('res.company').search(cr, uid, [('currency_ids', 'in', False)]))
|
||||
assert unreferenced_companies == companies
|
||||
-
|
||||
Equivalent queries.
|
||||
-
|
||||
!python {model: res.currency }: |
|
||||
max_currency_id = max(self.search(cr, uid, []))
|
||||
res_0 = self.search(cr, uid, [])
|
||||
res_1 = self.search(cr, uid, [('name', 'not like', 'probably_unexisting_name')])
|
||||
res_2 = self.search(cr, uid, [('id', 'not in', [max_currency_id + 1003])])
|
||||
res_3 = self.search(cr, uid, [('id', 'not in', [])])
|
||||
res_4 = self.search(cr, uid, [('id', 'not in', False)])
|
||||
res_0.sort()
|
||||
res_1.sort()
|
||||
res_2.sort()
|
||||
res_3.sort()
|
||||
res_4.sort()
|
||||
assert res_0 == res_1
|
||||
assert res_0 == res_2
|
||||
assert res_0 == res_3
|
||||
assert res_0 == res_4
|
||||
-
|
||||
Equivalent queries, integer and string.
|
||||
-
|
||||
!python {model: res.partner }: |
|
||||
all_ids = self.search(cr, uid, [])
|
||||
if len(all_ids) > 1:
|
||||
one = all_ids[0]
|
||||
record = self.browse(cr, uid, one)
|
||||
others = all_ids[1:]
|
||||
res_1 = self.search(cr, uid, [('id', '=', one)])
|
||||
# self.search(cr, uid, [('id', '!=', others)]) # not permitted
|
||||
res_2 = self.search(cr, uid, [('id', 'not in', others)])
|
||||
res_3 = self.search(cr, uid, ['!', ('id', '!=', one)])
|
||||
res_4 = self.search(cr, uid, ['!', ('id', 'in', others)])
|
||||
# res_5 = self.search(cr, uid, [('id', 'in', one)]) # TODO make it permitted, just like for child_of
|
||||
res_6 = self.search(cr, uid, [('id', 'in', [one])])
|
||||
res_7 = self.search(cr, uid, [('name', '=', record.name)])
|
||||
res_8 = self.search(cr, uid, [('name', 'in', [record.name])])
|
||||
# res_9 = self.search(cr, uid, [('name', 'in', record.name)]) # TODO
|
||||
assert [one] == res_1
|
||||
assert [one] == res_2
|
||||
assert [one] == res_3
|
||||
assert [one] == res_4
|
||||
#assert [one] == res_5
|
||||
assert [one] == res_6
|
||||
assert [one] == res_7
|
||||
-
|
||||
Need a company with a parent_id.
|
||||
-
|
||||
!record {model: res.company, id: ymltest_company3}:
|
||||
name: Acme 3
|
||||
-
|
||||
Need a company with a parent_id.
|
||||
-
|
||||
!record {model: res.company, id: ymltest_company4}:
|
||||
name: Acme 4
|
||||
parent_id: ymltest_company3
|
||||
-
|
||||
Equivalent queries, one2many.
|
||||
-
|
||||
!python {model: res.company }: |
|
||||
# Search the company via its one2many (the one2many must point back at the company).
|
||||
company = self.browse(cr, uid, ref('ymltest_company3'))
|
||||
max_currency_id = max(self.pool.get('res.currency').search(cr, uid, []))
|
||||
currency_ids1 = self.pool.get('res.currency').search(cr, uid, [('name', 'not like', 'probably_unexisting_name')])
|
||||
currency_ids2 = self.pool.get('res.currency').search(cr, uid, [('id', 'not in', [max_currency_id + 1003])])
|
||||
currency_ids3 = self.pool.get('res.currency').search(cr, uid, [('id', 'not in', [])])
|
||||
assert currency_ids1 == currency_ids2 == currency_ids3, 'All 3 results should have be the same: all currencies'
|
||||
default_company = self.browse(cr, uid, 1)
|
||||
# one2many towards same model
|
||||
res_1 = self.search(cr, uid, [('child_ids', 'in', [x.id for x in company.child_ids])]) # any company having a child of company3 as child
|
||||
res_2 = self.search(cr, uid, [('child_ids', 'in', [company.child_ids[0].id])]) # any company having the first child of company3 as child
|
||||
# one2many towards another model
|
||||
res_3 = self.search(cr, uid, [('currency_ids', 'in', [x.id for x in default_company.currency_ids])]) # companies having a currency of main company
|
||||
res_4 = self.search(cr, uid, [('currency_ids', 'in', [default_company.currency_ids[0].id])]) # companies having first currency of main company
|
||||
res_5 = self.search(cr, uid, [('currency_ids', 'in', default_company.currency_ids[0].id)]) # companies having first currency of main company
|
||||
# res_6 = self.search(cr, uid, [('currency_ids', 'in', [default_company.currency_ids[0].name])]) # TODO
|
||||
res_7 = self.search(cr, uid, [('currency_ids', '=', default_company.currency_ids[0].name)])
|
||||
res_8 = self.search(cr, uid, [('currency_ids', 'like', default_company.currency_ids[0].name)])
|
||||
res_9 = self.search(cr, uid, [('currency_ids', 'like', 'probably_unexisting_name')])
|
||||
# self.search(cr, uid, [('currency_ids', 'unexisting_op', 'probably_unexisting_name')]) # TODO expected exception
|
||||
assert res_1 == [ref('ymltest_company3')]
|
||||
assert res_2 == [ref('ymltest_company3')]
|
||||
assert res_3 == [1]
|
||||
assert res_4 == [1]
|
||||
assert res_5 == [1]
|
||||
assert res_7 == [1]
|
||||
assert res_8 == [1]
|
||||
assert res_9 == []
|
||||
|
||||
# get the companies referenced by some currency (this is normally the main company)
|
||||
res_10 = self.search(cr, uid, [('currency_ids', 'not like', 'probably_unexisting_name')])
|
||||
res_11 = self.search(cr, uid, [('currency_ids', 'not in', [max_currency_id + 1])])
|
||||
res_12 = self.search(cr, uid, [('currency_ids', 'not in', False)])
|
||||
res_13 = self.search(cr, uid, [('currency_ids', 'not in', [])])
|
||||
res_10.sort()
|
||||
res_11.sort()
|
||||
res_12.sort()
|
||||
res_13.sort()
|
||||
assert res_10 == res_11
|
||||
assert res_10 == res_12
|
||||
assert res_10 == res_13
|
||||
|
||||
# child_of x returns x and its children (direct or not).
|
||||
company = self.browse(cr, uid, ref('ymltest_company3'))
|
||||
expected = [ref('ymltest_company3'), ref('ymltest_company4')]
|
||||
expected.sort()
|
||||
res_1 = self.search(cr, uid, [('id', 'child_of', [ref('ymltest_company3')])])
|
||||
res_1.sort()
|
||||
res_2 = self.search(cr, uid, [('id', 'child_of', ref('ymltest_company3'))])
|
||||
res_2.sort()
|
||||
res_3 = self.search(cr, uid, [('id', 'child_of', [company.name])])
|
||||
res_3.sort()
|
||||
res_4 = self.search(cr, uid, [('id', 'child_of', company.name)])
|
||||
res_4.sort()
|
||||
assert res_1 == expected
|
||||
assert res_2 == expected
|
||||
assert res_3 == expected
|
||||
assert res_4 == expected
|
||||
-
|
||||
Verify that normalize_domain() works.
|
||||
-
|
||||
|
@ -187,6 +432,72 @@
|
|||
domain = [('x','in',['y','z']),('a.v','=','e'),'|','|',('a','=','b'),'!',('c','>','d'),('e','!=','f'),('g','=','h')]
|
||||
norm_domain = ['&','&','&'] + domain
|
||||
assert norm_domain == expression.normalize(domain), "Non-normalized domains should be properly normalized"
|
||||
-
|
||||
Unaccent. Create a company with an accent in its name.
|
||||
-
|
||||
!record {model: res.company, id: ymltest_unaccent_company}:
|
||||
name: Hélène
|
||||
-
|
||||
Test the unaccent-enabled 'ilike'.
|
||||
-
|
||||
!python {model: res.company}: |
|
||||
if self.pool.has_unaccent:
|
||||
ids = self.search(cr, uid, [('name','ilike','Helene')], {})
|
||||
assert ids == [ref('ymltest_unaccent_company')]
|
||||
ids = self.search(cr, uid, [('name','ilike','hélène')], {})
|
||||
assert ids == [ref('ymltest_unaccent_company')]
|
||||
ids = self.search(cr, uid, [('name','not ilike','Helene')], {})
|
||||
assert ref('ymltest_unaccent_company') not in ids
|
||||
ids = self.search(cr, uid, [('name','not ilike','hélène')], {})
|
||||
assert ref('ymltest_unaccent_company') not in ids
|
||||
-
|
||||
Check that =like/=ilike expressions (no wildcard variants of like/ilike) are working on an untranslated field.
|
||||
-
|
||||
!python {model: res.partner }: |
|
||||
all_ids = self.search(cr, uid, [('name', '=like', 'A_e_or')])
|
||||
assert len(all_ids) == 1, "Must match one partner (Axelor), got %r"%all_ids
|
||||
all_ids = self.search(cr, uid, [('name', '=ilike', 'm_____')])
|
||||
assert len(all_ids) == 1, "Must match *only* one partner (Maxtor), got %r"%all_ids
|
||||
-
|
||||
Check that =like/=ilike expressions (no wildcard variants of like/ilike) are working on translated field.
|
||||
-
|
||||
!python {model: res.country }: |
|
||||
all_ids = self.search(cr, uid, [('name', '=like', 'Ind__')])
|
||||
assert len(all_ids) == 1, "Must match India only, got %r"%all_ids
|
||||
all_ids = self.search(cr, uid, [('name', '=ilike', 'z%')])
|
||||
assert len(all_ids) == 3, "Must match only countries with names starting with Z (currently 3), got %r"%all_ids
|
||||
-
|
||||
Use the create_date column on res.country (which doesn't declare it in _columns).
|
||||
-
|
||||
!python {model: res.country }: |
|
||||
ids = self.search(cr, uid, [('create_date', '<', '2001-01-01 12:00:00')])
|
||||
|
||||
|
||||
-
|
||||
Verify that invalid expressions are refused, even for magic fields
|
||||
-
|
||||
!python {model: res.country }: |
|
||||
try:
|
||||
self.search(cr, uid, [('does_not_exist', '=', 'foo')])
|
||||
raise AssertionError('Invalid fields should not be accepted')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.search(cr, uid, [('create_date', '>>', 'foo')])
|
||||
raise AssertionError('Invalid operators should not be accepted')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
import psycopg2
|
||||
try:
|
||||
cr._default_log_exceptions = False
|
||||
cr.execute('SAVEPOINT expression_failure_test')
|
||||
self.search(cr, uid, [('create_date', '=', "1970-01-01'); --")])
|
||||
# if the above search gives no error, the operand was not escaped!
|
||||
cr.execute('RELEASE SAVEPOINT expression_failure_test')
|
||||
raise AssertionError('Operands should always be SQL escaped')
|
||||
except psycopg2.DataError:
|
||||
# Should give: 'DataError: invalid input syntax for type timestamp' or similar
|
||||
cr.execute('ROLLBACK TO SAVEPOINT expression_failure_test')
|
||||
|
||||
|
|
|
@ -28,8 +28,25 @@ parsing, configuration file loading and saving, ...) in this module
|
|||
and provide real Python variables, e.g. addons_paths is really a list
|
||||
of paths.
|
||||
|
||||
To initialize properly this module, openerp.tools.config.parse_config()
|
||||
must be used.
|
||||
|
||||
"""
|
||||
|
||||
import deprecation
|
||||
|
||||
# Maximum number of threads processing concurrently cron jobs.
|
||||
max_cron_threads = 4 # Actually the default value here is meaningless,
|
||||
# look at tools.config for the default value.
|
||||
|
||||
# Paths to search for OpenERP addons.
|
||||
addons_paths = []
|
||||
|
||||
# List of server-wide modules to load. Those modules are supposed to provide
|
||||
# features not necessarily tied to a particular database. This is in contrast
|
||||
# to modules that are always bound to a specific database when they are
|
||||
# installed (i.e. the majority of OpenERP addons). This is set with the --load
|
||||
# command-line option.
|
||||
server_wide_modules = []
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -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 logging
|
||||
|
||||
def is_initialized(cr):
|
||||
""" Check if a database has been initialized for the ORM.
|
||||
|
@ -40,6 +41,10 @@ def initialize(cr):
|
|||
|
||||
"""
|
||||
f = openerp.modules.get_module_resource('base', 'base.sql')
|
||||
if not f:
|
||||
m = "File not found: 'base.sql' (provided by module 'base')."
|
||||
logging.getLogger('init').critical(m)
|
||||
raise IOError(m)
|
||||
base_sql_file = openerp.tools.misc.file_open(f)
|
||||
try:
|
||||
cr.execute(base_sql_file.read())
|
||||
|
@ -118,4 +123,14 @@ def create_categories(cr, categories):
|
|||
categories = categories[1:]
|
||||
return p_id
|
||||
|
||||
def has_unaccent(cr):
|
||||
""" Test if the database has an unaccent function.
|
||||
|
||||
The unaccent is supposed to be provided by the PostgreSQL unaccent contrib
|
||||
module but any similar function will be picked by OpenERP.
|
||||
|
||||
"""
|
||||
cr.execute("SELECT proname FROM pg_proc WHERE proname='unaccent'")
|
||||
return len(cr.fetchall()) > 0
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -58,16 +58,16 @@ class Graph(dict):
|
|||
|
||||
"""
|
||||
|
||||
def add_node(self, name, deps):
|
||||
def add_node(self, name, info):
|
||||
max_depth, father = 0, None
|
||||
for n in [Node(x, self) for x in deps]:
|
||||
for n in [Node(x, self, None) for x in info['depends']]:
|
||||
if n.depth >= max_depth:
|
||||
father = n
|
||||
max_depth = n.depth
|
||||
if father:
|
||||
return father.add_child(name)
|
||||
return father.add_child(name, info)
|
||||
else:
|
||||
return Node(name, self)
|
||||
return Node(name, self, info)
|
||||
|
||||
def update_from_db(self, cr):
|
||||
if not len(self):
|
||||
|
@ -120,7 +120,7 @@ class Graph(dict):
|
|||
continue
|
||||
later.clear()
|
||||
current.remove(package)
|
||||
node = self.add_node(package, deps)
|
||||
node = self.add_node(package, info)
|
||||
node.data = info
|
||||
for kind in ('init', 'demo', 'update'):
|
||||
if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
|
||||
|
@ -154,12 +154,13 @@ class Graph(dict):
|
|||
|
||||
|
||||
class Singleton(object):
|
||||
def __new__(cls, name, graph):
|
||||
def __new__(cls, name, graph, info):
|
||||
if name in graph:
|
||||
inst = graph[name]
|
||||
else:
|
||||
inst = object.__new__(cls)
|
||||
inst.name = name
|
||||
inst.info = info
|
||||
graph[name] = inst
|
||||
return inst
|
||||
|
||||
|
@ -167,19 +168,21 @@ class Singleton(object):
|
|||
class Node(Singleton):
|
||||
""" One module in the modules dependency graph.
|
||||
|
||||
Node acts as a per-module singleton.
|
||||
Node acts as a per-module singleton. A node is constructed via
|
||||
Graph.add_module() or Graph.add_modules(). Some of its fields are from
|
||||
ir_module_module (setted by Graph.update_from_db()).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, graph):
|
||||
def __init__(self, name, graph, info):
|
||||
self.graph = graph
|
||||
if not hasattr(self, 'children'):
|
||||
self.children = []
|
||||
if not hasattr(self, 'depth'):
|
||||
self.depth = 0
|
||||
|
||||
def add_child(self, name):
|
||||
node = Node(name, self.graph)
|
||||
def add_child(self, name, info):
|
||||
node = Node(name, self.graph, info)
|
||||
node.depth = self.depth + 1
|
||||
if node not in self.children:
|
||||
self.children.append(node)
|
||||
|
|
|
@ -158,7 +158,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
|
|||
logger.info('module %s: loading objects', package.name)
|
||||
migrations.migrate_module(package, 'pre')
|
||||
register_module_classes(package.name)
|
||||
models = pool.instanciate(package.name, cr)
|
||||
models = pool.load(cr, package)
|
||||
loaded_modules.append(package.name)
|
||||
if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
|
||||
init_module_models(cr, package.name, models)
|
||||
|
@ -273,8 +273,6 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
|
|||
# This is a brand new pool, just created in pooler.get_db_and_pool()
|
||||
pool = pooler.get_pool(cr.dbname)
|
||||
|
||||
processed_modules = [] # for cleanup step after install
|
||||
loaded_modules = [] # to avoid double loading
|
||||
report = tools.assertion_report()
|
||||
if 'base' in tools.config['update'] or 'all' in tools.config['update']:
|
||||
cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
|
||||
|
@ -285,8 +283,10 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
|
|||
if not graph:
|
||||
logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
|
||||
raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
|
||||
loaded, processed = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
|
||||
processed_modules.extend(processed)
|
||||
|
||||
# processed_modules: for cleanup step after install
|
||||
# loaded_modules: to avoid double loading
|
||||
loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
|
||||
|
||||
if tools.config['load_language']:
|
||||
for lang in tools.config['load_language'].split(','):
|
||||
|
@ -339,16 +339,16 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
|
|||
cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
|
||||
for (model, name) in cr.fetchall():
|
||||
model_obj = pool.get(model)
|
||||
if model_obj and not isinstance(model_obj, osv.osv.osv_memory):
|
||||
logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
|
||||
if model_obj and not model_obj.is_transient():
|
||||
logger.notifyChannel('init', netsvc.LOG_WARNING, 'Model %s (%s) has no access rules!' % (model, name))
|
||||
|
||||
# Temporary warning while we remove access rights on osv_memory objects, as they have
|
||||
# been replaced by owner-only access rights
|
||||
cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
|
||||
for (model, name) in cr.fetchall():
|
||||
model_obj = pool.get(model)
|
||||
if isinstance(model_obj, osv.osv.osv_memory) and not isinstance(model_obj, osv.osv.osv):
|
||||
logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
|
||||
if model_obj and model_obj.is_transient():
|
||||
logger.notifyChannel('init', netsvc.LOG_WARNING, 'The transient model %s (%s) should not have explicit access rules!' % (model, name))
|
||||
|
||||
cr.execute("SELECT model from ir_model")
|
||||
for (model,) in cr.fetchall():
|
||||
|
@ -356,7 +356,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
|
|||
if obj:
|
||||
obj._check_removed_columns(cr, log=True)
|
||||
else:
|
||||
logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model)
|
||||
logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)" % model)
|
||||
|
||||
# Cleanup orphan records
|
||||
pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
|
||||
|
|
|
@ -31,7 +31,6 @@ import openerp.osv as osv
|
|||
import openerp.tools as tools
|
||||
import openerp.tools.osutil as osutil
|
||||
from openerp.tools.safe_eval import safe_eval as eval
|
||||
import openerp.pooler as pooler
|
||||
from openerp.tools.translate import _
|
||||
|
||||
import openerp.netsvc as netsvc
|
||||
|
@ -58,6 +57,11 @@ loaded = []
|
|||
logger = netsvc.Logger()
|
||||
|
||||
def initialize_sys_path():
|
||||
""" Add all addons paths in sys.path.
|
||||
|
||||
This ensures something like ``import crm`` works even if the addons are
|
||||
not in the PYTHONPATH.
|
||||
"""
|
||||
global ad_paths
|
||||
|
||||
if ad_paths:
|
||||
|
@ -250,6 +254,8 @@ def load_information_from_description_file(module):
|
|||
info['license'] = info.get('license') or 'AGPL-3'
|
||||
info.setdefault('installable', True)
|
||||
info.setdefault('active', False)
|
||||
# If the following is provided, it is called after the module is --loaded.
|
||||
info.setdefault('post_load', None)
|
||||
for kind in ['data', 'demo', 'test',
|
||||
'init_xml', 'update_xml', 'demo_xml']:
|
||||
info.setdefault(kind, [])
|
||||
|
@ -271,7 +277,6 @@ def init_module_models(cr, module_name, obj_list):
|
|||
TODO better explanation of _auto_init and init.
|
||||
|
||||
"""
|
||||
|
||||
logger.notifyChannel('init', netsvc.LOG_INFO,
|
||||
'module %s: creating or updating database tables' % module_name)
|
||||
todo = []
|
||||
|
@ -290,23 +295,22 @@ def init_module_models(cr, module_name, obj_list):
|
|||
t[1](cr, *t[2])
|
||||
cr.commit()
|
||||
|
||||
# Import hook to write a addon m in both sys.modules['m'] and
|
||||
# sys.modules['openerp.addons.m']. Otherwise it could be loaded twice
|
||||
# if imported twice using different names.
|
||||
#class MyImportHook(object):
|
||||
# def find_module(self, module_name, package_path):
|
||||
# print ">>>", module_name, package_path
|
||||
# def load_module(self, module_name):
|
||||
# raise ImportError("Restricted")
|
||||
|
||||
def load_module(module_name):
|
||||
""" Load a Python module found on the addons paths."""
|
||||
fm = imp.find_module(module_name, ad_paths)
|
||||
try:
|
||||
imp.load_module(module_name, *fm)
|
||||
finally:
|
||||
if fm[0]:
|
||||
fm[0].close()
|
||||
|
||||
#sys.meta_path.append(MyImportHook())
|
||||
|
||||
def register_module_classes(m):
|
||||
""" Register module named m, if not already registered.
|
||||
|
||||
This will load the module and register all of its models. (Actually, the
|
||||
explicit constructor call of each of the models inside the module will
|
||||
register them.)
|
||||
This loads the module and register all of its models, thanks to either
|
||||
the MetaModel metaclass, or the explicit instantiation of the model.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -326,7 +330,7 @@ def register_module_classes(m):
|
|||
try:
|
||||
zip_mod_path = mod_path + '.zip'
|
||||
if not os.path.isfile(zip_mod_path):
|
||||
load_module(m)
|
||||
__import__(m)
|
||||
else:
|
||||
zimp = zipimport.zipimporter(zip_mod_path)
|
||||
zimp.load_module(m)
|
||||
|
|
|
@ -22,10 +22,16 @@
|
|||
""" Models registries.
|
||||
|
||||
"""
|
||||
import threading
|
||||
|
||||
import logging
|
||||
|
||||
import openerp.sql_db
|
||||
import openerp.osv.orm
|
||||
|
||||
import openerp.cron
|
||||
import openerp.tools
|
||||
import openerp.modules.db
|
||||
import openerp.tools.config
|
||||
|
||||
class Registry(object):
|
||||
""" Model registry for a particular database.
|
||||
|
@ -44,6 +50,14 @@ class Registry(object):
|
|||
self.db_name = db_name
|
||||
self.db = openerp.sql_db.db_connect(db_name)
|
||||
|
||||
cr = self.db.cursor()
|
||||
has_unaccent = openerp.modules.db.has_unaccent(cr)
|
||||
if openerp.tools.config['unaccent'] and not has_unaccent:
|
||||
logger = logging.getLogger('unaccent')
|
||||
logger.warning("The option --unaccent was given but no unaccent() function was found in database.")
|
||||
self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent
|
||||
cr.close()
|
||||
|
||||
def do_parent_store(self, cr):
|
||||
for o in self._init_parent:
|
||||
self.get(o)._parent_store_compute(cr)
|
||||
|
@ -65,28 +79,42 @@ class Registry(object):
|
|||
""" Return a model for a given name or raise KeyError if it doesn't exist."""
|
||||
return self.models[model_name]
|
||||
|
||||
def instanciate(self, module, cr):
|
||||
""" Instanciate all the classes of a given module for a particular db."""
|
||||
def load(self, cr, module):
|
||||
""" Load a given module in the registry.
|
||||
|
||||
At the Python level, the modules are already loaded, but not yet on a
|
||||
per-registry level. This method populates a registry with the given
|
||||
modules, i.e. it instanciates all the classes of a the given module
|
||||
and registers them in the registry.
|
||||
|
||||
"""
|
||||
|
||||
res = []
|
||||
|
||||
# Instantiate registered classes (via metamodel discovery or via explicit
|
||||
# constructor call), and add them to the pool.
|
||||
for cls in openerp.osv.orm.MetaModel.module_to_models.get(module, []):
|
||||
res.append(cls.createInstance(self, cr))
|
||||
# Instantiate registered classes (via the MetaModel automatic discovery
|
||||
# or via explicit constructor call), and add them to the pool.
|
||||
for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
|
||||
res.append(cls.create_instance(self, cr))
|
||||
|
||||
return res
|
||||
|
||||
def schedule_cron_jobs(self):
|
||||
""" Make the cron thread care about this registry/database jobs.
|
||||
This will initiate the cron thread to check for any pending jobs for
|
||||
this registry/database as soon as possible. Then it will continuously
|
||||
monitor the ir.cron model for future jobs. See openerp.cron for
|
||||
details.
|
||||
"""
|
||||
openerp.cron.schedule_wakeup(openerp.cron.WAKE_UP_NOW, self.db.dbname)
|
||||
|
||||
def clear_caches(self):
|
||||
""" Clear the caches
|
||||
|
||||
This clears the caches associated to methods decorated with
|
||||
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
|
||||
"""
|
||||
for model in self.models.itervalues():
|
||||
model.clear_caches()
|
||||
|
||||
|
||||
class RegistryManager(object):
|
||||
""" Model registries manager.
|
||||
|
||||
|
@ -94,24 +122,22 @@ class RegistryManager(object):
|
|||
registries (essentially database connection/model registry pairs).
|
||||
|
||||
"""
|
||||
|
||||
# Mapping between db name and model registry.
|
||||
# Accessed through the methods below.
|
||||
registries = {}
|
||||
|
||||
registries_lock = threading.RLock()
|
||||
|
||||
@classmethod
|
||||
def get(cls, db_name, force_demo=False, status=None, update_module=False,
|
||||
pooljobs=True):
|
||||
""" Return a registry for a given database name."""
|
||||
|
||||
if db_name in cls.registries:
|
||||
registry = cls.registries[db_name]
|
||||
else:
|
||||
registry = cls.new(db_name, force_demo, status,
|
||||
update_module, pooljobs)
|
||||
return registry
|
||||
|
||||
with cls.registries_lock:
|
||||
if db_name in cls.registries:
|
||||
registry = cls.registries[db_name]
|
||||
else:
|
||||
registry = cls.new(db_name, force_demo, status,
|
||||
update_module, pooljobs)
|
||||
return registry
|
||||
|
||||
@classmethod
|
||||
def new(cls, db_name, force_demo=False, status=None,
|
||||
|
@ -121,47 +147,64 @@ class RegistryManager(object):
|
|||
The (possibly) previous registry for that database name is discarded.
|
||||
|
||||
"""
|
||||
|
||||
import openerp.modules
|
||||
registry = Registry(db_name)
|
||||
with cls.registries_lock:
|
||||
registry = Registry(db_name)
|
||||
|
||||
# Initializing a registry will call general code which will in turn
|
||||
# call registries.get (this object) to obtain the registry being
|
||||
# initialized. Make it available in the registries dictionary then
|
||||
# remove it if an exception is raised.
|
||||
cls.delete(db_name)
|
||||
cls.registries[db_name] = registry
|
||||
try:
|
||||
# This should be a method on Registry
|
||||
openerp.modules.load_modules(registry.db, force_demo, status, update_module)
|
||||
except Exception:
|
||||
del cls.registries[db_name]
|
||||
raise
|
||||
# Initializing a registry will call general code which will in turn
|
||||
# call registries.get (this object) to obtain the registry being
|
||||
# initialized. Make it available in the registries dictionary then
|
||||
# remove it if an exception is raised.
|
||||
cls.delete(db_name)
|
||||
cls.registries[db_name] = registry
|
||||
try:
|
||||
# This should be a method on Registry
|
||||
openerp.modules.load_modules(registry.db, force_demo, status, update_module)
|
||||
except Exception:
|
||||
del cls.registries[db_name]
|
||||
raise
|
||||
|
||||
cr = registry.db.cursor()
|
||||
try:
|
||||
registry.do_parent_store(cr)
|
||||
registry.get('ir.actions.report.xml').register_all(cr)
|
||||
cr.commit()
|
||||
finally:
|
||||
cr.close()
|
||||
cr = registry.db.cursor()
|
||||
try:
|
||||
registry.do_parent_store(cr)
|
||||
registry.get('ir.actions.report.xml').register_all(cr)
|
||||
cr.commit()
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
if pooljobs:
|
||||
registry.get('ir.cron').restart(registry.db.dbname)
|
||||
|
||||
return registry
|
||||
if pooljobs:
|
||||
registry.schedule_cron_jobs()
|
||||
|
||||
return registry
|
||||
|
||||
@classmethod
|
||||
def delete(cls, db_name):
|
||||
""" Delete the registry linked to a given database. """
|
||||
if db_name in cls.registries:
|
||||
del cls.registries[db_name]
|
||||
"""Delete the registry linked to a given database.
|
||||
|
||||
This also cleans the associated caches. For good measure this also
|
||||
cancels the associated cron job. But please note that the cron job can
|
||||
be running and take some time before ending, and that you should not
|
||||
remove a registry if it can still be used by some thread. So it might
|
||||
be necessary to call yourself openerp.cron.Agent.cancel(db_name) and
|
||||
and join (i.e. wait for) the thread.
|
||||
"""
|
||||
with cls.registries_lock:
|
||||
if db_name in cls.registries:
|
||||
cls.registries[db_name].clear_caches()
|
||||
del cls.registries[db_name]
|
||||
openerp.cron.cancel(db_name)
|
||||
|
||||
|
||||
@classmethod
|
||||
def delete_all(cls):
|
||||
"""Delete all the registries. """
|
||||
with cls.registries_lock:
|
||||
for db_name in cls.registries.keys():
|
||||
cls.delete(db_name)
|
||||
|
||||
@classmethod
|
||||
def clear_caches(cls, db_name):
|
||||
""" Clear the caches
|
||||
"""Clear caches
|
||||
|
||||
This clears the caches associated to methods decorated with
|
||||
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models
|
||||
|
@ -170,8 +213,9 @@ class RegistryManager(object):
|
|||
This method is given to spare you a ``RegistryManager.get(db_name)``
|
||||
that would loads the given database if it was not already loaded.
|
||||
"""
|
||||
if db_name in cls.registries:
|
||||
cls.registries[db_name].clear_caches()
|
||||
with cls.registries_lock:
|
||||
if db_name in cls.registries:
|
||||
cls.registries[db_name].clear_caches()
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -21,7 +21,6 @@
|
|||
##############################################################################
|
||||
|
||||
import errno
|
||||
import heapq
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
|
@ -31,12 +30,14 @@ import socket
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import types
|
||||
from pprint import pformat
|
||||
|
||||
# TODO modules that import netsvc only for things from loglevels must be changed to use loglevels.
|
||||
from loglevels import *
|
||||
import tools
|
||||
import openerp
|
||||
|
||||
def close_socket(sock):
|
||||
""" Closes a socket instance cleanly
|
||||
|
@ -60,20 +61,22 @@ def close_socket(sock):
|
|||
#.apidoc title: Common Services: netsvc
|
||||
#.apidoc module-mods: member-order: bysource
|
||||
|
||||
def abort_response(dummy_1, description, dummy_2, details):
|
||||
# TODO Replace except_{osv,orm} with these directly.
|
||||
if description == 'AccessError':
|
||||
raise openerp.exceptions.AccessError(details)
|
||||
else:
|
||||
raise openerp.exceptions.Warning(details)
|
||||
|
||||
class Service(object):
|
||||
""" Base class for *Local* services
|
||||
|
||||
Functionality here is trusted, no authentication.
|
||||
"""
|
||||
_services = {}
|
||||
def __init__(self, name, audience=''):
|
||||
def __init__(self, name):
|
||||
Service._services[name] = self
|
||||
self.__name = name
|
||||
self._methods = {}
|
||||
|
||||
def joinGroup(self, name):
|
||||
raise Exception("No group for local services")
|
||||
#GROUPS.setdefault(name, {})[self.__name] = self
|
||||
|
||||
@classmethod
|
||||
def exists(cls, name):
|
||||
|
@ -84,75 +87,39 @@ class Service(object):
|
|||
if cls.exists(name):
|
||||
cls._services.pop(name)
|
||||
|
||||
def exportMethod(self, method):
|
||||
if callable(method):
|
||||
self._methods[method.__name__] = method
|
||||
def LocalService(name):
|
||||
# Special case for addons support, will be removed in a few days when addons
|
||||
# are updated to directly use openerp.osv.osv.service.
|
||||
if name == 'object_proxy':
|
||||
return openerp.osv.osv.service
|
||||
|
||||
def abortResponse(self, error, description, origin, details):
|
||||
if not tools.config['debug_mode']:
|
||||
raise Exception("%s -- %s\n\n%s"%(origin, description, details))
|
||||
else:
|
||||
raise
|
||||
|
||||
class LocalService(object):
|
||||
""" Proxy for local services.
|
||||
|
||||
Any instance of this class will behave like the single instance
|
||||
of Service(name)
|
||||
"""
|
||||
__logger = logging.getLogger('service')
|
||||
def __init__(self, name):
|
||||
self.__name = name
|
||||
try:
|
||||
self._service = Service._services[name]
|
||||
for method_name, method_definition in self._service._methods.items():
|
||||
setattr(self, method_name, method_definition)
|
||||
except KeyError, keyError:
|
||||
self.__logger.error('This service does not exist: %s' % (str(keyError),) )
|
||||
raise
|
||||
|
||||
def __call__(self, method, *params):
|
||||
return getattr(self, method)(*params)
|
||||
return Service._services[name]
|
||||
|
||||
class ExportService(object):
|
||||
""" Proxy for exported services.
|
||||
|
||||
All methods here should take an AuthProxy as their first parameter. It
|
||||
will be appended by the calling framework.
|
||||
|
||||
Note that this class has no direct proxy, capable of calling
|
||||
eservice.method(). Rather, the proxy should call
|
||||
dispatch(method,auth,params)
|
||||
dispatch(method, params)
|
||||
"""
|
||||
|
||||
_services = {}
|
||||
_groups = {}
|
||||
_logger = logging.getLogger('web-services')
|
||||
|
||||
def __init__(self, name, audience=''):
|
||||
|
||||
def __init__(self, name):
|
||||
ExportService._services[name] = self
|
||||
self.__name = name
|
||||
self._logger.debug("Registered an exported service: %s" % name)
|
||||
|
||||
def joinGroup(self, name):
|
||||
ExportService._groups.setdefault(name, {})[self.__name] = self
|
||||
|
||||
@classmethod
|
||||
def getService(cls,name):
|
||||
return cls._services[name]
|
||||
|
||||
def dispatch(self, method, auth, params):
|
||||
# Dispatch a RPC call w.r.t. the method name. The dispatching
|
||||
# w.r.t. the service (this class) is done by OpenERPDispatcher.
|
||||
def dispatch(self, method, params):
|
||||
raise Exception("stub dispatch at %s" % self.__name)
|
||||
|
||||
def new_dispatch(self,method,auth,params):
|
||||
raise Exception("stub dispatch at %s" % self.__name)
|
||||
|
||||
def abortResponse(self, error, description, origin, details):
|
||||
if not tools.config['debug_mode']:
|
||||
raise Exception("%s -- %s\n\n%s"%(origin, description, details))
|
||||
else:
|
||||
raise
|
||||
|
||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
|
||||
#The background is set with 40 plus the number of the color, and the foreground with 30
|
||||
#These are the sequences need to get colored ouput
|
||||
|
@ -244,83 +211,6 @@ def init_alternative_logger():
|
|||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
class Agent(object):
|
||||
""" Singleton that keeps track of cancellable tasks to run at a given
|
||||
timestamp.
|
||||
|
||||
The tasks are characterised by:
|
||||
|
||||
* a timestamp
|
||||
* the database on which the task run
|
||||
* the function to call
|
||||
* the arguments and keyword arguments to pass to the function
|
||||
|
||||
Implementation details:
|
||||
|
||||
- Tasks are stored as list, allowing the cancellation by setting
|
||||
the timestamp to 0.
|
||||
- A heapq is used to store tasks, so we don't need to sort
|
||||
tasks ourself.
|
||||
"""
|
||||
__tasks = []
|
||||
__tasks_by_db = {}
|
||||
_logger = logging.getLogger('netsvc.agent')
|
||||
|
||||
@classmethod
|
||||
def setAlarm(cls, function, timestamp, db_name, *args, **kwargs):
|
||||
task = [timestamp, db_name, function, args, kwargs]
|
||||
heapq.heappush(cls.__tasks, task)
|
||||
cls.__tasks_by_db.setdefault(db_name, []).append(task)
|
||||
|
||||
@classmethod
|
||||
def cancel(cls, db_name):
|
||||
"""Cancel all tasks for a given database. If None is passed, all tasks are cancelled"""
|
||||
cls._logger.debug("Cancel timers for %s db", db_name or 'all')
|
||||
if db_name is None:
|
||||
cls.__tasks, cls.__tasks_by_db = [], {}
|
||||
else:
|
||||
if db_name in cls.__tasks_by_db:
|
||||
for task in cls.__tasks_by_db[db_name]:
|
||||
task[0] = 0
|
||||
|
||||
@classmethod
|
||||
def quit(cls):
|
||||
cls.cancel(None)
|
||||
|
||||
@classmethod
|
||||
def runner(cls):
|
||||
"""Neverending function (intended to be ran in a dedicated thread) that
|
||||
checks every 60 seconds tasks to run. TODO: make configurable
|
||||
"""
|
||||
current_thread = threading.currentThread()
|
||||
while True:
|
||||
while cls.__tasks and cls.__tasks[0][0] < time.time():
|
||||
task = heapq.heappop(cls.__tasks)
|
||||
timestamp, dbname, function, args, kwargs = task
|
||||
cls.__tasks_by_db[dbname].remove(task)
|
||||
if not timestamp:
|
||||
# null timestamp -> cancelled task
|
||||
continue
|
||||
current_thread.dbname = dbname # hack hack
|
||||
cls._logger.debug("Run %s.%s(*%s, **%s)", function.im_class.__name__, function.func_name, args, kwargs)
|
||||
delattr(current_thread, 'dbname')
|
||||
task_thread = threading.Thread(target=function, name='netsvc.Agent.task', args=args, kwargs=kwargs)
|
||||
# force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
|
||||
task_thread.setDaemon(False)
|
||||
task_thread.start()
|
||||
time.sleep(1)
|
||||
time.sleep(60)
|
||||
|
||||
def start_agent():
|
||||
agent_runner = threading.Thread(target=Agent.runner, name="netsvc.Agent.runner")
|
||||
# the agent runner is a typical daemon thread, that will never quit and must be
|
||||
# terminated when the main process exits - with no consequence (the processing
|
||||
# threads it spawns are not marked daemon)
|
||||
agent_runner.setDaemon(True)
|
||||
agent_runner.start()
|
||||
|
||||
import traceback
|
||||
|
||||
class Server:
|
||||
""" Generic interface for all servers with an event loop etc.
|
||||
Override this to impement http, net-rpc etc. servers.
|
||||
|
@ -403,11 +293,6 @@ class Server:
|
|||
def _close_socket(self):
|
||||
close_socket(self.socket)
|
||||
|
||||
class OpenERPDispatcherException(Exception):
|
||||
def __init__(self, exception, traceback):
|
||||
self.exception = exception
|
||||
self.traceback = traceback
|
||||
|
||||
def replace_request_password(args):
|
||||
# password is always 3rd argument in a request, we replace it in RPC logs
|
||||
# so it's easier to forward logs for diagnostics/debugging purposes...
|
||||
|
@ -425,33 +310,47 @@ def log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
|
|||
logger.log(channel, indent+line)
|
||||
indent=indent_after
|
||||
|
||||
class OpenERPDispatcher:
|
||||
def log(self, title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
|
||||
def dispatch_rpc(service_name, method, params):
|
||||
""" Handle a RPC call.
|
||||
|
||||
This is pure Python code, the actual marshalling (from/to XML-RPC or
|
||||
NET-RPC) is done in a upper layer.
|
||||
"""
|
||||
def _log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
|
||||
log(title, msg, channel=channel, depth=depth, fn=fn)
|
||||
def dispatch(self, service_name, method, params):
|
||||
try:
|
||||
auth = getattr(self, 'auth_provider', None)
|
||||
logger = logging.getLogger('result')
|
||||
start_time = end_time = 0
|
||||
if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
||||
self.log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method))
|
||||
if logger.isEnabledFor(logging.DEBUG_RPC):
|
||||
start_time = time.time()
|
||||
result = ExportService.getService(service_name).dispatch(method, auth, params)
|
||||
if logger.isEnabledFor(logging.DEBUG_RPC):
|
||||
end_time = time.time()
|
||||
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
||||
self.log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method))
|
||||
self.log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER)
|
||||
self.log('result', result, channel=logging.DEBUG_RPC_ANSWER)
|
||||
return result
|
||||
except Exception, e:
|
||||
self.log('exception', tools.exception_to_unicode(e))
|
||||
tb = getattr(e, 'traceback', sys.exc_info())
|
||||
tb_s = "".join(traceback.format_exception(*tb))
|
||||
if tools.config['debug_mode'] and isinstance(tb[2], types.TracebackType):
|
||||
import pdb
|
||||
pdb.post_mortem(tb[2])
|
||||
raise OpenERPDispatcherException(e, tb_s)
|
||||
try:
|
||||
logger = logging.getLogger('result')
|
||||
start_time = end_time = 0
|
||||
if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
||||
_log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method))
|
||||
if logger.isEnabledFor(logging.DEBUG_RPC):
|
||||
start_time = time.time()
|
||||
result = ExportService.getService(service_name).dispatch(method, params)
|
||||
if logger.isEnabledFor(logging.DEBUG_RPC):
|
||||
end_time = time.time()
|
||||
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
||||
_log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method))
|
||||
_log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER)
|
||||
_log('result', result, channel=logging.DEBUG_RPC_ANSWER)
|
||||
return result
|
||||
except openerp.exceptions.AccessError:
|
||||
raise
|
||||
except openerp.exceptions.AccessDenied:
|
||||
raise
|
||||
except openerp.exceptions.Warning:
|
||||
raise
|
||||
except openerp.exceptions.DeferredException, e:
|
||||
_log('exception', tools.exception_to_unicode(e))
|
||||
post_mortem(e.traceback)
|
||||
raise
|
||||
except Exception, e:
|
||||
_log('exception', tools.exception_to_unicode(e))
|
||||
post_mortem(sys.exc_info())
|
||||
raise
|
||||
|
||||
def post_mortem(info):
|
||||
if tools.config['debug_mode'] and isinstance(info[2], types.TracebackType):
|
||||
import pdb
|
||||
pdb.post_mortem(info[2])
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -20,17 +20,146 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
""" Domain expression processing
|
||||
|
||||
The main duty of this module is to compile a domain expression into a SQL
|
||||
query. A lot of things should be documented here, but as a first step in the
|
||||
right direction, some tests in test_osv_expression.yml might give you some
|
||||
additional information.
|
||||
|
||||
For legacy reasons, a domain uses an inconsistent two-levels abstract syntax
|
||||
(domains are regular Python data structures). At the first level, a domain
|
||||
is an expression made of terms (sometimes called leaves) and (domain) operators
|
||||
used in prefix notation. The available operators at this level are '!', '&',
|
||||
and '|'. '!' is a unary 'not', '&' is a binary 'and', and '|' is a binary 'or'.
|
||||
For instance, here is a possible domain. (<term> stands for an arbitrary term,
|
||||
more on this later.)
|
||||
|
||||
['&', '!', <term1>, '|', <term2>, <term3>]
|
||||
|
||||
It is equivalent to this pseudo code using infix notation:
|
||||
|
||||
(not <term1>) and (<term2> or <term3>)
|
||||
|
||||
The second level of syntax deals with the term representation. A term is
|
||||
a triple of the form (left, operator, right). That is, a term uses an infix
|
||||
notation, and the available operators, and possible left and right operands
|
||||
differ with those of the previous level. Here is a possible term:
|
||||
|
||||
('company_id.name', '=', 'OpenERP')
|
||||
|
||||
The left and right operand don't have the same possible values. The left
|
||||
operand is field name (related to the model for which the domain applies).
|
||||
Actually, the field name can use the dot-notation to traverse relationships.
|
||||
The right operand is a Python value whose type should match the used operator
|
||||
and field type. In the above example, a string is used because the name field
|
||||
of a company has type string, and because we use the '=' operator. When
|
||||
appropriate, a 'in' operator can be used, and thus the right operand should be
|
||||
a list.
|
||||
|
||||
Note: the non-uniform syntax could have been more uniform, but this would hide
|
||||
an important limitation of the domain syntax. Say that the term representation
|
||||
was ['=', 'company_id.name', 'OpenERP']. Used in a complete domain, this would
|
||||
look like:
|
||||
|
||||
['!', ['=', 'company_id.name', 'OpenERP']]
|
||||
|
||||
and you would be tempted to believe something like this would be possible:
|
||||
|
||||
['!', ['=', 'company_id.name', ['&', ..., ...]]]
|
||||
|
||||
That is, a domain could be a valid operand. But this is not the case. A domain
|
||||
is really limited to a two-level nature, and can not takes a recursive form: a
|
||||
domain is not a valid second-level operand.
|
||||
|
||||
Unaccent - Accent-insensitive search
|
||||
|
||||
OpenERP will use the SQL function 'unaccent' when available for the 'ilike' and
|
||||
'not ilike' operators, and enabled in the configuration.
|
||||
Normally the 'unaccent' function is obtained from the PostgreSQL 'unaccent'
|
||||
contrib module[0].
|
||||
|
||||
|
||||
..todo: The following explanation should be moved in some external installation
|
||||
guide
|
||||
|
||||
The steps to install the module might differ on specific PostgreSQL versions.
|
||||
We give here some instruction for PostgreSQL 9.x on a Ubuntu system.
|
||||
|
||||
Ubuntu doesn't come yet with PostgreSQL 9.x, so an alternative package source
|
||||
is used. We use Martin Pitt's PPA available at ppa:pitti/postgresql[1]. See
|
||||
[2] for instructions. Basically:
|
||||
|
||||
> sudo add-apt-repository ppa:pitti/postgresql
|
||||
> sudo apt-get update
|
||||
|
||||
Once the package list is up-to-date, you have to install PostgreSQL 9.0 and
|
||||
its contrib modules.
|
||||
|
||||
> sudo apt-get install postgresql-9.0 postgresql-contrib-9.0
|
||||
|
||||
When you want to enable unaccent on some database:
|
||||
|
||||
> psql9 <database> -f /usr/share/postgresql/9.0/contrib/unaccent.sql
|
||||
|
||||
Here 'psql9' is an alias for the newly installed PostgreSQL 9.0 tool, together
|
||||
with the correct port if necessary (for instance if PostgreSQL 8.4 is running
|
||||
on 5432). (Other aliases can be used for createdb and dropdb.)
|
||||
|
||||
> alias psql9='/usr/lib/postgresql/9.0/bin/psql -p 5433'
|
||||
|
||||
You can check unaccent is working:
|
||||
|
||||
> psql9 <database> -c"select unaccent('hélène')"
|
||||
|
||||
Finally, to instruct OpenERP to really use the unaccent function, you have to
|
||||
start the server specifying the --unaccent flag.
|
||||
|
||||
[0] http://developer.postgresql.org/pgdocs/postgres/unaccent.html
|
||||
[1] https://launchpad.net/~pitti/+archive/postgresql
|
||||
[2] https://launchpad.net/+help/soyuz/ppa-sources-list.html
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from openerp.tools import flatten, reverse_enumerate
|
||||
import fields
|
||||
import openerp.modules
|
||||
from openerp.osv.orm import MAGIC_COLUMNS
|
||||
|
||||
#.apidoc title: Domain Expressions
|
||||
|
||||
# Domain operators.
|
||||
NOT_OPERATOR = '!'
|
||||
OR_OPERATOR = '|'
|
||||
AND_OPERATOR = '&'
|
||||
DOMAIN_OPERATORS = (NOT_OPERATOR, OR_OPERATOR, AND_OPERATOR)
|
||||
|
||||
TRUE_DOMAIN = [(1,'=',1)]
|
||||
FALSE_DOMAIN = [(0,'=',1)]
|
||||
# List of available term operators. It is also possible to use the '<>'
|
||||
# operator, which is strictly the same as '!='; the later should be prefered
|
||||
# for consistency. This list doesn't contain '<>' as it is simpified to '!='
|
||||
# by the normalize_operator() function (so later part of the code deals with
|
||||
# only one representation).
|
||||
# An internal (i.e. not available to the user) 'inselect' operator is also
|
||||
# used. In this case its right operand has the form (subselect, params).
|
||||
TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike',
|
||||
'like', 'not like', 'ilike', 'not ilike', 'in', 'not in',
|
||||
'child_of')
|
||||
|
||||
# A subset of the above operators, with a 'negative' semantic. When the
|
||||
# expressions 'in NEGATIVE_TERM_OPERATORS' or 'not in NEGATIVE_TERM_OPERATORS' are used in the code
|
||||
# below, this doesn't necessarily mean that any of those NEGATIVE_TERM_OPERATORS is
|
||||
# legal in the processed term.
|
||||
NEGATIVE_TERM_OPERATORS = ('!=', 'not like', 'not ilike', 'not in')
|
||||
|
||||
TRUE_LEAF = (1, '=', 1)
|
||||
FALSE_LEAF = (0, '=', 1)
|
||||
|
||||
TRUE_DOMAIN = [TRUE_LEAF]
|
||||
FALSE_DOMAIN = [FALSE_LEAF]
|
||||
|
||||
_logger = logging.getLogger('expression')
|
||||
|
||||
def normalize(domain):
|
||||
"""Returns a normalized version of ``domain_expr``, where all implicit '&' operators
|
||||
|
@ -45,10 +174,10 @@ def normalize(domain):
|
|||
op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
|
||||
for token in domain:
|
||||
if expected == 0: # more than expected, like in [A, B]
|
||||
result[0:0] = ['&'] # put an extra '&' in front
|
||||
result[0:0] = [AND_OPERATOR] # put an extra '&' in front
|
||||
expected = 1
|
||||
result.append(token)
|
||||
if isinstance(token, (list,tuple)): # domain term
|
||||
if isinstance(token, (list, tuple)): # domain term
|
||||
expected -= 1
|
||||
else:
|
||||
expected += op_arity.get(token, 0) - 1
|
||||
|
@ -57,7 +186,8 @@ def normalize(domain):
|
|||
|
||||
def combine(operator, unit, zero, domains):
|
||||
"""Returns a new domain expression where all domain components from ``domains``
|
||||
have been added together using the binary operator ``operator``.
|
||||
have been added together using the binary operator ``operator``. The given
|
||||
domains must be normalized.
|
||||
|
||||
:param unit: the identity element of the domains "set" with regard to the operation
|
||||
performed by ``operator``, i.e the domain component ``i`` which, when
|
||||
|
@ -69,6 +199,7 @@ def combine(operator, unit, zero, domains):
|
|||
combined with any domain ``x`` via ``operator``, yields ``z``.
|
||||
E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as
|
||||
you see it in a domain component the resulting domain is the zero.
|
||||
:param domains: a list of normalized domains.
|
||||
"""
|
||||
result = []
|
||||
count = 0
|
||||
|
@ -84,13 +215,130 @@ def combine(operator, unit, zero, domains):
|
|||
return result
|
||||
|
||||
def AND(domains):
|
||||
""" AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
|
||||
"""AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
|
||||
return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
|
||||
|
||||
def OR(domains):
|
||||
""" OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
|
||||
"""OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
|
||||
return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains)
|
||||
|
||||
def is_operator(element):
|
||||
"""Test whether an object is a valid domain operator. """
|
||||
return isinstance(element, basestring) and element in DOMAIN_OPERATORS
|
||||
|
||||
# TODO change the share wizard to use this function.
|
||||
def is_leaf(element, internal=False):
|
||||
""" Test whether an object is a valid domain term.
|
||||
|
||||
:param internal: allow or not the 'inselect' internal operator in the term.
|
||||
This normally should be always left to False.
|
||||
"""
|
||||
INTERNAL_OPS = TERM_OPERATORS + ('inselect',)
|
||||
return (isinstance(element, tuple) or isinstance(element, list)) \
|
||||
and len(element) == 3 \
|
||||
and (((not internal) and element[1] in TERM_OPERATORS + ('<>',)) \
|
||||
or (internal and element[1] in INTERNAL_OPS + ('<>',)))
|
||||
|
||||
def normalize_leaf(left, operator, right):
|
||||
""" Change a term's operator to some canonical form, simplifying later
|
||||
processing.
|
||||
"""
|
||||
original = operator
|
||||
operator = operator.lower()
|
||||
if operator == '<>':
|
||||
operator = '!='
|
||||
if isinstance(right, bool) and operator in ('in', 'not in'):
|
||||
_logger.warning("The domain term '%s' should use the '=' or '!=' operator." % ((left, original, right),))
|
||||
operator = '=' if operator == 'in' else '!='
|
||||
if isinstance(right, (list, tuple)) and operator in ('=', '!='):
|
||||
_logger.warning("The domain term '%s' should use the 'in' or 'not in' operator." % ((left, original, right),))
|
||||
operator = 'in' if operator == '=' else 'not in'
|
||||
return left, operator, right
|
||||
|
||||
def distribute_not(domain):
|
||||
""" Distribute any '!' domain operators found inside a normalized domain.
|
||||
|
||||
Because we don't use SQL semantic for processing a 'left not in right'
|
||||
query (i.e. our 'not in' is not simply translated to a SQL 'not in'),
|
||||
it means that a '! left in right' can not be simply processed
|
||||
by __leaf_to_sql by first emitting code for 'left in right' then wrapping
|
||||
the result with 'not (...)', as it would result in a 'not in' at the SQL
|
||||
level.
|
||||
|
||||
This function is thus responsible for pushing any '!' domain operators
|
||||
inside the terms themselves. For example::
|
||||
|
||||
['!','&',('user_id','=',4),('partner_id','in',[1,2])]
|
||||
will be turned into:
|
||||
['|',('user_id','!=',4),('partner_id','not in',[1,2])]
|
||||
|
||||
"""
|
||||
def negate(leaf):
|
||||
"""Negates and returns a single domain leaf term,
|
||||
using the opposite operator if possible"""
|
||||
left, operator, right = leaf
|
||||
mapping = {
|
||||
'<': '>=',
|
||||
'>': '<=',
|
||||
'<=': '>',
|
||||
'>=': '<',
|
||||
'=': '!=',
|
||||
'!=': '=',
|
||||
}
|
||||
if operator in ('in', 'like', 'ilike'):
|
||||
operator = 'not ' + operator
|
||||
return [(left, operator, right)]
|
||||
if operator in ('not in', 'not like', 'not ilike'):
|
||||
operator = operator[4:]
|
||||
return [(left, operator, right)]
|
||||
if operator in mapping:
|
||||
operator = mapping[operator]
|
||||
return [(left, operator, right)]
|
||||
return [NOT_OPERATOR, (left, operator, right)]
|
||||
def distribute_negate(domain):
|
||||
"""Negate the domain ``subtree`` rooted at domain[0],
|
||||
leaving the rest of the domain intact, and return
|
||||
(negated_subtree, untouched_domain_rest)
|
||||
"""
|
||||
if is_leaf(domain[0]):
|
||||
return negate(domain[0]), domain[1:]
|
||||
if domain[0] == AND_OPERATOR:
|
||||
done1, todo1 = distribute_negate(domain[1:])
|
||||
done2, todo2 = distribute_negate(todo1)
|
||||
return [OR_OPERATOR] + done1 + done2, todo2
|
||||
if domain[0] == OR_OPERATOR:
|
||||
done1, todo1 = distribute_negate(domain[1:])
|
||||
done2, todo2 = distribute_negate(todo1)
|
||||
return [AND_OPERATOR] + done1 + done2, todo2
|
||||
if not domain:
|
||||
return []
|
||||
if domain[0] != NOT_OPERATOR:
|
||||
return [domain[0]] + distribute_not(domain[1:])
|
||||
if domain[0] == NOT_OPERATOR:
|
||||
done, todo = distribute_negate(domain[1:])
|
||||
return done + distribute_not(todo)
|
||||
|
||||
def select_from_where(cr, select_field, from_table, where_field, where_ids, where_operator):
|
||||
# todo: merge into parent query as sub-query
|
||||
res = []
|
||||
if where_ids:
|
||||
if where_operator in ['<','>','>=','<=']:
|
||||
cr.execute('SELECT "%s" FROM "%s" WHERE "%s" %s %%s' % \
|
||||
(select_field, from_table, where_field, where_operator),
|
||||
(where_ids[0],)) # TODO shouldn't this be min/max(where_ids) ?
|
||||
res = [r[0] for r in cr.fetchall()]
|
||||
else: # TODO where_operator is supposed to be 'in'? It is called with child_of...
|
||||
for i in range(0, len(where_ids), cr.IN_MAX):
|
||||
subids = where_ids[i:i+cr.IN_MAX]
|
||||
cr.execute('SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % \
|
||||
(select_field, from_table, where_field), (tuple(subids),))
|
||||
res.extend([r[0] for r in cr.fetchall()])
|
||||
return res
|
||||
|
||||
def select_distinct_from_where_not_null(cr, select_field, from_table):
|
||||
cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % \
|
||||
(select_field, from_table, select_field))
|
||||
return [r[0] for r in cr.fetchall()]
|
||||
|
||||
class expression(object):
|
||||
"""
|
||||
|
@ -100,148 +348,124 @@ class expression(object):
|
|||
For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _is_operator(cls, element):
|
||||
return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR]
|
||||
|
||||
@classmethod
|
||||
def _is_leaf(cls, element, internal=False):
|
||||
OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
|
||||
INTERNAL_OPS = OPS + ('inselect',)
|
||||
return (isinstance(element, tuple) or isinstance(element, list)) \
|
||||
and len(element) == 3 \
|
||||
and (((not internal) and element[1] in OPS) \
|
||||
or (internal and element[1] in INTERNAL_OPS))
|
||||
|
||||
def __execute_recursive_in(self, cr, s, f, w, ids, op, type):
|
||||
# todo: merge into parent query as sub-query
|
||||
res = []
|
||||
if ids:
|
||||
if op in ['<','>','>=','<=']:
|
||||
cr.execute('SELECT "%s"' \
|
||||
' FROM "%s"' \
|
||||
' WHERE "%s" %s %%s' % (s, f, w, op), (ids[0],))
|
||||
res.extend([r[0] for r in cr.fetchall()])
|
||||
else:
|
||||
for i in range(0, len(ids), cr.IN_MAX):
|
||||
subids = ids[i:i+cr.IN_MAX]
|
||||
cr.execute('SELECT "%s"' \
|
||||
' FROM "%s"' \
|
||||
' WHERE "%s" IN %%s' % (s, f, w),(tuple(subids),))
|
||||
res.extend([r[0] for r in cr.fetchall()])
|
||||
else:
|
||||
cr.execute('SELECT distinct("%s")' \
|
||||
' FROM "%s" where "%s" is not null' % (s, f, s)),
|
||||
res.extend([r[0] for r in cr.fetchall()])
|
||||
return res
|
||||
|
||||
def __init__(self, exp):
|
||||
# check if the expression is valid
|
||||
if not reduce(lambda acc, val: acc and (self._is_operator(val) or self._is_leaf(val)), exp, True):
|
||||
raise ValueError('Bad domain expression: %r' % (exp,))
|
||||
self.__exp = exp
|
||||
def __init__(self, cr, uid, exp, table, context):
|
||||
self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent
|
||||
self.__field_tables = {} # used to store the table to use for the sql generation. key = index of the leaf
|
||||
self.__all_tables = set()
|
||||
self.__joins = []
|
||||
self.__main_table = None # 'root' table. set by parse()
|
||||
self.__DUMMY_LEAF = (1, '=', 1) # a dummy leaf that must not be parsed or sql generated
|
||||
# assign self.__exp with the normalized, parsed domain.
|
||||
self.parse(cr, uid, distribute_not(normalize(exp)), table, context)
|
||||
|
||||
# TODO used only for osv_memory
|
||||
@property
|
||||
def exp(self):
|
||||
return self.__exp[:]
|
||||
|
||||
def parse(self, cr, uid, table, context):
|
||||
""" transform the leafs of the expression """
|
||||
if not self.__exp:
|
||||
return self
|
||||
def parse(self, cr, uid, exp, table, context):
|
||||
""" transform the leaves of the expression """
|
||||
self.__exp = exp
|
||||
self.__main_table = table
|
||||
self.__all_tables.add(table)
|
||||
|
||||
def _rec_get(ids, table, parent=None, left='id', prefix=''):
|
||||
if table._parent_store and (not table.pool._init):
|
||||
# TODO: Improve where joins are implemented for many with '.', replace by:
|
||||
# doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
|
||||
def child_of_domain(left, ids, left_model, parent=None, prefix=''):
|
||||
"""Returns a domain implementing the child_of operator for [(left,child_of,ids)],
|
||||
either as a range using the parent_left/right tree lookup fields (when available),
|
||||
or as an expanded [(left,in,child_ids)]"""
|
||||
if left_model._parent_store and (not left_model.pool._init):
|
||||
# TODO: Improve where joins are implemented for many with '.', replace by:
|
||||
# doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
|
||||
doms = []
|
||||
for o in table.browse(cr, uid, ids, context=context):
|
||||
for o in left_model.browse(cr, uid, ids, context=context):
|
||||
if doms:
|
||||
doms.insert(0, OR_OPERATOR)
|
||||
doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
|
||||
if prefix:
|
||||
return [(left, 'in', table.search(cr, uid, doms, context=context))]
|
||||
return [(left, 'in', left_model.search(cr, uid, doms, context=context))]
|
||||
return doms
|
||||
else:
|
||||
def rg(ids, table, parent):
|
||||
def recursive_children(ids, model, parent_field):
|
||||
if not ids:
|
||||
return []
|
||||
ids2 = table.search(cr, uid, [(parent, 'in', ids)], context=context)
|
||||
return ids + rg(ids2, table, parent)
|
||||
return [(left, 'in', rg(ids, table, parent or table._parent_name))]
|
||||
ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context)
|
||||
return ids + recursive_children(ids2, model, parent_field)
|
||||
return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))]
|
||||
|
||||
def child_of_right_to_ids(value):
|
||||
""" Normalize a single id, or a string, or a list of ids to a list of ids.
|
||||
|
||||
This function is always used with _rec_get() above, so it should be
|
||||
called directly from _rec_get instead of repeatedly before _rec_get.
|
||||
|
||||
"""
|
||||
def to_ids(value, field_obj):
|
||||
"""Normalize a single id or name, or a list of those, into a list of ids"""
|
||||
names = []
|
||||
if isinstance(value, basestring):
|
||||
return [x[0] for x in field_obj.name_search(cr, uid, value, [], 'ilike', context=context, limit=None)]
|
||||
names = [value]
|
||||
if value and isinstance(value, (tuple, list)) and isinstance(value[0], basestring):
|
||||
names = value
|
||||
if names:
|
||||
return flatten([[x[0] for x in field_obj.name_search(cr, uid, n, [], 'ilike', context=context, limit=None)] \
|
||||
for n in names])
|
||||
elif isinstance(value, (int, long)):
|
||||
return [value]
|
||||
else:
|
||||
return list(value)
|
||||
|
||||
self.__main_table = table
|
||||
self.__all_tables.add(table)
|
||||
return list(value)
|
||||
|
||||
i = -1
|
||||
while i + 1<len(self.__exp):
|
||||
i += 1
|
||||
e = self.__exp[i]
|
||||
if self._is_operator(e) or e == self.__DUMMY_LEAF:
|
||||
if is_operator(e) or e == TRUE_LEAF or e == FALSE_LEAF:
|
||||
continue
|
||||
|
||||
# check if the expression is valid
|
||||
if not is_leaf(e):
|
||||
raise ValueError("Invalid term %r in domain expression %r" % (e, exp))
|
||||
|
||||
# normalize the leaf's operator
|
||||
e = normalize_leaf(*e)
|
||||
self.__exp[i] = e
|
||||
left, operator, right = e
|
||||
operator = operator.lower()
|
||||
working_table = table
|
||||
main_table = table
|
||||
fargs = left.split('.', 1)
|
||||
if fargs[0] in table._inherit_fields:
|
||||
|
||||
working_table = table # The table containing the field (the name provided in the left operand)
|
||||
field_path = left.split('.', 1)
|
||||
|
||||
# If the field is _inherits'd, search for the working_table,
|
||||
# and extract the field.
|
||||
if field_path[0] in table._inherit_fields:
|
||||
while True:
|
||||
field = main_table._columns.get(fargs[0], False)
|
||||
field = working_table._columns.get(field_path[0])
|
||||
if field:
|
||||
working_table = main_table
|
||||
self.__field_tables[i] = working_table
|
||||
break
|
||||
working_table = main_table.pool.get(main_table._inherit_fields[fargs[0]][0])
|
||||
if working_table not in self.__all_tables:
|
||||
self.__joins.append('%s.%s=%s.%s' % (working_table._table, 'id', main_table._table, main_table._inherits[working_table._name]))
|
||||
self.__all_tables.add(working_table)
|
||||
main_table = working_table
|
||||
next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0])
|
||||
if next_table not in self.__all_tables:
|
||||
self.__joins.append('%s."%s"=%s."%s"' % (next_table._table, 'id', working_table._table, working_table._inherits[next_table._name]))
|
||||
self.__all_tables.add(next_table)
|
||||
working_table = next_table
|
||||
# Or (try to) directly extract the field.
|
||||
else:
|
||||
field = working_table._columns.get(field_path[0])
|
||||
|
||||
field = working_table._columns.get(fargs[0], False)
|
||||
if not field:
|
||||
if left == 'id' and operator == 'child_of':
|
||||
ids2 = child_of_right_to_ids(right)
|
||||
dom = _rec_get(ids2, working_table)
|
||||
ids2 = to_ids(right, table)
|
||||
dom = child_of_domain(left, ids2, working_table)
|
||||
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
|
||||
else:
|
||||
# field could not be found in model columns, it's probably invalid, unless
|
||||
# it's one of the _log_access special fields
|
||||
# TODO: make these fields explicitly available in self.columns instead!
|
||||
if field_path[0] not in MAGIC_COLUMNS:
|
||||
raise ValueError("Invalid field %r in domain expression %r" % (left, exp))
|
||||
continue
|
||||
|
||||
field_obj = table.pool.get(field._obj)
|
||||
if len(fargs) > 1:
|
||||
if len(field_path) > 1:
|
||||
if field._type == 'many2one':
|
||||
right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
|
||||
if right == []:
|
||||
self.__exp[i] = ( 'id', '=', 0 )
|
||||
else:
|
||||
self.__exp[i] = (fargs[0], 'in', right)
|
||||
right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context)
|
||||
self.__exp[i] = (field_path[0], 'in', right)
|
||||
# Making search easier when there is a left operand as field.o2m or field.m2m
|
||||
if field._type in ['many2many','one2many']:
|
||||
right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
|
||||
right1 = table.search(cr, uid, [(fargs[0],'in', right)], context=context)
|
||||
if right1 == []:
|
||||
self.__exp[i] = ( 'id', '=', 0 )
|
||||
else:
|
||||
self.__exp[i] = ('id', 'in', right1)
|
||||
if field._type in ['many2many', 'one2many']:
|
||||
right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context)
|
||||
right1 = table.search(cr, uid, [(field_path[0], 'in', right)], context=context)
|
||||
self.__exp[i] = ('id', 'in', right1)
|
||||
|
||||
if not isinstance(field,fields.property):
|
||||
if not isinstance(field, fields.property):
|
||||
continue
|
||||
|
||||
if field._properties and not field.store:
|
||||
|
@ -249,16 +473,16 @@ class expression(object):
|
|||
if not field._fnct_search:
|
||||
# the function field doesn't provide a search function and doesn't store
|
||||
# values in the database, so we must ignore it : we generate a dummy leaf
|
||||
self.__exp[i] = self.__DUMMY_LEAF
|
||||
self.__exp[i] = TRUE_LEAF
|
||||
else:
|
||||
subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
|
||||
if not subexp:
|
||||
self.__exp[i] = self.__DUMMY_LEAF
|
||||
self.__exp[i] = TRUE_LEAF
|
||||
else:
|
||||
# we assume that the expression is valid
|
||||
# we create a dummy leaf for forcing the parsing of the resulting expression
|
||||
self.__exp[i] = AND_OPERATOR
|
||||
self.__exp.insert(i + 1, self.__DUMMY_LEAF)
|
||||
self.__exp.insert(i + 1, TRUE_LEAF)
|
||||
for j, se in enumerate(subexp):
|
||||
self.__exp.insert(i + 2 + j, se)
|
||||
# else, the value of the field is store in the database, so we search on it
|
||||
|
@ -266,11 +490,11 @@ class expression(object):
|
|||
elif field._type == 'one2many':
|
||||
# Applying recursivity on field(one2many)
|
||||
if operator == 'child_of':
|
||||
ids2 = child_of_right_to_ids(right)
|
||||
ids2 = to_ids(right, field_obj)
|
||||
if field._obj != working_table._name:
|
||||
dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj)
|
||||
dom = child_of_domain(left, ids2, field_obj, prefix=field._obj)
|
||||
else:
|
||||
dom = _rec_get(ids2, working_table, parent=left)
|
||||
dom = child_of_domain('id', ids2, working_table, parent=left)
|
||||
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
|
||||
|
||||
else:
|
||||
|
@ -282,7 +506,7 @@ class expression(object):
|
|||
if ids2:
|
||||
operator = 'in'
|
||||
else:
|
||||
if not isinstance(right,list):
|
||||
if not isinstance(right, list):
|
||||
ids2 = [right]
|
||||
else:
|
||||
ids2 = right
|
||||
|
@ -290,22 +514,16 @@ class expression(object):
|
|||
if operator in ['like','ilike','in','=']:
|
||||
#no result found with given search criteria
|
||||
call_null = False
|
||||
self.__exp[i] = ('id','=',0)
|
||||
else:
|
||||
call_null = True
|
||||
operator = 'in' # operator changed because ids are directly related to main object
|
||||
self.__exp[i] = FALSE_LEAF
|
||||
else:
|
||||
call_null = False
|
||||
o2m_op = 'in'
|
||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
||||
o2m_op = 'not in'
|
||||
self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', ids2, operator, field._type))
|
||||
ids2 = select_from_where(cr, field._fields_id, field_obj._table, 'id', ids2, operator)
|
||||
if ids2:
|
||||
call_null = False
|
||||
self.__exp[i] = ('id', 'in', ids2)
|
||||
|
||||
if call_null:
|
||||
o2m_op = 'not in'
|
||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
||||
o2m_op = 'in'
|
||||
self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', [], operator, field._type) or [0])
|
||||
o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
|
||||
self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, field_obj._table))
|
||||
|
||||
elif field._type == 'many2many':
|
||||
#FIXME
|
||||
|
@ -313,10 +531,10 @@ class expression(object):
|
|||
def _rec_convert(ids):
|
||||
if field_obj == table:
|
||||
return ids
|
||||
return self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, ids, operator, field._type)
|
||||
return select_from_where(cr, field._id1, field._rel, field._id2, ids, operator)
|
||||
|
||||
ids2 = child_of_right_to_ids(right)
|
||||
dom = _rec_get(ids2, field_obj)
|
||||
ids2 = to_ids(right, field_obj)
|
||||
dom = child_of_domain('id', ids2, field_obj)
|
||||
ids2 = field_obj.search(cr, uid, dom, context=context)
|
||||
self.__exp[i] = ('id', 'in', _rec_convert(ids2))
|
||||
else:
|
||||
|
@ -335,34 +553,28 @@ class expression(object):
|
|||
if operator in ['like','ilike','in','=']:
|
||||
#no result found with given search criteria
|
||||
call_null_m2m = False
|
||||
self.__exp[i] = ('id','=',0)
|
||||
self.__exp[i] = FALSE_LEAF
|
||||
else:
|
||||
call_null_m2m = True
|
||||
operator = 'in' # operator changed because ids are directly related to main object
|
||||
else:
|
||||
call_null_m2m = False
|
||||
m2m_op = 'in'
|
||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
||||
m2m_op = 'not in'
|
||||
m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
|
||||
self.__exp[i] = ('id', m2m_op, select_from_where(cr, field._id1, field._rel, field._id2, res_ids, operator) or [0])
|
||||
|
||||
self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, res_ids, operator, field._type) or [0])
|
||||
if call_null_m2m:
|
||||
m2m_op = 'not in'
|
||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
||||
m2m_op = 'in'
|
||||
self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, [], operator, field._type) or [0])
|
||||
m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
|
||||
self.__exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, field._id1, field._rel))
|
||||
|
||||
elif field._type == 'many2one':
|
||||
if operator == 'child_of':
|
||||
ids2 = child_of_right_to_ids(right)
|
||||
self.__operator = 'in'
|
||||
ids2 = to_ids(right, field_obj)
|
||||
if field._obj != working_table._name:
|
||||
dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj)
|
||||
dom = child_of_domain(left, ids2, field_obj, prefix=field._obj)
|
||||
else:
|
||||
dom = _rec_get(ids2, working_table, parent=left)
|
||||
dom = child_of_domain('id', ids2, working_table, parent=left)
|
||||
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
|
||||
else:
|
||||
def _get_expression(field_obj,cr, uid, left, right, operator, context=None):
|
||||
def _get_expression(field_obj, cr, uid, left, right, operator, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
c = context.copy()
|
||||
|
@ -370,46 +582,35 @@ class expression(object):
|
|||
#Special treatment to ill-formed domains
|
||||
operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
|
||||
|
||||
dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'}
|
||||
if isinstance(right,tuple):
|
||||
dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in'}
|
||||
if isinstance(right, tuple):
|
||||
right = list(right)
|
||||
if (not isinstance(right,list)) and operator in ['not in','in']:
|
||||
if (not isinstance(right, list)) and operator in ['not in','in']:
|
||||
operator = dict_op[operator]
|
||||
elif isinstance(right,list) and operator in ['<>','!=','=']: #for domain (FIELD,'=',['value1','value2'])
|
||||
elif isinstance(right, list) and operator in ['!=','=']: #for domain (FIELD,'=',['value1','value2'])
|
||||
operator = dict_op[operator]
|
||||
res_ids = field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)
|
||||
if not res_ids:
|
||||
return ('id','=',0)
|
||||
else:
|
||||
right = map(lambda x: x[0], res_ids)
|
||||
return (left, 'in', right)
|
||||
res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)]
|
||||
if operator in NEGATIVE_TERM_OPERATORS:
|
||||
res_ids.append(False) # TODO this should not be appended if False was in 'right'
|
||||
return (left, 'in', res_ids)
|
||||
|
||||
m2o_str = False
|
||||
if right:
|
||||
if isinstance(right, basestring): # and not isinstance(field, fields.related):
|
||||
m2o_str = True
|
||||
elif isinstance(right,(list,tuple)):
|
||||
elif isinstance(right, (list, tuple)):
|
||||
m2o_str = True
|
||||
for ele in right:
|
||||
if not isinstance(ele, basestring):
|
||||
m2o_str = False
|
||||
break
|
||||
if m2o_str:
|
||||
self.__exp[i] = _get_expression(field_obj, cr, uid, left, right, operator, context=context)
|
||||
elif right == []:
|
||||
m2o_str = False
|
||||
if operator in ('not in', '!=', '<>'):
|
||||
# (many2one not in []) should return all records
|
||||
self.__exp[i] = self.__DUMMY_LEAF
|
||||
else:
|
||||
self.__exp[i] = ('id','=',0)
|
||||
else:
|
||||
new_op = '='
|
||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
||||
new_op = '!='
|
||||
#Is it ok to put 'left' and not 'id' ?
|
||||
self.__exp[i] = (left,new_op,False)
|
||||
pass # Handled by __leaf_to_sql().
|
||||
else: # right is False
|
||||
pass # Handled by __leaf_to_sql().
|
||||
|
||||
if m2o_str:
|
||||
self.__exp[i] = _get_expression(field_obj,cr, uid, left, right, operator, context=context)
|
||||
else:
|
||||
# other field type
|
||||
# add the time part to datetime field when it's not there:
|
||||
|
@ -425,127 +626,160 @@ class expression(object):
|
|||
self.__exp[i] = tuple(self.__exp[i])
|
||||
|
||||
if field.translate:
|
||||
if operator in ('like', 'ilike', 'not like', 'not ilike'):
|
||||
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
|
||||
sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
|
||||
if need_wildcard:
|
||||
right = '%%%s%%' % right
|
||||
|
||||
operator = operator == '=like' and 'like' or operator
|
||||
|
||||
query1 = '( SELECT res_id' \
|
||||
subselect = '( SELECT res_id' \
|
||||
' FROM ir_translation' \
|
||||
' WHERE name = %s' \
|
||||
' AND lang = %s' \
|
||||
' AND type = %s'
|
||||
instr = ' %s'
|
||||
#Covering in,not in operators with operands (%s,%s) ,etc.
|
||||
if operator in ['in','not in']:
|
||||
if sql_operator in ['in','not in']:
|
||||
instr = ','.join(['%s'] * len(right))
|
||||
query1 += ' AND value ' + operator + ' ' +" (" + instr + ")" \
|
||||
subselect += ' AND value ' + sql_operator + ' ' +" (" + instr + ")" \
|
||||
') UNION (' \
|
||||
' SELECT id' \
|
||||
' FROM "' + working_table._table + '"' \
|
||||
' WHERE "' + left + '" ' + operator + ' ' +" (" + instr + "))"
|
||||
' WHERE "' + left + '" ' + sql_operator + ' ' +" (" + instr + "))"
|
||||
else:
|
||||
query1 += ' AND value ' + operator + instr + \
|
||||
subselect += ' AND value ' + sql_operator + instr + \
|
||||
') UNION (' \
|
||||
' SELECT id' \
|
||||
' FROM "' + working_table._table + '"' \
|
||||
' WHERE "' + left + '" ' + operator + instr + ")"
|
||||
' WHERE "' + left + '" ' + sql_operator + instr + ")"
|
||||
|
||||
query2 = [working_table._name + ',' + left,
|
||||
params = [working_table._name + ',' + left,
|
||||
context.get('lang', False) or 'en_US',
|
||||
'model',
|
||||
right,
|
||||
right,
|
||||
]
|
||||
|
||||
self.__exp[i] = ('id', 'inselect', (query1, query2))
|
||||
return self
|
||||
self.__exp[i] = ('id', 'inselect', (subselect, params))
|
||||
|
||||
def __leaf_to_sql(self, leaf, table):
|
||||
if leaf == self.__DUMMY_LEAF:
|
||||
return ('(1=1)', [])
|
||||
left, operator, right = leaf
|
||||
|
||||
if operator == 'inselect':
|
||||
query = '(%s.%s in (%s))' % (table._table, left, right[0])
|
||||
params = right[1]
|
||||
elif operator in ['in', 'not in']:
|
||||
params = right and right[:] or []
|
||||
len_before = len(params)
|
||||
for i in range(len_before)[::-1]:
|
||||
if params[i] == False:
|
||||
del params[i]
|
||||
# final sanity checks - should never fail
|
||||
assert operator in (TERM_OPERATORS + ('inselect',)), \
|
||||
"Invalid operator %r in domain term %r" % (operator, leaf)
|
||||
assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \
|
||||
or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf)
|
||||
|
||||
len_after = len(params)
|
||||
check_nulls = len_after != len_before
|
||||
query = '(1=0)'
|
||||
|
||||
if len_after:
|
||||
if left == 'id':
|
||||
instr = ','.join(['%s'] * len_after)
|
||||
else:
|
||||
instr = ','.join([table._columns[left]._symbol_set[0]] * len_after)
|
||||
query = '(%s.%s %s (%s))' % (table._table, left, operator, instr)
|
||||
else:
|
||||
# the case for [field, 'in', []] or [left, 'not in', []]
|
||||
if operator == 'in':
|
||||
query = '(%s.%s IS NULL)' % (table._table, left)
|
||||
else:
|
||||
query = '(%s.%s IS NOT NULL)' % (table._table, left)
|
||||
if check_nulls:
|
||||
query = '(%s OR %s.%s IS NULL)' % (query, table._table, left)
|
||||
else:
|
||||
if leaf == TRUE_LEAF:
|
||||
query = 'TRUE'
|
||||
params = []
|
||||
|
||||
if right == False and (leaf[0] in table._columns) and table._columns[leaf[0]]._type=="boolean" and (operator == '='):
|
||||
query = '(%s.%s IS NULL or %s.%s = false )' % (table._table, left,table._table, left)
|
||||
elif (((right == False) and (type(right)==bool)) or (right is None)) and (operator == '='):
|
||||
query = '%s.%s IS NULL ' % (table._table, left)
|
||||
elif right == False and (leaf[0] in table._columns) and table._columns[leaf[0]]._type=="boolean" and (operator in ['<>', '!=']):
|
||||
query = '(%s.%s IS NOT NULL and %s.%s != false)' % (table._table, left,table._table, left)
|
||||
elif (((right == False) and (type(right)==bool)) or right is None) and (operator in ['<>', '!=']):
|
||||
query = '%s.%s IS NOT NULL' % (table._table, left)
|
||||
elif (operator == '=?'):
|
||||
op = '='
|
||||
if (right is False or right is None):
|
||||
return ( 'TRUE',[])
|
||||
if left in table._columns:
|
||||
format = table._columns[left]._symbol_set[0]
|
||||
query = '(%s.%s %s %s)' % (table._table, left, op, format)
|
||||
params = table._columns[left]._symbol_set[1](right)
|
||||
else:
|
||||
query = "(%s.%s %s '%%s')" % (table._table, left, op)
|
||||
params = right
|
||||
elif leaf == FALSE_LEAF:
|
||||
query = 'FALSE'
|
||||
params = []
|
||||
|
||||
else:
|
||||
if left == 'id':
|
||||
query = '%s.id %s %%s' % (table._table, operator)
|
||||
params = right
|
||||
else:
|
||||
like = operator in ('like', 'ilike', 'not like', 'not ilike')
|
||||
elif operator == 'inselect':
|
||||
query = '(%s."%s" in (%s))' % (table._table, left, right[0])
|
||||
params = right[1]
|
||||
|
||||
op = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
|
||||
if left in table._columns:
|
||||
format = like and '%s' or table._columns[left]._symbol_set[0]
|
||||
query = '(%s.%s %s %s)' % (table._table, left, op, format)
|
||||
elif operator in ['in', 'not in']:
|
||||
# Two cases: right is a boolean or a list. The boolean case is an
|
||||
# abuse and handled for backward compatibility.
|
||||
if isinstance(right, bool):
|
||||
_logger.warning("The domain term '%s' should use the '=' or '!=' operator." % (leaf,))
|
||||
if operator == 'in':
|
||||
r = 'NOT NULL' if right else 'NULL'
|
||||
else:
|
||||
r = 'NULL' if right else 'NOT NULL'
|
||||
query = '(%s."%s" IS %s)' % (table._table, left, r)
|
||||
params = []
|
||||
elif isinstance(right, (list, tuple)):
|
||||
params = right[:]
|
||||
check_nulls = False
|
||||
for i in range(len(params))[::-1]:
|
||||
if params[i] == False:
|
||||
check_nulls = True
|
||||
del params[i]
|
||||
|
||||
if params:
|
||||
if left == 'id':
|
||||
instr = ','.join(['%s'] * len(params))
|
||||
else:
|
||||
query = "(%s.%s %s '%s')" % (table._table, left, op, right)
|
||||
instr = ','.join([table._columns[left]._symbol_set[0]] * len(params))
|
||||
query = '(%s."%s" %s (%s))' % (table._table, left, operator, instr)
|
||||
else:
|
||||
# The case for (left, 'in', []) or (left, 'not in', []).
|
||||
query = 'FALSE' if operator == 'in' else 'TRUE'
|
||||
|
||||
add_null = False
|
||||
if like:
|
||||
if isinstance(right, str):
|
||||
str_utf8 = right
|
||||
elif isinstance(right, unicode):
|
||||
str_utf8 = right.encode('utf-8')
|
||||
else:
|
||||
str_utf8 = str(right)
|
||||
params = '%%%s%%' % str_utf8
|
||||
add_null = not str_utf8
|
||||
elif left in table._columns:
|
||||
params = table._columns[left]._symbol_set[1](right)
|
||||
if check_nulls and operator == 'in':
|
||||
query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
|
||||
elif not check_nulls and operator == 'not in':
|
||||
query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
|
||||
elif check_nulls and operator == 'not in':
|
||||
query = '(%s AND %s."%s" IS NOT NULL)' % (query, table._table, left) # needed only for TRUE.
|
||||
else: # Must not happen
|
||||
raise ValueError("Invalid domain term %r" % (leaf,))
|
||||
|
||||
if add_null:
|
||||
query = '(%s OR %s IS NULL)' % (query, left)
|
||||
elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '='):
|
||||
query = '(%s."%s" IS NULL or %s."%s" = false )' % (table._table, left, table._table, left)
|
||||
params = []
|
||||
|
||||
elif (right is False or right is None) and (operator == '='):
|
||||
query = '%s."%s" IS NULL ' % (table._table, left)
|
||||
params = []
|
||||
|
||||
elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '!='):
|
||||
query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table._table, left, table._table, left)
|
||||
params = []
|
||||
|
||||
elif (right is False or right is None) and (operator == '!='):
|
||||
query = '%s."%s" IS NOT NULL' % (table._table, left)
|
||||
params = []
|
||||
|
||||
elif (operator == '=?'):
|
||||
if (right is False or right is None):
|
||||
# '=?' is a short-circuit that makes the term TRUE if right is None or False
|
||||
query = 'TRUE'
|
||||
params = []
|
||||
else:
|
||||
# '=?' behaves like '=' in other cases
|
||||
query, params = self.__leaf_to_sql((left, '=', right), table)
|
||||
|
||||
elif left == 'id':
|
||||
query = '%s.id %s %%s' % (table._table, operator)
|
||||
params = right
|
||||
|
||||
else:
|
||||
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
|
||||
sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
|
||||
|
||||
if left in table._columns:
|
||||
format = need_wildcard and '%s' or table._columns[left]._symbol_set[0]
|
||||
if self.has_unaccent and sql_operator in ('ilike', 'not ilike'):
|
||||
query = '(unaccent(%s."%s") %s unaccent(%s))' % (table._table, left, sql_operator, format)
|
||||
else:
|
||||
query = '(%s."%s" %s %s)' % (table._table, left, sql_operator, format)
|
||||
elif left in MAGIC_COLUMNS:
|
||||
query = "(%s.\"%s\" %s %%s)" % (table._table, left, sql_operator)
|
||||
params = right
|
||||
else: # Must not happen
|
||||
raise ValueError("Invalid field %r in domain term %r" % (left, leaf))
|
||||
|
||||
add_null = False
|
||||
if need_wildcard:
|
||||
if isinstance(right, str):
|
||||
str_utf8 = right
|
||||
elif isinstance(right, unicode):
|
||||
str_utf8 = right.encode('utf-8')
|
||||
else:
|
||||
str_utf8 = str(right)
|
||||
params = '%%%s%%' % str_utf8
|
||||
add_null = not str_utf8
|
||||
elif left in table._columns:
|
||||
params = table._columns[left]._symbol_set[1](right)
|
||||
|
||||
if add_null:
|
||||
query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
|
||||
|
||||
if isinstance(params, basestring):
|
||||
params = [params]
|
||||
|
@ -555,25 +789,26 @@ class expression(object):
|
|||
def to_sql(self):
|
||||
stack = []
|
||||
params = []
|
||||
# Process the domain from right to left, using a stack, to generate a SQL expression.
|
||||
for i, e in reverse_enumerate(self.__exp):
|
||||
if self._is_leaf(e, internal=True):
|
||||
if is_leaf(e, internal=True):
|
||||
table = self.__field_tables.get(i, self.__main_table)
|
||||
q, p = self.__leaf_to_sql(e, table)
|
||||
params.insert(0, p)
|
||||
stack.append(q)
|
||||
elif e == NOT_OPERATOR:
|
||||
stack.append('(NOT (%s))' % (stack.pop(),))
|
||||
else:
|
||||
if e == NOT_OPERATOR:
|
||||
stack.append('(NOT (%s))' % (stack.pop(),))
|
||||
else:
|
||||
ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
|
||||
q1 = stack.pop()
|
||||
q2 = stack.pop()
|
||||
stack.append('(%s %s %s)' % (q1, ops[e], q2,))
|
||||
ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
|
||||
q1 = stack.pop()
|
||||
q2 = stack.pop()
|
||||
stack.append('(%s %s %s)' % (q1, ops[e], q2,))
|
||||
|
||||
query = ' AND '.join(reversed(stack))
|
||||
assert len(stack) == 1
|
||||
query = stack[0]
|
||||
joins = ' AND '.join(self.__joins)
|
||||
if joins:
|
||||
query = '(%s) AND (%s)' % (joins, query)
|
||||
query = '(%s) AND %s' % (joins, query)
|
||||
return (query, flatten(params))
|
||||
|
||||
def get_tables(self):
|
||||
|
|
|
@ -55,7 +55,7 @@ def _symbol_set(symb):
|
|||
|
||||
class _column(object):
|
||||
""" Base of all fields, a database column
|
||||
|
||||
|
||||
An instance of this object is a *description* of a database column. It will
|
||||
not hold any data, but only provide the methods to manipulate data of an
|
||||
ORM record or even prepare/update the database to hold such a field of data.
|
||||
|
@ -72,7 +72,7 @@ class _column(object):
|
|||
_symbol_set = (_symbol_c, _symbol_f)
|
||||
_symbol_get = None
|
||||
|
||||
def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete="set null", translate=False, select=False, manual=False, **args):
|
||||
def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
|
||||
"""
|
||||
|
||||
The 'manual' keyword argument specifies if the field is a custom one.
|
||||
|
@ -91,7 +91,7 @@ class _column(object):
|
|||
self.help = args.get('help', '')
|
||||
self.priority = priority
|
||||
self.change_default = change_default
|
||||
self.ondelete = ondelete
|
||||
self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
|
||||
self.translate = translate
|
||||
self._domain = domain
|
||||
self._context = context
|
||||
|
@ -112,12 +112,6 @@ class _column(object):
|
|||
def set(self, cr, obj, id, name, value, user=None, context=None):
|
||||
cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
|
||||
|
||||
def set_memory(self, cr, obj, id, name, value, user=None, context=None):
|
||||
raise Exception(_('Not implemented set_memory method !'))
|
||||
|
||||
def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None):
|
||||
raise Exception(_('Not implemented get_memory method !'))
|
||||
|
||||
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
|
||||
raise Exception(_('undefined get method !'))
|
||||
|
||||
|
@ -126,9 +120,6 @@ class _column(object):
|
|||
res = obj.read(cr, uid, ids, [name], context=context)
|
||||
return [x[name] for x in res]
|
||||
|
||||
def search_memory(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
|
||||
raise Exception(_('Not implemented search_memory method !'))
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Simple fields
|
||||
|
@ -269,7 +260,7 @@ class binary(_column):
|
|||
_column.__init__(self, string=string, **args)
|
||||
self.filters = filters
|
||||
|
||||
def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None):
|
||||
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
|
||||
if not context:
|
||||
context = {}
|
||||
if not values:
|
||||
|
@ -293,9 +284,6 @@ class binary(_column):
|
|||
res[i] = val
|
||||
return res
|
||||
|
||||
get = get_memory
|
||||
|
||||
|
||||
class selection(_column):
|
||||
_type = 'selection'
|
||||
|
||||
|
@ -355,30 +343,6 @@ class many2one(_column):
|
|||
_column.__init__(self, string=string, **args)
|
||||
self._obj = obj
|
||||
|
||||
def set_memory(self, cr, obj, id, field, values, user=None, context=None):
|
||||
obj.datas.setdefault(id, {})
|
||||
obj.datas[id][field] = values
|
||||
|
||||
def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None):
|
||||
result = {}
|
||||
for id in ids:
|
||||
result[id] = obj.datas[id].get(name, False)
|
||||
|
||||
# build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
|
||||
# we use uid=1 because the visibility of a many2one field value (just id and name)
|
||||
# must be the access right of the parent form and not the linked object itself.
|
||||
obj = obj.pool.get(self._obj)
|
||||
records = dict(obj.name_get(cr, 1,
|
||||
list(set([x for x in result.values() if x and isinstance(x, (int,long))])),
|
||||
context=context))
|
||||
for id in ids:
|
||||
if result[id] in records:
|
||||
result[id] = (result[id], records[result[id]])
|
||||
else:
|
||||
result[id] = False
|
||||
|
||||
return result
|
||||
|
||||
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
|
@ -447,55 +411,6 @@ class one2many(_column):
|
|||
#one2many can't be used as condition for defaults
|
||||
assert(self.change_default != True)
|
||||
|
||||
def get_memory(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if self._context:
|
||||
context = context.copy()
|
||||
context.update(self._context)
|
||||
if not values:
|
||||
values = {}
|
||||
res = {}
|
||||
for id in ids:
|
||||
res[id] = []
|
||||
ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
|
||||
for r in obj.pool.get(self._obj).read(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
|
||||
if r[self._fields_id] in res:
|
||||
res[r[self._fields_id]].append(r['id'])
|
||||
return res
|
||||
|
||||
def set_memory(self, cr, obj, id, field, values, user=None, context=None):
|
||||
if not context:
|
||||
context = {}
|
||||
if self._context:
|
||||
context = context.copy()
|
||||
context.update(self._context)
|
||||
if not values:
|
||||
return
|
||||
obj = obj.pool.get(self._obj)
|
||||
for act in values:
|
||||
if act[0] == 0:
|
||||
act[2][self._fields_id] = id
|
||||
obj.create(cr, user, act[2], context=context)
|
||||
elif act[0] == 1:
|
||||
obj.write(cr, user, [act[1]], act[2], context=context)
|
||||
elif act[0] == 2:
|
||||
obj.unlink(cr, user, [act[1]], context=context)
|
||||
elif act[0] == 3:
|
||||
obj.datas[act[1]][self._fields_id] = False
|
||||
elif act[0] == 4:
|
||||
obj.datas[act[1]][self._fields_id] = id
|
||||
elif act[0] == 5:
|
||||
for o in obj.datas.values():
|
||||
if o[self._fields_id] == id:
|
||||
o[self._fields_id] = False
|
||||
elif act[0] == 6:
|
||||
for id2 in (act[2] or []):
|
||||
obj.datas[id2][self._fields_id] = id
|
||||
|
||||
def search_memory(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
|
||||
raise _('Not Implemented')
|
||||
|
||||
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
|
@ -578,14 +493,34 @@ class one2many(_column):
|
|||
# (6, ?, ids) set a list of links
|
||||
#
|
||||
class many2many(_column):
|
||||
"""Encapsulates the logic of a many-to-many bidirectional relationship, handling the
|
||||
low-level details of the intermediary relationship table transparently.
|
||||
|
||||
:param str obj: destination model
|
||||
:param str rel: optional name of the intermediary relationship table. If not specified,
|
||||
a canonical name will be derived based on the alphabetically-ordered
|
||||
model names of the source and destination (in the form: ``amodel_bmodel_rel``).
|
||||
Automatic naming is not possible when the source and destination are
|
||||
the same, for obvious ambiguity reasons.
|
||||
:param str id1: optional name for the column holding the foreign key to the current
|
||||
model in the relationship table. If not specified, a canonical name
|
||||
will be derived based on the model name (in the form: `src_model_id`).
|
||||
:param str id2: optional name for the column holding the foreign key to the destination
|
||||
model in the relationship table. If not specified, a canonical name
|
||||
will be derived based on the model name (in the form: `dest_model_id`)
|
||||
:param str string: field label
|
||||
"""
|
||||
_classic_read = False
|
||||
_classic_write = False
|
||||
_prefetch = False
|
||||
_type = 'many2many'
|
||||
def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, **args):
|
||||
|
||||
def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
|
||||
"""
|
||||
"""
|
||||
_column.__init__(self, string=string, **args)
|
||||
self._obj = obj
|
||||
if '.' in rel:
|
||||
if rel and '.' in rel:
|
||||
raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
|
||||
'You used %s, which is not a valid SQL table name.')% (string,rel))
|
||||
self._rel = rel
|
||||
|
@ -593,7 +528,30 @@ class many2many(_column):
|
|||
self._id2 = id2
|
||||
self._limit = limit
|
||||
|
||||
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
|
||||
def _sql_names(self, source_model):
|
||||
"""Return the SQL names defining the structure of the m2m relationship table
|
||||
|
||||
:return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
|
||||
local_col is the name of the column holding the current model's FK, and
|
||||
dest_col is the name of the column holding the destination model's FK, and
|
||||
"""
|
||||
tbl, col1, col2 = self._rel, self._id1, self._id2
|
||||
if not all((tbl, col1, col2)):
|
||||
# the default table name is based on the stable alphabetical order of tables
|
||||
dest_model = source_model.pool.get(self._obj)
|
||||
tables = tuple(sorted([source_model._table, dest_model._table]))
|
||||
if not tbl:
|
||||
assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
|
||||
'is not possible when source and destination models are '\
|
||||
'the same'
|
||||
tbl = '%s_%s_rel' % tables
|
||||
if not col1:
|
||||
col1 = '%s_id' % source_model._table
|
||||
if not col2:
|
||||
col2 = '%s_id' % dest_model._table
|
||||
return (tbl, col1, col2)
|
||||
|
||||
def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
|
||||
if not context:
|
||||
context = {}
|
||||
if not values:
|
||||
|
@ -606,7 +564,8 @@ class many2many(_column):
|
|||
if offset:
|
||||
warnings.warn("Specifying offset at a many2many.get() may produce unpredictable results.",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
obj = obj.pool.get(self._obj)
|
||||
obj = model.pool.get(self._obj)
|
||||
rel, id1, id2 = self._sql_names(model)
|
||||
|
||||
# static domains are lists, and are evaluated both here and on client-side, while string
|
||||
# domains supposed by dynamic and evaluated on client-side only (thus ignored here)
|
||||
|
@ -636,11 +595,11 @@ class many2many(_column):
|
|||
%(order_by)s \
|
||||
%(limit)s \
|
||||
OFFSET %(offset)d' \
|
||||
% {'rel': self._rel,
|
||||
% {'rel': rel,
|
||||
'from_c': from_c,
|
||||
'tbl': obj._table,
|
||||
'id1': self._id1,
|
||||
'id2': self._id2,
|
||||
'id1': id1,
|
||||
'id2': id2,
|
||||
'where_c': where_c,
|
||||
'limit': limit_str,
|
||||
'order_by': order_by,
|
||||
|
@ -651,31 +610,32 @@ class many2many(_column):
|
|||
res[r[1]].append(r[0])
|
||||
return res
|
||||
|
||||
def set(self, cr, obj, id, name, values, user=None, context=None):
|
||||
def set(self, cr, model, id, name, values, user=None, context=None):
|
||||
if not context:
|
||||
context = {}
|
||||
if not values:
|
||||
return
|
||||
obj = obj.pool.get(self._obj)
|
||||
rel, id1, id2 = self._sql_names(model)
|
||||
obj = model.pool.get(self._obj)
|
||||
for act in values:
|
||||
if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
|
||||
continue
|
||||
if act[0] == 0:
|
||||
idnew = obj.create(cr, user, act[2], context=context)
|
||||
cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, idnew))
|
||||
cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
|
||||
elif act[0] == 1:
|
||||
obj.write(cr, user, [act[1]], act[2], context=context)
|
||||
elif act[0] == 2:
|
||||
obj.unlink(cr, user, [act[1]], context=context)
|
||||
elif act[0] == 3:
|
||||
cr.execute('delete from '+self._rel+' where ' + self._id1 + '=%s and '+ self._id2 + '=%s', (id, act[1]))
|
||||
cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
|
||||
elif act[0] == 4:
|
||||
# following queries are in the same transaction - so should be relatively safe
|
||||
cr.execute('SELECT 1 FROM '+self._rel+' WHERE '+self._id1+' = %s and '+self._id2+' = %s', (id, act[1]))
|
||||
cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
|
||||
if not cr.fetchone():
|
||||
cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, act[1]))
|
||||
cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
|
||||
elif act[0] == 5:
|
||||
cr.execute('update '+self._rel+' set '+self._id2+'=null where '+self._id2+'=%s', (id,))
|
||||
cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
|
||||
elif act[0] == 6:
|
||||
|
||||
d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
|
||||
|
@ -683,10 +643,10 @@ class many2many(_column):
|
|||
d1 = ' and ' + ' and '.join(d1)
|
||||
else:
|
||||
d1 = ''
|
||||
cr.execute('delete from '+self._rel+' where '+self._id1+'=%s AND '+self._id2+' IN (SELECT '+self._rel+'.'+self._id2+' FROM '+self._rel+', '+','.join(tables)+' WHERE '+self._rel+'.'+self._id1+'=%s AND '+self._rel+'.'+self._id2+' = '+obj._table+'.id '+ d1 +')', [id, id]+d2)
|
||||
cr.execute('delete from '+rel+' where '+id1+'=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, id]+d2)
|
||||
|
||||
for act_nbr in act[2]:
|
||||
cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s, %s)', (id, act_nbr))
|
||||
cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
|
||||
|
||||
#
|
||||
# TODO: use a name_search
|
||||
|
@ -694,32 +654,6 @@ class many2many(_column):
|
|||
def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
|
||||
return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
|
||||
|
||||
def get_memory(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
|
||||
result = {}
|
||||
for id in ids:
|
||||
result[id] = obj.datas[id].get(name, [])
|
||||
return result
|
||||
|
||||
def set_memory(self, cr, obj, id, name, values, user=None, context=None):
|
||||
if not values:
|
||||
return
|
||||
for act in values:
|
||||
# TODO: use constants instead of these magic numbers
|
||||
if act[0] == 0:
|
||||
raise _('Not Implemented')
|
||||
elif act[0] == 1:
|
||||
raise _('Not Implemented')
|
||||
elif act[0] == 2:
|
||||
raise _('Not Implemented')
|
||||
elif act[0] == 3:
|
||||
raise _('Not Implemented')
|
||||
elif act[0] == 4:
|
||||
raise _('Not Implemented')
|
||||
elif act[0] == 5:
|
||||
raise _('Not Implemented')
|
||||
elif act[0] == 6:
|
||||
obj.datas[id][name] = act[2]
|
||||
|
||||
|
||||
def get_nice_size(value):
|
||||
size = 0
|
||||
|
@ -801,8 +735,8 @@ class function(_column):
|
|||
|
||||
Implements the function field.
|
||||
|
||||
:param orm_template model: model to which the field belongs (should be ``self`` for
|
||||
a model method)
|
||||
:param orm model: model to which the field belongs (should be ``self`` for
|
||||
a model method)
|
||||
:param field_name(s): name of the field to compute, or if ``multi`` is provided,
|
||||
list of field names to compute.
|
||||
:type field_name(s): str | [str]
|
||||
|
@ -865,8 +799,8 @@ class function(_column):
|
|||
|
||||
Callable that implements the ``write`` operation for the function field.
|
||||
|
||||
:param orm_template model: model to which the field belongs (should be ``self`` for
|
||||
a model method)
|
||||
:param orm model: model to which the field belongs (should be ``self`` for
|
||||
a model method)
|
||||
:param int id: the identifier of the object to write on
|
||||
:param str field_name: name of the field to set
|
||||
:param fnct_inv_arg: arbitrary value passed when declaring the function field
|
||||
|
@ -887,10 +821,10 @@ class function(_column):
|
|||
a search criterion based on the function field into a new domain based only on
|
||||
columns that are stored in the database.
|
||||
|
||||
:param orm_template model: model to which the field belongs (should be ``self`` for
|
||||
a model method)
|
||||
:param orm_template model_again: same value as ``model`` (seriously! this is for backwards
|
||||
compatibility)
|
||||
:param orm model: model to which the field belongs (should be ``self`` for
|
||||
a model method)
|
||||
:param orm model_again: same value as ``model`` (seriously! this is for backwards
|
||||
compatibility)
|
||||
:param str field_name: name of the field to search on
|
||||
:param list criterion: domain component specifying the search criterion on the field.
|
||||
:rtype: list
|
||||
|
@ -935,7 +869,7 @@ class function(_column):
|
|||
corresponding records in the source model (whose field values
|
||||
need to be recomputed).
|
||||
|
||||
:param orm_template model: trigger_model
|
||||
:param orm model: trigger_model
|
||||
:param list trigger_ids: ids of the records of trigger_model that were
|
||||
modified
|
||||
:rtype: list
|
||||
|
@ -1064,14 +998,11 @@ class function(_column):
|
|||
result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
|
||||
return result
|
||||
|
||||
get_memory = get
|
||||
|
||||
def set(self, cr, obj, id, name, value, user=None, context=None):
|
||||
if not context:
|
||||
context = {}
|
||||
if self._fnct_inv:
|
||||
self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
|
||||
set_memory = set
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Related fields
|
||||
|
@ -1295,7 +1226,7 @@ class property(function):
|
|||
|
||||
def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
|
||||
prop = obj.pool.get('ir.property')
|
||||
# get the default values (for res_id = False) for the property fields
|
||||
# get the default values (for res_id = False) for the property fields
|
||||
default_val = self._get_defaults(obj, cr, uid, prop_names, context)
|
||||
|
||||
# build the dictionary that will be returned
|
||||
|
@ -1417,12 +1348,16 @@ class column_info(object):
|
|||
:attr parent_column: the name of the column containing the m2o
|
||||
relationship to the parent model that contains
|
||||
this column, None for local columns.
|
||||
:attr original_parent: if the column is inherited, name of the original
|
||||
parent model that contains it i.e in case of multilevel
|
||||
inheritence, None for local columns.
|
||||
"""
|
||||
def __init__(self, name, column, parent_model=None, parent_column=None):
|
||||
def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
|
||||
self.name = name
|
||||
self.column = column
|
||||
self.parent_model = parent_model
|
||||
self.parent_column = parent_column
|
||||
self.original_parent = original_parent
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
1325
openerp/osv/orm.py
|
@ -21,31 +21,29 @@
|
|||
|
||||
#.apidoc title: Objects Services (OSV)
|
||||
|
||||
import logging
|
||||
from psycopg2 import IntegrityError, errorcodes
|
||||
|
||||
import orm
|
||||
import openerp
|
||||
import openerp.netsvc as netsvc
|
||||
import openerp.pooler as pooler
|
||||
import openerp.sql_db as sql_db
|
||||
import logging
|
||||
from psycopg2 import IntegrityError, errorcodes
|
||||
from openerp.tools.func import wraps
|
||||
from openerp.tools.translate import translate
|
||||
from openerp.osv.orm import MetaModel
|
||||
from openerp.osv.orm import MetaModel, Model, TransientModel, AbstractModel
|
||||
import openerp.exceptions
|
||||
|
||||
# For backward compatibility
|
||||
except_osv = openerp.exceptions.Warning
|
||||
|
||||
class except_osv(Exception):
|
||||
def __init__(self, name, value, exc_type='warning'):
|
||||
self.name = name
|
||||
self.exc_type = exc_type
|
||||
self.value = value
|
||||
self.args = (exc_type, name)
|
||||
service = None
|
||||
|
||||
|
||||
class object_proxy(netsvc.Service):
|
||||
class object_proxy():
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('web-services')
|
||||
netsvc.Service.__init__(self, 'object_proxy', audience='')
|
||||
self.exportMethod(self.exec_workflow)
|
||||
self.exportMethod(self.execute)
|
||||
global service
|
||||
service = self
|
||||
|
||||
def check(f):
|
||||
@wraps(f)
|
||||
|
@ -119,14 +117,14 @@ class object_proxy(netsvc.Service):
|
|||
except orm.except_orm, inst:
|
||||
if inst.name == 'AccessError':
|
||||
self.logger.debug("AccessError", exc_info=True)
|
||||
self.abortResponse(1, inst.name, 'warning', inst.value)
|
||||
except except_osv, inst:
|
||||
self.abortResponse(1, inst.name, inst.exc_type, inst.value)
|
||||
netsvc.abort_response(1, inst.name, 'warning', inst.value)
|
||||
except except_osv:
|
||||
raise
|
||||
except IntegrityError, inst:
|
||||
osv_pool = pooler.get_pool(dbname)
|
||||
for key in osv_pool._sql_error.keys():
|
||||
if key in inst[0]:
|
||||
self.abortResponse(1, _('Constraint Error'), 'warning',
|
||||
netsvc.abort_response(1, _('Constraint Error'), 'warning',
|
||||
tr(osv_pool._sql_error[key], 'sql_constraint') or inst[0])
|
||||
if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
|
||||
msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set')
|
||||
|
@ -147,9 +145,9 @@ class object_proxy(netsvc.Service):
|
|||
msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
|
||||
except Exception:
|
||||
pass
|
||||
self.abortResponse(1, _('Integrity Error'), 'warning', msg)
|
||||
netsvc.abort_response(1, _('Integrity Error'), 'warning', msg)
|
||||
else:
|
||||
self.abortResponse(1, _('Integrity Error'), 'warning', inst[0])
|
||||
netsvc.abort_response(1, _('Integrity Error'), 'warning', inst[0])
|
||||
except Exception:
|
||||
self.logger.exception("Uncaught exception")
|
||||
raise
|
||||
|
@ -198,17 +196,10 @@ class object_proxy(netsvc.Service):
|
|||
cr.close()
|
||||
return res
|
||||
|
||||
|
||||
class osv_memory(orm.orm_memory):
|
||||
""" Deprecated class. """
|
||||
__metaclass__ = MetaModel
|
||||
_register = False # Set to false if the model shouldn't be automatically discovered.
|
||||
|
||||
|
||||
class osv(orm.orm):
|
||||
""" Deprecated class. """
|
||||
__metaclass__ = MetaModel
|
||||
_register = False # Set to false if the model shouldn't be automatically discovered.
|
||||
# deprecated - for backward compatibility.
|
||||
osv = Model
|
||||
osv_memory = TransientModel
|
||||
osv_abstract = AbstractModel # ;-)
|
||||
|
||||
|
||||
def start_object_proxy():
|
||||
|
|
|
@ -34,11 +34,6 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False,
|
|||
return registry.db, registry
|
||||
|
||||
|
||||
def delete_pool(db_name):
|
||||
"""Delete an existing registry."""
|
||||
RegistryManager.delete(db_name)
|
||||
|
||||
|
||||
def restart_pool(db_name, force_demo=False, status=None, update_module=False):
|
||||
"""Delete an existing registry and return a database connection and a newly initialized registry."""
|
||||
registry = RegistryManager.new(db_name, force_demo, status, update_module, True)
|
||||
|
|