[MERGE] trunk
bzr revid: al@openerp.com-20121216121509-rowl05966ej8su0q
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
print '-' * 70
|
||||
print "DEPRECATED: you are starting the OpenERP server with its old path,"
|
||||
print "please use the new executable (available in the parent directory)."
|
||||
print '-' * 70
|
||||
|
||||
# Change to the parent directory ...
|
||||
os.chdir(os.path.normpath(os.path.dirname(__file__)))
|
||||
os.chdir('..')
|
||||
# ... and execute the new executable.
|
||||
os.execv('openerp-server', sys.argv)
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
Vendored
+2
@@ -23,6 +23,7 @@ Depends:
|
||||
python-libxslt1,
|
||||
python-lxml,
|
||||
python-mako,
|
||||
python-mock,
|
||||
python-openid,
|
||||
python-psutil,
|
||||
python-psycopg2,
|
||||
@@ -33,6 +34,7 @@ Depends:
|
||||
python-reportlab,
|
||||
python-simplejson,
|
||||
python-tz,
|
||||
python-unittest2,
|
||||
python-vatnumber,
|
||||
python-vobject,
|
||||
python-webdav,
|
||||
|
||||
@@ -135,6 +135,9 @@ The view describes how the edition form or the data tree/list appear on screen.
|
||||
|
||||
A form can be called by an action opening in 'Tree' mode. The form view is generally opened from the list mode (like if the user pushes on 'switch view').
|
||||
|
||||
.. _domain:
|
||||
.. _domains:
|
||||
|
||||
The domain
|
||||
----------
|
||||
|
||||
|
||||
@@ -10,3 +10,4 @@ Miscellanous
|
||||
06_misc_need_action_specs.rst
|
||||
06_misc_user_img_specs.rst
|
||||
06_misc_import.rst
|
||||
06_misc_auto_join.rst
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
.. _performing_joins_in_select:
|
||||
|
||||
Perfoming joins in select
|
||||
=========================
|
||||
|
||||
.. versionadded:: 7.0
|
||||
|
||||
Starting with OpenERP 7.0, an ``auto_join`` attribute is added on *many2one* and
|
||||
*one2many* fields. The purpose is to allow the automatic generation of joins in
|
||||
select queries. This attribute is set to False by default, therefore not changing
|
||||
the default behavior. Please note that we consider this feature as still experimental
|
||||
and should be used only if you understand its limitations and targets.
|
||||
|
||||
Without ``_auto_join``, the behavior of expression.parse() is the same as before.
|
||||
Leafs holding a path beginning with many2one or one2many fields perform a search
|
||||
on the relational table. The result is then used to replace the leaf content.
|
||||
For example, if you have on res.partner a domain like ``[('bank_ids.name',
|
||||
'like', 'foo')]`` with bank_ids linking to res.partner.bank, 3 queries will be
|
||||
performed :
|
||||
|
||||
- 1 on res_partner_bank, with domain ``[('name', '=', 'foo')]``, that returns a
|
||||
list of res.partner.bank ids (bids)
|
||||
- 1 on res_partner, with a domain ``['bank_ids', 'in', bids)]``, that returns a
|
||||
list of res.partner ids (pids)
|
||||
- 1 on res_partner, with a domain ``[('id', 'in', pids)]``
|
||||
|
||||
When the ``auto_join`` attribute is True on a relational field, the destination
|
||||
table will be joined to produce only one query.
|
||||
|
||||
- the relational table is accessed using an alias: ``'"res_partner_bank"
|
||||
as res_partner__bank_ids``. The alias is generated using the relational field
|
||||
name. This allows to have multiple joins with different join conditions on the
|
||||
same table, depending on the domain.
|
||||
- there is a join condition between the destination table and the main table:
|
||||
``res_partner__bank_ids."partner_id"=res_partner."id"``
|
||||
- the condition is then written on the relational table:
|
||||
``res_partner__bank_ids."name" = 'foo'``
|
||||
|
||||
This manipulation is performed in expression.parse(). It checks leafs that
|
||||
contain a path, i.e. any domain containing a '.'. It then checks whether the
|
||||
first item of the path is a *many2one* or *one2many* field with the ``auto_join``
|
||||
attribute set. If set, it adds a join query and recursively analyzes the
|
||||
remaining of the leaf, using the same behavior. If the remaining path also holds
|
||||
a path with auto_join fields, it will add all tables and add every necessary
|
||||
join conditions.
|
||||
|
||||
Chaining joins allows to reduce the number of queries performed, and to avoid
|
||||
having too long equivalent leaf replacement in domains. Indeed, the internal
|
||||
queries produced by this behavior can be very costly, because they were generally
|
||||
select queries without limit that could lead to huge ('id', 'in', [...])
|
||||
leafs to analyze and execute.
|
||||
|
||||
Some limitations exist on this feature that limits its current use as of version
|
||||
7.0. **This feature is therefore considered as experimental, and used
|
||||
to speedup some precise bottlenecks in OpenERP**.
|
||||
|
||||
List of known issues and limitations:
|
||||
|
||||
- using ``auto_join`` bypasses the business logic; no name search is performed,
|
||||
only direct matches between ids using join conditions
|
||||
- ir.rules are not taken into account when analyzing and adding the join
|
||||
conditions
|
||||
|
||||
List of already-supported corner cases :
|
||||
|
||||
- one2many fields having a domain attribute. Static domains as well as dynamic
|
||||
domain are supported
|
||||
- auto_join leading to functional searchable fields
|
||||
|
||||
Typical use in OpenERP 7.0:
|
||||
|
||||
- in mail module: notification_ids field on mail_message, allowing to speedup
|
||||
the display of the various mailboxes
|
||||
- in mail module: message_ids field on mail_thread, allowing to speedup the
|
||||
display of needaction counters and documents having unread messages
|
||||
+11
-11
@@ -76,9 +76,9 @@ This phase also generates the ``rows`` indexes for any
|
||||
Conversion
|
||||
++++++++++
|
||||
|
||||
This second phase takes the record dicts, extracts the :ref:`dbid` and
|
||||
:ref:`xid` if present and attempts to convert each field to a type
|
||||
matching what OpenERP expects to write.
|
||||
This second phase takes the record dicts, extracts the :term:`database
|
||||
ID` and :term:`external ID` if present and attempts to convert each
|
||||
field to a type matching what OpenERP expects to write.
|
||||
|
||||
* Empty fields (empty strings) are replaced with the ``False`` value
|
||||
|
||||
@@ -141,14 +141,14 @@ If ``name_search`` finds no value, an error is generated. If
|
||||
``name_search`` finds multiple value, a warning is generated to warn
|
||||
the user of ``name_search`` collisions.
|
||||
|
||||
If the specified field is a :ref:`xid` (``m2o/id``), the
|
||||
If the specified field is a :term:`external ID` (``m2o/id``), the
|
||||
corresponding record it looked up in the database and used as the
|
||||
field's value. If no record is found matching the provided external
|
||||
ID, an error is generated.
|
||||
|
||||
If the specified field is a :ref:`dbid` (``m2o/.id``), the process is
|
||||
the same as for external ids (on database identifiers instead of
|
||||
external ones).
|
||||
If the specified field is a :term:`database ID` (``m2o/.id``), the
|
||||
process is the same as for external ids (on database identifiers
|
||||
instead of external ones).
|
||||
|
||||
Many to Many field
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
@@ -161,11 +161,11 @@ One to Many field
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
For each o2m record extracted, if the record has a ``name``,
|
||||
:ref:`xid` or :ref:`dbid` the :ref:`dbid` is looked up and checked
|
||||
through the same process as for m2o fields.
|
||||
:term:`external ID` or :term:`database ID` the :term:`database ID` is
|
||||
looked up and checked through the same process as for m2o fields.
|
||||
|
||||
If a :ref:`dbid` was found, a LINK_TO command is emmitted, followed by
|
||||
an UPDATE with the non-db values for the relational field.
|
||||
If a :term:`database ID` was found, a LINK_TO command is emmitted,
|
||||
followed by an UPDATE with the non-db values for the relational field.
|
||||
|
||||
Otherwise a CREATE command is emmitted.
|
||||
|
||||
|
||||
@@ -27,5 +27,15 @@ OpenERP Server API
|
||||
api_core.rst
|
||||
api_models.rst
|
||||
|
||||
Concepts
|
||||
''''''''
|
||||
|
||||
.. glossary::
|
||||
|
||||
Database ID
|
||||
|
||||
The primary key of a record in a PostgreSQL table (or a
|
||||
virtual version thereof), usually varies from one database to
|
||||
the next.
|
||||
|
||||
External ID
|
||||
|
||||
@@ -40,7 +40,6 @@ import service
|
||||
import sql_db
|
||||
import test
|
||||
import tools
|
||||
import wizard
|
||||
import workflow
|
||||
# backward compatilbility
|
||||
# TODO: This is for the web addons, can be removed later.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
# Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
|
||||
# Copyright (C) 2010, 2012 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
|
||||
@@ -85,7 +85,6 @@ The kernel of OpenERP, needed for all installation.
|
||||
'res/ir_property_view.xml',
|
||||
'security/base_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/ir.model.access-1.csv', # res.partner.address is deprecated; it is still there for backward compability only and will be removed in next version
|
||||
],
|
||||
'demo': [
|
||||
'base_demo.xml',
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<record id="main_partner" model="res.partner" context="{'default_is_company': True}">
|
||||
<field name="name">Your Company</field>
|
||||
<field name="company_id" eval="None"/>
|
||||
<field name="image" eval="False"/>
|
||||
<field name="customer" eval="False"/>
|
||||
<field name="is_company" eval="True"/>
|
||||
<field name="street"></field>
|
||||
|
||||
@@ -7,6 +7,74 @@
|
||||
<field name="customer" eval="False"/>
|
||||
<field name="email">demo@example.com</field>
|
||||
</record>
|
||||
|
||||
<record id="main_partner" model="res.partner">
|
||||
<field name="image">iVBORw0KGgoAAAANSUhEUgAAALQAAAAuCAYAAACBMDMXAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A
|
||||
/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sDCAo7GWN31l0AAA1fSURBVHja
|
||||
7Zx5dFXFHcc/eQk7KBiUTVGRRezA8ahYamgRFbWAcmyPe+uGSrW1FrFqF9u61bZWm1Kx1lgVpHVp
|
||||
3ShVVBTcBYSyDHHBulEUhVRBRJJA0j/m95rJZOa++zYS2vs95xLevLkzc+d+72++v99v7oMECRIk
|
||||
SJAgQYIECRIkSJAgQYIECQqB9skUFA4luZ6ooRzoA/QGPgWqlfn7/4aBwJHAEUA/oANwA3C/Vaen
|
||||
/N3gnPs14ErgaGB9QscdSGgNewHj5TgC6Oyp9h6wylTnUQULdsI52U2Oj4GaiHoVwC3AcM93a4DB
|
||||
QHfgAeAwoBFYAVwjZe2AamA/ma8jA6SeAowDtgH18neb9Rmg1DrK5Ggn1r+dlH8ObAE+A24D5su5
|
||||
/YCZVtvu30an/XQf7eXYJNe7BlhMvHs+DPhNRJ8pGbd9Lem/24C10t/bMpebsrHEAzXco6FBQ6Mc
|
||||
72qYoeEaDZdoqNKwSMMWq06jhuc1jNxJiHww8ILcwEaZuHnANz0P/qFAg1XXd9wKvB/4bgZwvnxf
|
||||
AawTsu/uGddlwKtCxsYCHZOs9vsBS4APCtT2QuCYGIReBnxUgP4+Aa4DukRaaG2Wzl8D35KnA7Eo
|
||||
l4v1bfCcs4c87fYF1QMXK/h9GybzaOBpsQw+PAucC6yWzw8CJ+TZZwPwE7kZ+wBzgVpZ/WoCq+kM
|
||||
ecBcrBDS18pRJ39LgF5yfBHoKvUnAH/3tHMg8A9P+RZgmvRRAwwAFHAG0NFTf5vM6Ysx5uFY4DFP
|
||||
+QYxCq8DG4Eh0uaEQDuzAnNjiKnhRcfaPqShWwyLXqLhaufcRg3faKNk3gV4N4Yl+Fz0bgdgeYz6
|
||||
f5KlfVtEnanWOMqFMEuBHoGxTgq0c3FMKfWW1D84ot7HnvbXBOr2F0PgG9O/gE4xxtUhcP7iQP3j
|
||||
ga2Bc071EXKASAqbjPN12Hr52ijV8KbTxgbtX1JbGzOyXOLWigXMVCf98A8RvfhhoF6ZNZZ9RH4s
|
||||
Bnb1jHVCHoQGeFzq94uo81oWhEZkUkg6fCnmuD7JgtCI0+3r7+6UQ8TOwEPy5KWxHjjdJzFCULAd
|
||||
+IVTXA5UtjEydw8uU2HUyTLow/sit74rcqKv1J0iJJoo0Y8tUr8vcJR1/jtC2qHyoLnINxKyVm78
|
||||
RxF1su1jfcR9PTiLNrLBTYHy4a7VvcPjtV+vzI3KFjNFx9k4TRuHqq1gRIZIT4M4TDeKZu4D7CtO
|
||||
zUjReD8SP2M8cJI4jA8A35eyPpaunA2cjPE1TgWeEX1o4xXgFOA44ETnu9o8r3eatFkfUSeXPpYH
|
||||
yrvFPD/bPj/AHyIuL7Os8wSZbByHblYuM6egTpsw3iAPiRa1EULv7SHwCglpLRBn8BPPeZ2B74im
|
||||
rXO+SwFnAXfJ3E0HrnCs4mfAvcB9gXHNEX29scDXu0yOQmNdlkQvBNYAB7j92frtp76JVfktc+94
|
||||
CD00jmMp9d5ULQnj1h0EbFXROi+EOw+Exy6FASWwsRLeWGwcjkiUwujr4Y5x0Khafv2cRBNKgc+v
|
||||
g6pnYfDj/mW+MaKbtibPouDTyltltSkWenrKlpZZ1vkQT4U78uz0XU/Z/hHkbC9L9cXibMwEzvTU
|
||||
GwX8QEJR5VI2WZmoQhyntauE4c6Wp7wM4E7zUFyojIWMM747gXM89Z4GLpIQZ++JUHsjjFHwUisR
|
||||
bprM0+lFav9wT9k1GbR6Pugmss3FC2kLfWZgGZmbZ8c+bTQ0QJZREuayv+/qIeL1wLc92ncSGQit
|
||||
Tabph8D3MIH4hRJ9SHv9ewH3aRimTIgr0/jae/oYIpJhoBOaGkfrEfqrGXRzPhiGSd03I5ZEIoqF
|
||||
SZ6yB4C5KW2s01hfBWUcmXzQ31NW5hAgpY1jtcBD9lVWvaHAStGuPhkyTJtlPkTmgZhA/8/EcgxR
|
||||
8GXR0fc7+nhCzPEtcvoYLaQd6BnCm61E5nJgT2JIqRywPyabajt/DwqfivUA7Ss+iRu9OT9NrsPw
|
||||
xzzfKEDn/QMeapoAe4jjNFb6G+wjtDb7HeYBm2WJv18mzrYMnYRIr3vIPAIjA7piQopHK5FDCrZr
|
||||
uFsiFM30mTO+1R5/YKHVxxlAlTgr9Z4lcVkRSXuO3Mc6uT77OoZhsnm1Beqri0RuTpSVLn2dS0Rm
|
||||
zM7gG2SLMZjsZAlmm8BVjn5+DRN6/Xea0KG9Fu8VIYrQjNDypJViUq4rMOnO3azvq7WRA08Joc9O
|
||||
x8M1POFZ6uo9ZO4LPGzJl4dVS23fxflcHRhfDU1ZvLo0SbWJOU/FkPovMsF3We3VWW0WA8Pxb5LC
|
||||
GUO+eASTqXOxUqJXjUW4tmnG7njl7M8x+Y46e/nvlYVDFxuSJu8eiHzYkZXNymQSu9A85VsvVnu2
|
||||
jOU8J7nzsaftDZ6yKgyp0/idp44tudbT5BTa49vFGd8yBbXaWKpLxOovtOSNjZdV8ZZggEdlBdps
|
||||
WeISWfEmilRqV4B+7gkQepgs+X8owrVdIM57bwljLpdjCZ4IXFnAW8yb0AG5AcayIsu9HRwf7Dh6
|
||||
K4DTRDON9ITvXD1bp5xthLLl9VjbkiiTzLDrfEUmDEwGb7IyxHDH58Y8F2mjTacBxyhLfnjCWPOK
|
||||
rJOfAH4b+G6WWNCOBejnXrknx3m+uwGzyei9Al/b83LEQgr//orNSjRJHjgksOw9GeFguJLnWmB8
|
||||
YCwHxHC6zqL5HpQqh8xjxTtOiV4foUzq3wfl8eTvBipVcy2domU2tNiEjsIqTKa3QwEt5qZAKK2K
|
||||
VkYqECssxFN2lqdsftr6xSD0OGCmatqymSn896RD1hLL8v63/3RoTcPNEpbsJuG4Q1W0zrUJvV10
|
||||
dZknPKUcr/9Tojfa7AgspHBvxKzF7NH24Wg8cfkdTehXPeWleernAZgQlm9ZCmGI83kL8MtA+50x
|
||||
O9O8UkYwWuSK7USM1Sb8ls7mnQj0VEZmbMlwWV+wVzDx8M/3bNpy5caCAoQ/88XX8Sc/csVtONLN
|
||||
wk1E7+YrKsoChO5fAOtc4rHOT0Wc40qI6cq/jwJMksNuf6Nngke4MkrCTT8GXlLNw1uZHtAUcJBV
|
||||
tKtES3xzV+F8for/PTQC54mf42rzXcU5nNBaFtq3zHbKde+y3Hw389iASVVHRURcQs+O6MaVEtOU
|
||||
2fBjw400PK3gMgXPZ0NmwaE0DycSWj0w8eC2op996IlxlvPFakySyofxmBBmqxD6nwGRPyiP5c21
|
||||
8Jc5UQAXIx2Z8yGBjS3ahM5OcCxvZYzx1+QxT+Ocz0sVvOwZWy9MEiiNTcrKdrYRzCHeq1FxcCPm
|
||||
DRsfKmnaOrvjCC3Wymc9L8rBOvel5buDdylz4VEY5Xyeq8JB+tMcj/3SQBRkkOfhzTT+kpiEnh+o
|
||||
V+GJMLQldMVsuo96uDvGLAPjG0zC7yP0IP57pL72O+VEaPl7Ky0tzkk6xlZPiwydMO/RlVvF9wGT
|
||||
Y5zuEuHZiLq2F12pPMF8IWafDKR0zxkLLNWOsylW9yCn+nMx5YaWf8o0XKmbz00uKMnz/FHiN9Vk
|
||||
kCQudoswCMsinP2JYoDiyCAXvXImtHjq59E8m5XC/DzBHjHI3AsTPTjcchquAk6NsZ+5FLMN1MaL
|
||||
gbqThVwNmJTnVF89se5vO8V76pYrARqGaxO+e0wcSzfbeKxDpEbCgX73wewtLwdrebB750nIXM/v
|
||||
iElcnRJDfvUM8KRHxDlXE977c7MTIXLRDv9eonJyiLaVWSTQ2ujf6ZbTUAEs18bJe9KVAaJnz8W8
|
||||
M5e2iK+KZp4TcwwH4mwTBa7ScJOVSu6CeWVpOmZb5xnKJDai8JzHMZyrTcjpbem3Qm7048DwQBza
|
||||
tezVykMIbUjjWvLj5JgBTFH+dH02ODlQfqlYwjrrqBcrtzfGKJVE+BPt5f6N9ji/aVyAyRSuxbwB
|
||||
b2Or8OAZzyrSQxzjKcDfaHLeO2Ik6vERq9GFovk/JHNY1b+ECXmuxOxPsPP/myRMsxITlRiE2RDT
|
||||
yfJ6rwVmZfNCrTYvlIbStpsxKfj9ZAKqgEsikjN2u70lghNlWaqBqSqw71u21q6n+Z6UW5W5uW7d
|
||||
AzyaeQ0mVp3vvvI9MSns0QXS0tdhwpfI3Ga7tXU8Zv+Ii1vwzI2F20UJVJBFOltwWxz5WuZZrj8D
|
||||
rtDGqpyE0dFDxZKNshy4GiH4HGC2Mv/PVdfZqBWLUYJJoJQCfwX+rPw/SEJAdqzTxgGqFNnQXuTC
|
||||
aszGlnnAjAwhvH3lvGWy8lRjUuU+pH9T4yB56B8BflWg3/vrLku6pumHZNI/pZD+2az0z365Rz1N
|
||||
P0CTPh622v4U+KPUSx91lvz0tbk2MM7LZTz1YsW3Csc6ypGOdH1k9Vnn9G33WWb9/6WcLHSExUth
|
||||
HKZtwDpVmO2IaDM5fZ3oyu0iez6IY41j9FEqS+8Glc3voGXfTwrYXZklMkEroKQ1O9fGAr7lRgpa
|
||||
8d27BDs5Uq3cvxsV2E5x3+xIkBC6qHD18yrV0oNOkGCntdBLkluSYKcktGTN3ID7K8ktSbCzWugx
|
||||
Hqc0IXSCnZbQRzqf68k9lp0gQasT+iiPQ7g1uSUJ8kFZK+nn/ph9Fo2Y1PZKmv96UYIEOeE/+J4k
|
||||
BZrmED0AAAAASUVORK5CYII=
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="user_demo" model="res.users">
|
||||
<field name="partner_id" ref="base.partner_demo"/>
|
||||
<field name="login">demo</field>
|
||||
|
||||
+266
-204
File diff suppressed because it is too large
Load Diff
+191
-131
File diff suppressed because it is too large
Load Diff
+1048
-517
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,14 @@ msgstr ""
|
||||
"Project-Id-Version: OpenERP Server 5.0.4\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-12-03 16:01+0000\n"
|
||||
"PO-Revision-Date: 2012-11-24 21:10+0000\n"
|
||||
"PO-Revision-Date: 2012-12-09 19:36+0000\n"
|
||||
"Last-Translator: Goran Kliska <gkliska@gmail.com>\n"
|
||||
"Language-Team: openerp-translators\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-04 04:59+0000\n"
|
||||
"X-Generator: Launchpad (build 16335)\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-10 04:36+0000\n"
|
||||
"X-Generator: Launchpad (build 16341)\n"
|
||||
"Language: hr\n"
|
||||
|
||||
#. module: base
|
||||
@@ -532,7 +532,7 @@ msgstr "Naziv relacije"
|
||||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
msgid "Create Access Right"
|
||||
msgstr ""
|
||||
msgstr "Kreiraj pravo pristupa"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.tv
|
||||
@@ -582,7 +582,7 @@ msgstr "Francuska Guyana"
|
||||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_hr
|
||||
msgid "Jobs, Departments, Employees Details"
|
||||
msgstr ""
|
||||
msgstr "Radna mjesta, odjeli, podaci o radnicima"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_analytic
|
||||
@@ -695,7 +695,7 @@ msgstr "Kolumbija"
|
||||
#. module: base
|
||||
#: model:res.partner.title,name:base.res_partner_title_mister
|
||||
msgid "Mister"
|
||||
msgstr ""
|
||||
msgstr "Gospodin"
|
||||
|
||||
#. module: base
|
||||
#: help:res.country,code:0
|
||||
@@ -828,6 +828,11 @@ msgid ""
|
||||
"This module provides the Integration of the LinkedIn with OpenERP.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"OpenERP Web LinkedIn modul.\n"
|
||||
"============================\n"
|
||||
"Integracija LinkedIn-a sa OpenERP-om.\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: help:ir.actions.act_window,src_model:0
|
||||
@@ -1132,7 +1137,7 @@ msgstr "Google korisnici"
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_fleet
|
||||
msgid "Fleet Management"
|
||||
msgstr ""
|
||||
msgstr "Upravljanje flotom (automobili)"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.server.object.lines,value:0
|
||||
@@ -1234,7 +1239,7 @@ msgstr "Zadnja verzija"
|
||||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
msgid "Delete Access Right"
|
||||
msgstr ""
|
||||
msgstr "Prava brisanja"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_mail_server.py:213
|
||||
@@ -1304,7 +1309,7 @@ msgstr "Vidljivo"
|
||||
#. module: base
|
||||
#: model:ir.actions.client,name:base.action_client_base_menu
|
||||
msgid "Open Settings Menu"
|
||||
msgstr ""
|
||||
msgstr "Otvori izbornik postavki"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
@@ -1427,7 +1432,7 @@ msgstr "Haiti"
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_fr_hr_payroll
|
||||
msgid "French Payroll"
|
||||
msgstr ""
|
||||
msgstr "Francuska plaća"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.ui.view:0
|
||||
@@ -2571,7 +2576,7 @@ msgstr "Grčka / Ελληνικά"
|
||||
#. module: base
|
||||
#: field:res.company,custom_footer:0
|
||||
msgid "Custom Footer"
|
||||
msgstr ""
|
||||
msgstr "Prilagođeno podnožje"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_sale_crm
|
||||
@@ -2879,7 +2884,7 @@ msgstr "Naslijeđeno"
|
||||
#: code:addons/base/ir/ir_fields.py:147
|
||||
#, python-format
|
||||
msgid "yes"
|
||||
msgstr ""
|
||||
msgstr "da"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.model.fields,serialization_field_id:0
|
||||
@@ -14826,7 +14831,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: field:ir.sequence,implementation:0
|
||||
msgid "Implementation"
|
||||
msgstr ""
|
||||
msgstr "Implementacija"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_ve
|
||||
@@ -14931,7 +14936,7 @@ msgstr "Ažuriranje sustava"
|
||||
#: field:ir.actions.report.xml,report_sxw_content:0
|
||||
#: field:ir.actions.report.xml,report_sxw_content_data:0
|
||||
msgid "SXW Content"
|
||||
msgstr ""
|
||||
msgstr "SXW Sadržaj"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.sequence,prefix:0
|
||||
@@ -14946,7 +14951,7 @@ msgstr "Sejšeli"
|
||||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_4
|
||||
msgid "Gold"
|
||||
msgstr ""
|
||||
msgstr "Zlatni"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_company.py:159
|
||||
@@ -14973,7 +14978,7 @@ msgstr "Opći podaci"
|
||||
#. module: base
|
||||
#: field:ir.model.data,complete_name:0
|
||||
msgid "Complete ID"
|
||||
msgstr ""
|
||||
msgstr "Puni ID"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.tc
|
||||
@@ -14985,7 +14990,7 @@ msgstr "Turks and Caicos Islands"
|
||||
msgid ""
|
||||
"Tax Identification Number. Check the box if this contact is subjected to "
|
||||
"taxes. Used by the some of the legal statements."
|
||||
msgstr ""
|
||||
msgstr "Porezni broj (OIB) ."
|
||||
|
||||
#. module: base
|
||||
#: field:res.partner.bank,partner_id:0
|
||||
@@ -15070,7 +15075,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Internal Notes"
|
||||
msgstr ""
|
||||
msgstr "Interne bilješke"
|
||||
|
||||
#. module: base
|
||||
#: selection:res.partner.address,type:0
|
||||
@@ -15086,7 +15091,7 @@ msgstr "Corp."
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_purchase_requisition
|
||||
msgid "Purchase Requisitions"
|
||||
msgstr ""
|
||||
msgstr "Natječaji u nabavi"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.actions.act_window,target:0
|
||||
@@ -15112,7 +15117,7 @@ msgstr "Partneri: "
|
||||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Is a Company?"
|
||||
msgstr ""
|
||||
msgstr "Je tvrtka="
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_company.py:159
|
||||
@@ -15134,7 +15139,7 @@ msgstr "Kreiraj objekt"
|
||||
#. module: base
|
||||
#: model:res.country,name:base.ss
|
||||
msgid "South Sudan"
|
||||
msgstr ""
|
||||
msgstr "Južni Sudan"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.filters,context:0
|
||||
|
||||
+951
-103
File diff suppressed because it is too large
Load Diff
+708
-326
File diff suppressed because it is too large
Load Diff
+664
-333
File diff suppressed because it is too large
Load Diff
+118
-17
@@ -7,14 +7,14 @@ msgstr ""
|
||||
"Project-Id-Version: OpenERP Server 6.0.0-rc1\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-12-03 16:01+0000\n"
|
||||
"PO-Revision-Date: 2012-11-28 13:28+0000\n"
|
||||
"PO-Revision-Date: 2012-12-14 04:43+0000\n"
|
||||
"Last-Translator: gobi <Unknown>\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: 2012-12-04 04:58+0000\n"
|
||||
"X-Generator: Launchpad (build 16335)\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-15 05:03+0000\n"
|
||||
"X-Generator: Launchpad (build 16372)\n"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_account_check_writing
|
||||
@@ -614,7 +614,7 @@ msgstr "Харьцааны Нэр"
|
||||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
msgid "Create Access Right"
|
||||
msgstr ""
|
||||
msgstr "Хандах Эрх Үүсгэх"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.tv
|
||||
@@ -1346,7 +1346,7 @@ msgstr "Андорра, Principality of"
|
||||
#. module: base
|
||||
#: field:ir.rule,perm_read:0
|
||||
msgid "Apply for Read"
|
||||
msgstr ""
|
||||
msgstr "Уншихаар Хэрэгжүүлэх"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.mn
|
||||
@@ -1431,7 +1431,7 @@ msgstr "Сүүлийн Хувилбар"
|
||||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
msgid "Delete Access Right"
|
||||
msgstr ""
|
||||
msgstr "Хандах Эрхийг Устгах"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_mail_server.py:213
|
||||
@@ -1486,7 +1486,7 @@ msgstr "Дэмжигчид"
|
||||
#. module: base
|
||||
#: field:ir.rule,perm_unlink:0
|
||||
msgid "Apply for Delete"
|
||||
msgstr ""
|
||||
msgstr "Устгахаар Хэрэгжүүлэх"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.property,type:0
|
||||
@@ -1561,6 +1561,9 @@ msgid ""
|
||||
" for uploading to OpenERP's translation "
|
||||
"platform,"
|
||||
msgstr ""
|
||||
"TGZ формат: Энэ нь шахагдсан архив бөгөөд PO файлуудыг агуулсан\n"
|
||||
" шууд OpenERP-н орчуулгын хөрсрүү хуулахад "
|
||||
"тохиромжтой файл юм."
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
@@ -1587,7 +1590,7 @@ msgstr "Тест"
|
||||
#. module: base
|
||||
#: field:ir.actions.report.xml,attachment:0
|
||||
msgid "Save as Attachment Prefix"
|
||||
msgstr ""
|
||||
msgstr "Хавсралтыг нэрлэж хадгалах угтвар"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.ui.view_sc,res_id:0
|
||||
@@ -1624,7 +1627,7 @@ msgstr "Гаити"
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_fr_hr_payroll
|
||||
msgid "French Payroll"
|
||||
msgstr ""
|
||||
msgstr "Фран Цалин"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.ui.view:0
|
||||
@@ -1698,6 +1701,33 @@ msgid ""
|
||||
"Also implements IETF RFC 5785 for services discovery on a http server,\n"
|
||||
"which needs explicit configuration in openerp-server.conf too.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Энэ модулиар баримтуудын хувьд WebDAV сервер идэвхжинэ.\n"
|
||||
"===============================================================\n"
|
||||
"\n"
|
||||
"Ингэснээр OpenObject дахь хавсралт баримтуудыг нийцтэй ямар ч интернет \n"
|
||||
"тольдруураар харах боломжтой.\n"
|
||||
"\n"
|
||||
"Суулгасан дараагаараа WebDAV сервер нь серверийн тохиргооны файлын [webdav] "
|
||||
"\n"
|
||||
" гэсэн хэсэгт тохиргоог хийх боломжтой.\n"
|
||||
"Серверийн тохиргооны параметр нь:\n"
|
||||
"\n"
|
||||
" [webdav]\n"
|
||||
" ; enable = True ; http(s) протоколь дээр WebDAV идэвхжинэ\n"
|
||||
" ; vdir = webdav ; WebDAV ажиллах директорын нэр.\n"
|
||||
" ; энэхүү webdav гэсэн анхны утгын хувьд \n"
|
||||
" ; дараах байдлаар хандана \\\"http://localhost:8069/webdav/\n"
|
||||
" ; verbose = True ; webdav-н дэлгэрэнгүй мэдэгдэлтэй горимыг нээнэ\n"
|
||||
" ; debug = True ; webdav-н дебаагдах горимыг идэвхжүүлнэ.\n"
|
||||
" ; ингэснээр мессежүүд нь python log-руу бичигдэнэ. \n"
|
||||
" ; логийн түвшин нь \\\"debug\\\" болон \\\"debug_rpc\\\" байдаг. эдгээр "
|
||||
"\n"
|
||||
"сонголтыг\n"
|
||||
" ; хэвээр нь үлдээж болно.\n"
|
||||
"\n"
|
||||
"Түүнчлэн IETF RFC 5785-г http серверт хэрэгжүүлдэг. Тодорхой тохиргоог \n"
|
||||
"openerp-server.conf-д тохируулах хэрэгтэй.\n"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.category,name:base.module_category_purchase_management
|
||||
@@ -1745,6 +1775,12 @@ msgid ""
|
||||
"=============\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Энэ модуль нь хэрэв гомдол, порталь суулгагдсан байгаа бол гомдол меню болон "
|
||||
"боломжийг портальд нэмдэг.\n"
|
||||
"============================================================================="
|
||||
"=============\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,help:base.action_res_partner_bank_account_form
|
||||
@@ -1791,7 +1827,7 @@ msgstr "Кодгүй хэл \"%s\" байна"
|
||||
#: model:ir.module.category,name:base.module_category_social_network
|
||||
#: model:ir.module.module,shortdesc:base.module_mail
|
||||
msgid "Social Network"
|
||||
msgstr ""
|
||||
msgstr "Нийгмийн Сүлжээ"
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
@@ -1832,6 +1868,28 @@ msgid ""
|
||||
"in their pockets, this module is essential.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Энэ модуль нь үдийн цайг менеж хийх модуль.\n"
|
||||
"================================\n"
|
||||
"\n"
|
||||
"Компаниуд нь ажилчиддаа орчин, нөхцлийг таатай болгох үүднээс олон "
|
||||
"нийлүүлэгчээс хачиртай талх, пицца гэх мэтийг захиалах боломжийг санал "
|
||||
"болгодог.\n"
|
||||
"\n"
|
||||
"Гэхдээ олон тооны ажилтан, нийлүүлэгч нар байгаа тохиолдолд зөв удирдлага "
|
||||
"шаардлагатай болдог.\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"\"Үдийн цайны Захиалга\" модуль нь энэ менежментийг хялбар болгож ажилчидад "
|
||||
"илүү өргөн багаж, хэрэглэгээг санал болгодог.\n"
|
||||
"\n"
|
||||
"Цаашлаад ажилчны тохиргоо дээр үндэслэн сануулга, хоол хурдан захиалах "
|
||||
"боломж зэрэгийг санал болгодогоороо хоол болон нийлүүлэгчийн менежментийг "
|
||||
"бүрэн гүйцэд болгодог.\n"
|
||||
"\n"
|
||||
"Ажилчид заавал задгай мөнгө кармалж явах заваан ажлаас ажилчдаа чөлөөлж "
|
||||
"ажилчдынхаа цагийг хэмнэхэд энэ модуль нь туйлын чухал.\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: view:wizard.ir.model.menu.create:0
|
||||
@@ -1871,7 +1929,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: help:res.partner,website:0
|
||||
msgid "Website of Partner or Company"
|
||||
msgstr ""
|
||||
msgstr "Харилцагч эсвэл Компаний веб сайт"
|
||||
|
||||
#. module: base
|
||||
#: help:base.language.install,overwrite:0
|
||||
@@ -1916,7 +1974,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_sale
|
||||
msgid "Quotations, Sale Orders, Invoicing"
|
||||
msgstr ""
|
||||
msgstr "Үнийн санал, Борлуулалтын Захиалга, Нэхэмжлэл"
|
||||
|
||||
#. module: base
|
||||
#: field:res.users,login:0
|
||||
@@ -1935,7 +1993,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_portal_project_issue
|
||||
msgid "Portal Issue"
|
||||
msgstr ""
|
||||
msgstr "Портал Асуудал"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.menu_tools
|
||||
@@ -1955,11 +2013,15 @@ msgid ""
|
||||
"Launch Manually Once: after having been launched manually, it sets "
|
||||
"automatically to Done."
|
||||
msgstr ""
|
||||
"Гараар: Гараар ажилуулна.\n"
|
||||
"Автомат: Системийн тохиргоо өөрчлөгдөх бүрт автомат ажиллана.\n"
|
||||
"Гараар нэг удаа: гараар ажилуулсан дараа автоматаар Хийгдсэн болж \n"
|
||||
"тохируулагдана."
|
||||
|
||||
#. module: base
|
||||
#: field:res.partner,image_small:0
|
||||
msgid "Small-sized image"
|
||||
msgstr ""
|
||||
msgstr "Жижиг-хэмжээт зураг"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_stock
|
||||
@@ -2040,6 +2102,15 @@ msgid ""
|
||||
"It assigns manager and user access rights to the Administrator and only user "
|
||||
"rights to the Demo user. \n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Санхүүгийн Хандах Эрх.\n"
|
||||
"=========================\n"
|
||||
"\n"
|
||||
"Энэ модуль нь санхүүгийн журнал, дансны мод гэх мэт бүх боломж руу хандах \n"
|
||||
"хандалтыг удирдах боломжийг олгодог.\n"
|
||||
"\n"
|
||||
"Энэ нь менежер, хэрэглэгч хандалтын эрхийг Администраторт олгож Demo \n"
|
||||
"хэрэглэгчид хэрэглэгч эрхийг олгодог. \n"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.attachment,res_id:0
|
||||
@@ -2066,6 +2137,8 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid "Unknown value '%s' for boolean field '%%(field)s', assuming '%s'"
|
||||
msgstr ""
|
||||
"'%s' гэсэн утга boolean '%%(field)s' талбарт мэдэгдэхгүй утга, '%s' гэж үзэж "
|
||||
"байна"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.nl
|
||||
@@ -2075,12 +2148,12 @@ msgstr "Нидерланд"
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_portal_event
|
||||
msgid "Portal Event"
|
||||
msgstr ""
|
||||
msgstr "Порталь Үйл явдал"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.translation,state:0
|
||||
msgid "Translation in Progress"
|
||||
msgstr ""
|
||||
msgstr "Орчуулга Хийгдэж байна"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_rule
|
||||
@@ -2095,7 +2168,7 @@ msgstr "Өдөр"
|
||||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_fleet
|
||||
msgid "Vehicle, leasing, insurances, costs"
|
||||
msgstr ""
|
||||
msgstr "Машин, лизинг, даатгал, өртөгүүд"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.model.access:0
|
||||
@@ -2123,6 +2196,23 @@ msgid ""
|
||||
"synchronization with other companies.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Энэ модуль нь ерөнхиий тохиолдолд хуваалцах боломжийг таны OpenERP өгөгдлийн "
|
||||
"\n"
|
||||
"баазад олгодог багаж юм.\n"
|
||||
"========================================================================\n"
|
||||
"\n"
|
||||
"Энэ нь 'хуваалцах' даруулыг нэмдэг бөгөөд OpenERP-н дуртай өгөгдлийг хамт \n"
|
||||
"ажиллагч, захиалагч, найз нартайгаа хуваалцах боломжийг вебэд олгодог.\n"
|
||||
"\n"
|
||||
"Систем нь шинэ хэрэглэгч болон группыг автоматаар үүсгэдэг. Зохистой хандах "
|
||||
"\n"
|
||||
"дүрэмийг ir.rules-д автоматаар нэмж зөвхөн хуваалцсан өгөгдөл рүү хандах \n"
|
||||
"хандалтын хяналтаар хангаддаг.\n"
|
||||
"\n"
|
||||
"Энэ нь хамтран ажиллах, мэдлэгээ хуваалцах, бусад компанитай мэдээллээ \n"
|
||||
"ижилтгэх зэрэгт маш зохимжтой.\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_process
|
||||
@@ -2135,6 +2225,8 @@ msgid ""
|
||||
"Check this box if this contact is a supplier. If it's not checked, purchase "
|
||||
"people will not see it when encoding a purchase order."
|
||||
msgstr ""
|
||||
"Хэрэв холбогч нь нийлүүлэгч бол үүнийг тэмдэглэнэ. Хэрэв тэмдэглээгүй бол "
|
||||
"худалдан авалтын хүмүүст худалдан авалтын захиалга шивэх үед харагдахгүй."
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_hr_evaluation
|
||||
@@ -2207,6 +2299,15 @@ msgid ""
|
||||
"with the effect of creating, editing and deleting either ways.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Төслийн даалгаврыг ажлын цагийн хуваарьтай ижилтгэх.\n"
|
||||
"====================================================================\n"
|
||||
"\n"
|
||||
"Энэ модуль нь Төслийн Менежмент модуль дахь даалгавруудыг Цагийн хуваарийн\n"
|
||||
"ажлууд руу тодорхой огноонд тодорхой хэрэглэгчид шилжүүлдэг. Мөн цагийн \n"
|
||||
"хуваариас нөгөө чиглэлд үүсгэх, засах, устгах\n"
|
||||
"хоёр чиглэлд хийх боломжтой.\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_model_access
|
||||
|
||||
+296
-250
File diff suppressed because it is too large
Load Diff
+159
-148
File diff suppressed because it is too large
Load Diff
+228
-117
File diff suppressed because it is too large
Load Diff
+3418
-318
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@ class actions(osv.osv):
|
||||
_order = 'name'
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=64, required=True),
|
||||
'type': fields.char('Action Type', required=True, size=32,readonly=True),
|
||||
'type': fields.char('Action Type', required=True, size=32),
|
||||
'usage': fields.char('Action Usage', size=32),
|
||||
'help': fields.text('Action description',
|
||||
help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
|
||||
|
||||
@@ -31,7 +31,6 @@ import netsvc
|
||||
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
|
||||
@@ -142,130 +141,6 @@ class ir_cron(osv.osv):
|
||||
except Exception, e:
|
||||
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
|
||||
|
||||
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:
|
||||
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 = calendar.timegm(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:
|
||||
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
|
||||
|
||||
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
|
||||
_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()
|
||||
_logger.debug('Cron execution thread for job `%s` spawned', job['name'])
|
||||
|
||||
# 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:
|
||||
cr.execute(find_next_time_query)
|
||||
next_call = cr.dictfetchone()['min_next_call']
|
||||
|
||||
if next_call:
|
||||
next_call = calendar.timegm(time.strptime(next_call, DEFAULT_SERVER_DATETIME_FORMAT))
|
||||
else:
|
||||
# no matching cron job found in database, re-schedule arbitrarily in 1 day,
|
||||
# this delay will likely be modified when running jobs complete their tasks
|
||||
next_call = time.time() + (24*3600)
|
||||
|
||||
openerp.cron.schedule_wakeup(next_call, db_name)
|
||||
|
||||
except Exception, ex:
|
||||
_logger.warning('Exception in cron:', exc_info=True)
|
||||
|
||||
finally:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
|
||||
def _process_job(self, cr, job):
|
||||
""" Run a given job taking care of the repetition.
|
||||
|
||||
@@ -365,19 +240,6 @@ class ir_cron(osv.osv):
|
||||
|
||||
return False
|
||||
|
||||
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
|
||||
# will query the database with its own cursor, possibly before the end of the
|
||||
# current transaction.
|
||||
# This commit() is not an issue in most cases, but we must absolutely avoid it
|
||||
# when the server is only starting or loading modules (hence the test on pool._init).
|
||||
if not self.pool._init:
|
||||
cr.commit()
|
||||
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
|
||||
@@ -393,20 +255,16 @@ class ir_cron(osv.osv):
|
||||
|
||||
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, 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:
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
from osv import osv
|
||||
|
||||
|
||||
class ir_needaction_mixin(osv.AbstractModel):
|
||||
'''Mixin class for objects using the need action feature.
|
||||
|
||||
@@ -60,4 +61,5 @@ class ir_needaction_mixin(osv.AbstractModel):
|
||||
dom = self._needaction_domain_get(cr, uid, context=context)
|
||||
if not dom:
|
||||
return 0
|
||||
return self.search(cr, uid, (domain or []) +dom, context=context, count=True)
|
||||
res = self.search(cr, uid, (domain or []) + dom, limit=100, order='id DESC', context=context)
|
||||
return len(res)
|
||||
|
||||
@@ -52,7 +52,7 @@ class ir_rule(osv.osv):
|
||||
eval_context = self._eval_context(cr, uid)
|
||||
for rule in self.browse(cr, uid, ids, context):
|
||||
if rule.domain_force:
|
||||
res[rule.id] = expression.normalize(eval(rule.domain_force, eval_context))
|
||||
res[rule.id] = expression.normalize_domain(eval(rule.domain_force, eval_context))
|
||||
else:
|
||||
res[rule.id] = []
|
||||
return res
|
||||
@@ -130,7 +130,7 @@ class ir_rule(osv.osv):
|
||||
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)
|
||||
dom = expression.normalize_domain(rule_domain)
|
||||
for group in rule.groups:
|
||||
if group in user.groups_id:
|
||||
group_domains.setdefault(group, []).append(dom)
|
||||
|
||||
@@ -43,7 +43,7 @@ class ir_ui_menu(osv.osv):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cache_lock = threading.RLock()
|
||||
self.clear_cache()
|
||||
self._cache = {}
|
||||
r = super(ir_ui_menu, self).__init__(*args, **kwargs)
|
||||
self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
|
||||
return r
|
||||
@@ -51,6 +51,10 @@ class ir_ui_menu(osv.osv):
|
||||
def clear_cache(self):
|
||||
with self.cache_lock:
|
||||
# radical but this doesn't frequently happen
|
||||
if self._cache:
|
||||
# Normally this is done by openerp.tools.ormcache
|
||||
# but since we do not use it, set it by ourself.
|
||||
self.pool._any_cache_cleared = True
|
||||
self._cache = {}
|
||||
|
||||
def _filter_visible_menus(self, cr, uid, ids, context=None):
|
||||
@@ -62,7 +66,7 @@ class ir_ui_menu(osv.osv):
|
||||
modelaccess = self.pool.get('ir.model.access')
|
||||
user_groups = set(self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['groups_id'])['groups_id'])
|
||||
result = []
|
||||
for menu in self.browse(cr, SUPERUSER_ID, ids, context=context):
|
||||
for menu in self.browse(cr, uid, ids, context=context):
|
||||
# this key works because user access rights are all based on user's groups (cfr ir_model_access.check)
|
||||
key = (cr.dbname, menu.id, tuple(user_groups))
|
||||
if key in self._cache:
|
||||
@@ -164,9 +168,22 @@ class ir_ui_menu(osv.osv):
|
||||
self.clear_cache()
|
||||
return super(ir_ui_menu, self).write(*args, **kwargs)
|
||||
|
||||
def unlink(self, *args, **kwargs):
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
# Detach children and promote them to top-level, because it would be unwise to
|
||||
# cascade-delete submenus blindly. We also can't use ondelete=set null because
|
||||
# that is not supported when _parent_store is used (would silently corrupt it).
|
||||
# TODO: ideally we should move them under a generic "Orphans" menu somewhere?
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
local_context = dict(context or {})
|
||||
local_context['ir.ui.menu.full_list'] = True
|
||||
direct_children_ids = self.search(cr, uid, [('parent_id', 'in', ids)], context=local_context)
|
||||
if direct_children_ids:
|
||||
self.write(cr, uid, direct_children_ids, {'parent_id': False})
|
||||
|
||||
result = super(ir_ui_menu, self).unlink(cr, uid, ids, context=context)
|
||||
self.clear_cache()
|
||||
return super(ir_ui_menu, self).unlink(*args, **kwargs)
|
||||
return result
|
||||
|
||||
def copy(self, cr, uid, id, default=None, context=None):
|
||||
ir_values_obj = self.pool.get('ir.values')
|
||||
@@ -265,17 +282,33 @@ class ir_ui_menu(osv.osv):
|
||||
|
||||
return res
|
||||
|
||||
def _get_needaction(self, cr, uid, ids, field_names, args, context=None):
|
||||
def _get_needaction_enabled(self, cr, uid, ids, field_names, args, context=None):
|
||||
""" needaction_enabled: tell whether the menu has a related action
|
||||
that uses the needaction mechanism. """
|
||||
res = dict.fromkeys(ids, False)
|
||||
for menu in self.browse(cr, uid, ids, context=context):
|
||||
if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
|
||||
obj = self.pool.get(menu.action.res_model)
|
||||
if obj and obj._needaction:
|
||||
res[menu.id] = True
|
||||
return res
|
||||
|
||||
def get_needaction_data(self, cr, uid, ids, context=None):
|
||||
""" Return for each menu entry of ids :
|
||||
- if it uses the needaction mechanism (needaction_enabled)
|
||||
- the needaction counter of the related action, taking into account
|
||||
the action domain
|
||||
"""
|
||||
res = {}
|
||||
for menu in self.browse(cr, uid, ids, context=context):
|
||||
res[menu.id] = {
|
||||
'needaction_enabled': False,
|
||||
'needaction_counter': False,
|
||||
}
|
||||
if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model:
|
||||
if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
|
||||
obj = self.pool.get(menu.action.res_model)
|
||||
if obj and obj._needaction:
|
||||
if menu.action.type=='ir.actions.act_window':
|
||||
if menu.action.type == 'ir.actions.act_window':
|
||||
dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or []
|
||||
else:
|
||||
dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain')
|
||||
@@ -286,8 +319,10 @@ class ir_ui_menu(osv.osv):
|
||||
_columns = {
|
||||
'name': fields.char('Menu', size=64, required=True, translate=True),
|
||||
'sequence': fields.integer('Sequence'),
|
||||
'child_id' : fields.one2many('ir.ui.menu', 'parent_id','Child IDs'),
|
||||
'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True),
|
||||
'child_id': fields.one2many('ir.ui.menu', 'parent_id', 'Child IDs'),
|
||||
'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True, ondelete="restrict"),
|
||||
'parent_left': fields.integer('Parent Left', select=True),
|
||||
'parent_right': fields.integer('Parent Right', select=True),
|
||||
'groups_id': fields.many2many('res.groups', 'ir_ui_menu_group_rel',
|
||||
'menu_id', 'gid', 'Groups', help="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."),
|
||||
@@ -296,11 +331,14 @@ class ir_ui_menu(osv.osv):
|
||||
'icon': fields.selection(tools.icons, 'Icon', size=64),
|
||||
'icon_pict': fields.function(_get_icon_pict, type='char', size=32),
|
||||
'web_icon': fields.char('Web Icon File', size=128),
|
||||
'web_icon_hover':fields.char('Web Icon File (hover)', size=128),
|
||||
'web_icon_hover': fields.char('Web Icon File (hover)', size=128),
|
||||
'web_icon_data': fields.function(_get_image_icon, string='Web Icon Image', type='binary', readonly=True, store=True, multi='icon'),
|
||||
'web_icon_hover_data':fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'),
|
||||
'needaction_enabled': fields.function(_get_needaction, string='Target model uses the need action mechanism', type='boolean', help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.', multi='_get_needaction'),
|
||||
'needaction_counter': fields.function(_get_needaction, string='Number of actions the user has to perform', type='integer', help='If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform.', multi='_get_needaction'),
|
||||
'web_icon_hover_data': fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'),
|
||||
'needaction_enabled': fields.function(_get_needaction_enabled,
|
||||
type='boolean',
|
||||
store=True,
|
||||
string='Target model uses the need action mechanism',
|
||||
help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.'),
|
||||
'action': fields.function(_action, fnct_inv=_action_inv,
|
||||
type='reference', string='Action',
|
||||
selection=[
|
||||
@@ -317,13 +355,14 @@ class ir_ui_menu(osv.osv):
|
||||
return _('Error ! You can not create recursive Menu.')
|
||||
|
||||
_constraints = [
|
||||
(osv.osv._check_recursion, _rec_message , ['parent_id'])
|
||||
(osv.osv._check_recursion, _rec_message, ['parent_id'])
|
||||
]
|
||||
_defaults = {
|
||||
'icon' : 'STOCK_OPEN',
|
||||
'icon_pict': ('stock', ('STOCK_OPEN','ICON_SIZE_MENU')),
|
||||
'sequence' : 10,
|
||||
'icon': 'STOCK_OPEN',
|
||||
'icon_pict': ('stock', ('STOCK_OPEN', 'ICON_SIZE_MENU')),
|
||||
'sequence': 10,
|
||||
}
|
||||
_order = "sequence,id"
|
||||
_parent_store = True
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -30,6 +30,7 @@ import re
|
||||
import urllib
|
||||
import zipimport
|
||||
|
||||
import openerp
|
||||
from openerp import modules, pooler, release, tools, addons
|
||||
from openerp.modules.db import create_categories
|
||||
from openerp.tools.parse_version import parse_version
|
||||
@@ -369,22 +370,28 @@ class module(osv.osv):
|
||||
# Mark the given modules to be installed.
|
||||
self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
|
||||
|
||||
# Mark (recursively) the newly satisfied modules to also be installed:
|
||||
# Mark (recursively) the newly satisfied modules to also be installed
|
||||
|
||||
# Select all auto-installable (but not yet installed) modules.
|
||||
domain = [('state', '=', 'uninstalled'), ('auto_install', '=', True),]
|
||||
domain = [('state', '=', 'uninstalled'), ('auto_install', '=', True)]
|
||||
uninstalled_ids = self.search(cr, uid, domain, context=context)
|
||||
uninstalled_modules = self.browse(cr, uid, uninstalled_ids, context=context)
|
||||
|
||||
# Keep those with all their dependencies satisfied.
|
||||
# Keep those with:
|
||||
# - all dependencies satisfied (installed or to be installed),
|
||||
# - at least one dependency being 'to install'
|
||||
satisfied_states = frozenset(('installed', 'to install', 'to upgrade'))
|
||||
def all_depencies_satisfied(m):
|
||||
return all(x.state in ('to install', 'installed', 'to upgrade') for x in m.dependencies_id)
|
||||
states = set(d.state for d in m.dependencies_id)
|
||||
return states.issubset(satisfied_states) and ('to install' in states)
|
||||
to_install_modules = filter(all_depencies_satisfied, uninstalled_modules)
|
||||
to_install_ids = map(lambda m: m.id, to_install_modules)
|
||||
|
||||
# Mark them to be installed.
|
||||
if to_install_ids:
|
||||
self.button_install(cr, uid, to_install_ids, context=context)
|
||||
|
||||
openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
|
||||
return dict(ACTION_DICT, name=_('Install'))
|
||||
|
||||
def button_immediate_install(self, cr, uid, ids, context=None):
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from osv import osv
|
||||
from osv import fields
|
||||
import os
|
||||
import tools
|
||||
|
||||
import openerp
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp.osv import osv, fields
|
||||
from openerp import tools
|
||||
from openerp.tools import image_resize_image
|
||||
from tools.translate import _
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
|
||||
@@ -102,6 +102,16 @@ class res_company(osv.osv):
|
||||
part_obj.create(cr, uid, {name: value or False, 'parent_id': company.partner_id.id}, context=context)
|
||||
return True
|
||||
|
||||
def _get_logo_web(self, cr, uid, ids, _field_name, _args, context=None):
|
||||
result = dict.fromkeys(ids, False)
|
||||
for record in self.browse(cr, uid, ids, context=context):
|
||||
size = (180, None)
|
||||
result[record.id] = image_resize_image(record.partner_id.image, size)
|
||||
return result
|
||||
|
||||
def _get_companies_from_partner(self, cr, uid, ids, context=None):
|
||||
return self.pool['res.company'].search(cr, uid, [('partner_id', 'in', ids)], context=context)
|
||||
|
||||
_columns = {
|
||||
'name': fields.related('partner_id', 'name', string='Company Name', size=128, required=True, store=True, type='char'),
|
||||
'parent_id': fields.many2one('res.company', 'Parent Company', select=True),
|
||||
@@ -115,6 +125,10 @@ class res_company(osv.osv):
|
||||
'rml_footer_readonly': fields.related('rml_footer', type='text', string='Report Footer', readonly=True),
|
||||
'custom_footer': fields.boolean('Custom Footer', help="Check this to define the report footer manually. Otherwise it will be filled in automatically."),
|
||||
'logo': fields.related('partner_id', 'image', string="Logo", type="binary"),
|
||||
'logo_web': fields.function(_get_logo_web, string="Logo Web", type="binary", store={
|
||||
'res.company': (lambda s, c, u, i, x: i, ['partner_id'], 10),
|
||||
'res.partner': (_get_companies_from_partner, ['image'], 10),
|
||||
}),
|
||||
'currency_id': fields.many2one('res.currency', 'Currency', required=True),
|
||||
'currency_ids': fields.one2many('res.currency', 'company_id', 'Currency'),
|
||||
'user_ids': fields.many2many('res.users', 'res_company_users_rel', 'cid', 'user_id', 'Accepted Users'),
|
||||
@@ -352,7 +366,4 @@ class res_company(osv.osv):
|
||||
]
|
||||
|
||||
|
||||
res_company()
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
||||
@@ -208,10 +208,6 @@ class res_partner(osv.osv, format_address):
|
||||
'bank_ids': fields.one2many('res.partner.bank', 'partner_id', 'Banks'),
|
||||
'website': fields.char('Website', size=64, help="Website of Partner or Company"),
|
||||
'comment': fields.text('Notes'),
|
||||
'address': fields.one2many('res.partner.address', 'partner_id', 'Addresses',
|
||||
deprecated="The address information is now directly stored on each Partner record. "\
|
||||
"Multiple contacts with their own address can be added via the child_ids relationship. "\
|
||||
"This field will be removed as of OpenERP 7.1."),
|
||||
'category_id': fields.many2many('res.partner.category', id1='partner_id', id2='category_id', string='Tags'),
|
||||
'credit_limit': fields.float(string='Credit Limit'),
|
||||
'ean13': fields.char('EAN13', size=13),
|
||||
@@ -538,7 +534,7 @@ class res_partner(osv.osv, format_address):
|
||||
The purpose of this function is to build and return an address formatted accordingly to the
|
||||
standards of the country where it belongs.
|
||||
|
||||
:param address: browse record of the res.partner.address to format
|
||||
:param address: browse record of the res.partner to format
|
||||
:returns: the address formatted in a display that fit its country habits (or the default ones
|
||||
if not country is specified)
|
||||
:rtype: string
|
||||
@@ -564,55 +560,4 @@ class res_partner(osv.osv, format_address):
|
||||
address_format = '%(company_name)s\n' + address_format
|
||||
return address_format % args
|
||||
|
||||
# res.partner.address is deprecated; it is still there for backward compability only and will be removed in next version
|
||||
class res_partner_address(osv.osv):
|
||||
_table = "res_partner"
|
||||
_name = 'res.partner.address'
|
||||
_order = 'type, name'
|
||||
_columns = {
|
||||
'parent_id': fields.many2one('res.partner', 'Company', ondelete='set null', select=True),
|
||||
'partner_id': fields.related('parent_id', type='many2one', relation='res.partner', string='Partner'), # for backward compatibility
|
||||
'type': fields.selection( [ ('default','Default'),('invoice','Invoice'), ('delivery','Delivery'), ('contact','Contact'), ('other','Other') ],'Address Type', help="Used to select automatically the right address according to the context in sales and purchases documents."),
|
||||
'function': fields.char('Function', size=128),
|
||||
'title': fields.many2one('res.partner.title','Title'),
|
||||
'name': fields.char('Contact Name', size=64, select=1),
|
||||
'street': fields.char('Street', size=128),
|
||||
'street2': fields.char('Street2', size=128),
|
||||
'zip': fields.char('Zip', change_default=True, size=24),
|
||||
'city': fields.char('City', size=128),
|
||||
'state_id': fields.many2one("res.country.state", 'Fed. State', domain="[('country_id','=',country_id)]"),
|
||||
'country_id': fields.many2one('res.country', 'Country'),
|
||||
'email': fields.char('Email', size=240),
|
||||
'phone': fields.char('Phone', size=64),
|
||||
'fax': fields.char('Fax', size=64),
|
||||
'mobile': fields.char('Mobile', size=64),
|
||||
'birthdate': fields.char('Birthdate', size=64),
|
||||
'is_customer_add': fields.related('partner_id', 'customer', type='boolean', string='Customer'),
|
||||
'is_supplier_add': fields.related('partner_id', 'supplier', type='boolean', string='Supplier'),
|
||||
'active': fields.boolean('Active', help="Uncheck the active field to hide the contact."),
|
||||
'company_id': fields.many2one('res.company', 'Company',select=1),
|
||||
'color': fields.integer('Color Index'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'active': True,
|
||||
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'res.partner', context=c),
|
||||
'color': 0,
|
||||
'type': 'default',
|
||||
}
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
logging.getLogger('res.partner').warning("Deprecated use of res.partner.address")
|
||||
if 'partner_id' in vals:
|
||||
vals['parent_id'] = vals.get('partner_id')
|
||||
del(vals['partner_id'])
|
||||
return self.pool.get('res.partner').write(cr, uid, ids, vals, context=context)
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
logging.getLogger('res.partner').warning("Deprecated use of res.partner.address")
|
||||
if 'partner_id' in vals:
|
||||
vals['parent_id'] = vals.get('partner_id')
|
||||
del(vals['partner_id'])
|
||||
return self.pool.get('res.partner').create(cr, uid, vals, context=context)
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="customer"/>
|
||||
<field name="supplier" invisible="not context.get('default_supplier')"/>
|
||||
<field name="supplier"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="ref"/>
|
||||
|
||||
@@ -253,7 +253,7 @@ class res_users(osv.osv):
|
||||
# User can write on a few of his own fields (but not his groups for example)
|
||||
SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz']
|
||||
# User can read a few of his own fields
|
||||
SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'groups_id', 'partner_id', '__last_update']
|
||||
SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update']
|
||||
|
||||
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
|
||||
def override_password(o):
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<field name="image" widget='image' class="oe_left oe_avatar" options='{"preview_image": "image_medium", "size": [90, 90]}'/>
|
||||
<div class="oe_title">
|
||||
<div class="oe_edit_only">
|
||||
<label for="name"/> (
|
||||
<label for="name"/>
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name" default_focus="1" placeholder="Name" />
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
"access_res_partner_address_group_partner_manager","res_partner_address group_partner_manager","model_res_partner_address","group_partner_manager",1,1,1,1
|
||||
"access_res_partner_address_group_user","res_partner_address group_user","model_res_partner_address","group_user",1,0,0,0
|
||||
"access_res_partner_address","res.partner.address","model_res_partner_address","group_system",1,1,1,1
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import test_base, test_expression, test_search, test_ir_values
|
||||
import test_base, test_expression, test_search, test_ir_values, test_menu
|
||||
|
||||
checks = [
|
||||
test_base,
|
||||
test_expression,
|
||||
test_search,
|
||||
test_ir_values,
|
||||
test_menu,
|
||||
]
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
import unittest2
|
||||
|
||||
from openerp.osv.orm import BaseModel
|
||||
import openerp.tests.common as common
|
||||
|
||||
|
||||
class test_expression(common.TransactionCase):
|
||||
|
||||
def test_in_not_in_m2m(self):
|
||||
def _reinit_mock(self):
|
||||
self.query_list = list()
|
||||
|
||||
def _mock_base_model_where_calc(self, model, *args, **kwargs):
|
||||
""" Mock build_email to be able to test its values. Store them into
|
||||
some internal variable for latter processing. """
|
||||
self.query_list.append(self._base_model_where_calc(model, *args, **kwargs))
|
||||
# return the lastly stored query, the one the ORM wants to perform
|
||||
return self.query_list[-1]
|
||||
|
||||
def setUp(self):
|
||||
super(test_expression, self).setUp()
|
||||
# Mock BaseModel._where_calc(), to be able to proceed to some tests about generated expression
|
||||
self._reinit_mock()
|
||||
self._base_model_where_calc = BaseModel._where_calc
|
||||
BaseModel._where_calc = lambda model, cr, uid, args, context: self._mock_base_model_where_calc(model, cr, uid, args, context)
|
||||
|
||||
def tearDown(self):
|
||||
# Remove mocks
|
||||
BaseModel._where_calc = self._base_model_where_calc
|
||||
super(test_expression, self).tearDown()
|
||||
|
||||
def test_00_in_not_in_m2m(self):
|
||||
registry, cr, uid = self.registry, self.cr, self.uid
|
||||
|
||||
# Create 4 partners with no category, or one or two categories (out of two categories).
|
||||
@@ -70,3 +92,346 @@ class test_expression(common.TransactionCase):
|
||||
# self.assertTrue(a not in with_any_other_than_a, "Search for category_id with any other than cat_a failed (1).")
|
||||
# self.assertTrue(ab in with_any_other_than_a, "Search for category_id with any other than cat_a failed (2).")
|
||||
|
||||
def test_10_expression_parse(self):
|
||||
# TDE note: those tests have been added when refactoring the expression.parse() method.
|
||||
# They come in addition to the already existing test_osv_expression.yml; maybe some tests
|
||||
# will be a bit redundant
|
||||
registry, cr, uid = self.registry, self.cr, self.uid
|
||||
users_obj = registry('res.users')
|
||||
|
||||
# Create users
|
||||
a = users_obj.create(cr, uid, {'name': 'test_A', 'login': 'test_A'})
|
||||
b1 = users_obj.create(cr, uid, {'name': 'test_B', 'login': 'test_B'})
|
||||
b1_user = users_obj.browse(cr, uid, [b1])[0]
|
||||
b2 = users_obj.create(cr, uid, {'name': 'test_B2', 'login': 'test_B2', 'parent_id': b1_user.partner_id.id})
|
||||
|
||||
# Test1: simple inheritance
|
||||
user_ids = users_obj.search(cr, uid, [('name', 'like', 'test')])
|
||||
self.assertEqual(set(user_ids), set([a, b1, b2]), 'searching through inheritance failed')
|
||||
user_ids = users_obj.search(cr, uid, [('name', '=', 'test_B')])
|
||||
self.assertEqual(set(user_ids), set([b1]), 'searching through inheritance failed')
|
||||
|
||||
# Test2: inheritance + relational fields
|
||||
user_ids = users_obj.search(cr, uid, [('child_ids.name', 'like', 'test_B')])
|
||||
self.assertEqual(set(user_ids), set([b1]), 'searching through inheritance failed')
|
||||
|
||||
def test_20_auto_join(self):
|
||||
registry, cr, uid = self.registry, self.cr, self.uid
|
||||
|
||||
# Get models
|
||||
partner_obj = registry('res.partner')
|
||||
state_obj = registry('res.country.state')
|
||||
bank_obj = registry('res.partner.bank')
|
||||
|
||||
# Get test columns
|
||||
partner_state_id_col = partner_obj._columns.get('state_id') # many2one on res.partner to res.country.state
|
||||
partner_parent_id_col = partner_obj._columns.get('parent_id') # many2one on res.partner to res.partner
|
||||
state_country_id_col = state_obj._columns.get('country_id') # many2one on res.country.state on res.country
|
||||
partner_child_ids_col = partner_obj._columns.get('child_ids') # one2many on res.partner to res.partner
|
||||
partner_bank_ids_col = partner_obj._columns.get('bank_ids') # one2many on res.partner to res.partner.bank
|
||||
category_id_col = partner_obj._columns.get('category_id') # many2many on res.partner to res.partner.category
|
||||
|
||||
# Get the first bank account type to be able to create a res.partner.bank
|
||||
bank_type = bank_obj._bank_type_get(cr, uid)[0]
|
||||
# Get country/state data
|
||||
country_us_id = registry('res.country').search(cr, uid, [('code', 'like', 'US')])[0]
|
||||
state_ids = registry('res.country.state').search(cr, uid, [('country_id', '=', country_us_id)], limit=2)
|
||||
|
||||
# Create demo data: partners and bank object
|
||||
p_a = partner_obj.create(cr, uid, {'name': 'test__A', 'state_id': state_ids[0]})
|
||||
p_b = partner_obj.create(cr, uid, {'name': 'test__B', 'state_id': state_ids[1]})
|
||||
p_aa = partner_obj.create(cr, uid, {'name': 'test__AA', 'parent_id': p_a, 'state_id': state_ids[0]})
|
||||
p_ab = partner_obj.create(cr, uid, {'name': 'test__AB', 'parent_id': p_a, 'state_id': state_ids[1]})
|
||||
p_ba = partner_obj.create(cr, uid, {'name': 'test__BA', 'parent_id': p_b, 'state_id': state_ids[0]})
|
||||
b_aa = bank_obj.create(cr, uid, {'name': '__bank_test_a', 'state': bank_type[0], 'partner_id': p_aa, 'acc_number': '1234'})
|
||||
b_ab = bank_obj.create(cr, uid, {'name': '__bank_test_b', 'state': bank_type[0], 'partner_id': p_ab, 'acc_number': '5678'})
|
||||
b_ba = bank_obj.create(cr, uid, {'name': '__bank_test_b', 'state': bank_type[0], 'partner_id': p_ba, 'acc_number': '9876'})
|
||||
|
||||
# --------------------------------------------------
|
||||
# Test1: basics about the attribute
|
||||
# --------------------------------------------------
|
||||
|
||||
category_id_col._auto_join = True
|
||||
self.assertRaises(NotImplementedError, partner_obj.search, cr, uid, [('category_id.name', '=', 'foo')])
|
||||
category_id_col._auto_join = False
|
||||
|
||||
# --------------------------------------------------
|
||||
# Test2: one2many
|
||||
# --------------------------------------------------
|
||||
|
||||
name_test = 'test_a'
|
||||
|
||||
# Do: one2many without _auto_join
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', name_test)])
|
||||
# Test result
|
||||
self.assertEqual(set(partner_ids), set([p_aa]),
|
||||
"_auto_join off: ('bank_ids.name', 'like', '..'): incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 3,
|
||||
"_auto_join off: ('bank_ids.name', 'like', '..') should produce 3 queries (1 in res_partner_bank, 2 on res_partner)")
|
||||
sql_query = self.query_list[0].get_sql()
|
||||
self.assertIn('res_partner_bank', sql_query[0],
|
||||
"_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect main table")
|
||||
self.assertIn('"res_partner_bank"."name" like %s', sql_query[1],
|
||||
"_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect where condition")
|
||||
self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]),
|
||||
"_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect parameter")
|
||||
sql_query = self.query_list[2].get_sql()
|
||||
self.assertIn('res_partner', sql_query[0],
|
||||
"_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect main table")
|
||||
self.assertIn('"res_partner"."id" in (%s)', sql_query[1],
|
||||
"_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect where condition")
|
||||
self.assertEqual(set([p_aa]), set(sql_query[2]),
|
||||
"_auto_join off: ('bank_ids.name', 'like', '..') third query incorrect parameter")
|
||||
|
||||
# Do: cascaded one2many without _auto_join
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('child_ids.bank_ids.id', 'in', [b_aa, b_ba])])
|
||||
# Test result
|
||||
self.assertEqual(set(partner_ids), set([p_a, p_b]),
|
||||
"_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 5,
|
||||
"_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 5 queries (1 in res_partner_bank, 4 on res_partner)")
|
||||
|
||||
# Do: one2many with _auto_join
|
||||
partner_bank_ids_col._auto_join = True
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('bank_ids.name', 'like', 'test_a')])
|
||||
# Test result
|
||||
self.assertEqual(set(partner_ids), set([p_aa]),
|
||||
"_auto_join on: ('bank_ids.name', 'like', '..') incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 1,
|
||||
"_auto_join on: ('bank_ids.name', 'like', '..') should produce 1 query")
|
||||
sql_query = self.query_list[0].get_sql()
|
||||
self.assertIn('"res_partner"', sql_query[0],
|
||||
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect main table")
|
||||
self.assertIn('"res_partner_bank" as "res_partner__bank_ids"', sql_query[0],
|
||||
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join")
|
||||
self.assertIn('"res_partner__bank_ids"."name" like %s', sql_query[1],
|
||||
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect where condition")
|
||||
self.assertIn('"res_partner"."id"="res_partner__bank_ids"."partner_id"', sql_query[1],
|
||||
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join condition")
|
||||
self.assertEqual(set(['%' + name_test + '%']), set(sql_query[2]),
|
||||
"_auto_join on: ('bank_ids.name', 'like', '..') query incorrect parameter")
|
||||
|
||||
# Do: one2many with _auto_join, test final leaf is an id
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('bank_ids.id', 'in', [b_aa, b_ab])])
|
||||
# Test result
|
||||
self.assertEqual(set(partner_ids), set([p_aa, p_ab]),
|
||||
"_auto_join on: ('bank_ids.id', 'in', [..]) incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 1,
|
||||
"_auto_join on: ('bank_ids.id', 'in', [..]) should produce 1 query")
|
||||
sql_query = self.query_list[0].get_sql()
|
||||
self.assertIn('"res_partner"', sql_query[0],
|
||||
"_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect main table")
|
||||
self.assertIn('"res_partner__bank_ids"."id" in (%s,%s)', sql_query[1],
|
||||
"_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect where condition")
|
||||
self.assertEqual(set([b_aa, b_ab]), set(sql_query[2]),
|
||||
"_auto_join on: ('bank_ids.id', 'in', [..]) query incorrect parameter")
|
||||
|
||||
# Do: 2 cascaded one2many with _auto_join, test final leaf is an id
|
||||
partner_child_ids_col._auto_join = True
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('child_ids.bank_ids.id', 'in', [b_aa, b_ba])])
|
||||
# Test result
|
||||
self.assertEqual(set(partner_ids), set([p_a, p_b]),
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'not in', [..]): incorrect result")
|
||||
# # Test produced queries
|
||||
self.assertEqual(len(self.query_list), 1,
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) should produce 1 query")
|
||||
sql_query = self.query_list[0].get_sql()
|
||||
self.assertIn('"res_partner"', sql_query[0],
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) incorrect main table")
|
||||
self.assertIn('"res_partner" as "res_partner__child_ids"', sql_query[0],
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join")
|
||||
self.assertIn('"res_partner_bank" as "res_partner__child_ids__bank_ids"', sql_query[0],
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join")
|
||||
self.assertIn('"res_partner__child_ids__bank_ids"."id" in (%s,%s)', sql_query[1],
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect where condition")
|
||||
self.assertIn('"res_partner"."id"="res_partner__child_ids"."parent_id"', sql_query[1],
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition")
|
||||
self.assertIn('"res_partner__child_ids"."id"="res_partner__child_ids__bank_ids"."partner_id"', sql_query[1],
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect join condition")
|
||||
self.assertEqual(set([b_aa, b_ba]), set(sql_query[2]),
|
||||
"_auto_join on: ('child_ids.bank_ids.id', 'in', [..]) query incorrect parameter")
|
||||
|
||||
# --------------------------------------------------
|
||||
# Test3: many2one
|
||||
# --------------------------------------------------
|
||||
|
||||
name_test = 'US'
|
||||
|
||||
# Do: many2one without _auto_join
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)])
|
||||
# Test result: at least our added data + demo data
|
||||
self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)),
|
||||
"_auto_join off: ('state_id.country_id.code', 'like', '..') incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 3,
|
||||
"_auto_join off: ('state_id.country_id.code', 'like', '..') should produce 3 queries (1 on res_country, 1 on res_country_state, 1 on res_partner)")
|
||||
|
||||
# Do: many2one with 1 _auto_join on the first many2one
|
||||
partner_state_id_col._auto_join = True
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)])
|
||||
# Test result: at least our added data + demo data
|
||||
self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)),
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 2,
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') should produce 2 query")
|
||||
sql_query = self.query_list[0].get_sql()
|
||||
self.assertIn('"res_country"', sql_query[0],
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table")
|
||||
self.assertIn('"res_country"."code" like %s', sql_query[1],
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition")
|
||||
self.assertEqual(['%' + name_test + '%'], sql_query[2],
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect parameter")
|
||||
sql_query = self.query_list[1].get_sql()
|
||||
self.assertIn('"res_partner"', sql_query[0],
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect main table")
|
||||
self.assertIn('"res_country_state" as "res_partner__state_id"', sql_query[0],
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect join")
|
||||
self.assertIn('"res_partner__state_id"."country_id" in (%s)', sql_query[1],
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect where condition")
|
||||
self.assertIn('"res_partner"."state_id"="res_partner__state_id"."id"', sql_query[1],
|
||||
"_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect join condition")
|
||||
|
||||
# Do: many2one with 1 _auto_join on the second many2one
|
||||
partner_state_id_col._auto_join = False
|
||||
state_country_id_col._auto_join = True
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)])
|
||||
# Test result: at least our added data + demo data
|
||||
self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)),
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 2,
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') should produce 2 query")
|
||||
# -- first query
|
||||
sql_query = self.query_list[0].get_sql()
|
||||
self.assertIn('"res_country_state"', sql_query[0],
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table")
|
||||
self.assertIn('"res_country" as "res_country_state__country_id"', sql_query[0],
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join")
|
||||
self.assertIn('"res_country_state__country_id"."code" like %s', sql_query[1],
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition")
|
||||
self.assertIn('"res_country_state"."country_id"="res_country_state__country_id"."id"', sql_query[1],
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join condition")
|
||||
self.assertEqual(['%' + name_test + '%'], sql_query[2],
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect parameter")
|
||||
# -- second query
|
||||
sql_query = self.query_list[1].get_sql()
|
||||
self.assertIn('"res_partner"', sql_query[0],
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect main table")
|
||||
self.assertIn('"res_partner"."state_id" in', sql_query[1],
|
||||
"_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 2 incorrect where condition")
|
||||
|
||||
# Do: many2one with 2 _auto_join
|
||||
partner_state_id_col._auto_join = True
|
||||
state_country_id_col._auto_join = True
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('state_id.country_id.code', 'like', name_test)])
|
||||
# Test result: at least our added data + demo data
|
||||
self.assertTrue(set([p_a, p_b, p_aa, p_ab, p_ba]).issubset(set(partner_ids)),
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 1,
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') should produce 1 query")
|
||||
sql_query = self.query_list[0].get_sql()
|
||||
self.assertIn('"res_partner"', sql_query[0],
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect main table")
|
||||
self.assertIn('"res_country_state" as "res_partner__state_id"', sql_query[0],
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join")
|
||||
self.assertIn('"res_country" as "res_partner__state_id__country_id"', sql_query[0],
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join")
|
||||
self.assertIn('"res_partner__state_id__country_id"."code" like %s', sql_query[1],
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect where condition")
|
||||
self.assertIn('"res_partner"."state_id"="res_partner__state_id"."id"', sql_query[1],
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition")
|
||||
self.assertIn('"res_partner__state_id"."country_id"="res_partner__state_id__country_id"."id"', sql_query[1],
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join condition")
|
||||
self.assertEqual(['%' + name_test + '%'], sql_query[2],
|
||||
"_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect parameter")
|
||||
|
||||
# --------------------------------------------------
|
||||
# Test4: domain attribute on one2many fields
|
||||
# --------------------------------------------------
|
||||
|
||||
partner_child_ids_col._auto_join = True
|
||||
partner_bank_ids_col._auto_join = True
|
||||
partner_child_ids_col._domain = lambda self: ['!', ('name', '=', self._name)]
|
||||
partner_bank_ids_col._domain = [('acc_number', 'like', '1')]
|
||||
# Do: 2 cascaded one2many with _auto_join, test final leaf is an id
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa, b_ba])])
|
||||
# Test result: at least one of our added data
|
||||
self.assertTrue(set([p_a]).issubset(set(partner_ids)),
|
||||
"_auto_join on one2many with domains incorrect result")
|
||||
self.assertTrue(set([p_ab, p_ba]) not in set(partner_ids),
|
||||
"_auto_join on one2many with domains incorrect result")
|
||||
# Test produced queries that domains effectively present
|
||||
sql_query = self.query_list[0].get_sql()
|
||||
self.assertIn('"res_partner__child_ids__bank_ids"."acc_number" like %s', sql_query[1],
|
||||
"_auto_join on one2many with domains incorrect result")
|
||||
# TDE TODO: check first domain has a correct table name
|
||||
self.assertIn('"res_partner__child_ids"."name" = %s', sql_query[1],
|
||||
"_auto_join on one2many with domains incorrect result")
|
||||
|
||||
partner_child_ids_col._domain = lambda self: [('name', '=', '__%s' % self._name)]
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, ['&', (1, '=', 1), ('child_ids.bank_ids.id', 'in', [b_aa, b_ba])])
|
||||
# Test result: no one
|
||||
self.assertFalse(partner_ids,
|
||||
"_auto_join on one2many with domains incorrect result")
|
||||
|
||||
# ----------------------------------------
|
||||
# Test5: result-based tests
|
||||
# ----------------------------------------
|
||||
|
||||
partner_bank_ids_col._auto_join = False
|
||||
partner_child_ids_col._auto_join = False
|
||||
partner_state_id_col._auto_join = False
|
||||
partner_parent_id_col._auto_join = False
|
||||
state_country_id_col._auto_join = False
|
||||
partner_child_ids_col._domain = []
|
||||
partner_bank_ids_col._domain = []
|
||||
|
||||
# Do: ('child_ids.state_id.country_id.code', 'like', '..') without _auto_join
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('child_ids.state_id.country_id.code', 'like', name_test)])
|
||||
# Test result: at least our added data + demo data
|
||||
self.assertTrue(set([p_a, p_b]).issubset(set(partner_ids)),
|
||||
"_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 5,
|
||||
"_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect")
|
||||
|
||||
# Do: ('child_ids.state_id.country_id.code', 'like', '..') with _auto_join
|
||||
partner_child_ids_col._auto_join = True
|
||||
partner_state_id_col._auto_join = True
|
||||
state_country_id_col._auto_join = True
|
||||
self._reinit_mock()
|
||||
partner_ids = partner_obj.search(cr, uid, [('child_ids.state_id.country_id.code', 'like', name_test)])
|
||||
# Test result: at least our added data + demo data
|
||||
self.assertTrue(set([p_a, p_b]).issubset(set(partner_ids)),
|
||||
"_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result")
|
||||
# Test produced queries
|
||||
self.assertEqual(len(self.query_list), 1,
|
||||
"_auto_join on: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect")
|
||||
|
||||
# Remove mocks and modifications
|
||||
partner_bank_ids_col._auto_join = False
|
||||
partner_child_ids_col._auto_join = False
|
||||
partner_state_id_col._auto_join = False
|
||||
partner_parent_id_col._auto_join = False
|
||||
state_country_id_col._auto_join = False
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest2.main()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import unittest2
|
||||
|
||||
import openerp.tests.common as common
|
||||
|
||||
class test_menu(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(test_menu,self).setUp()
|
||||
self.Menus = self.registry('ir.ui.menu')
|
||||
|
||||
def test_00_menu_deletion(self):
|
||||
"""Verify that menu deletion works properly when there are child menus, and those
|
||||
are indeed made orphans"""
|
||||
cr, uid, Menus = self.cr, self.uid, self.Menus
|
||||
|
||||
# Generic trick necessary for search() calls to avoid hidden menus
|
||||
ctx = {'ir.ui.menu.full_list': True}
|
||||
|
||||
root_id = Menus.create(cr, uid, {'name': 'Test root'})
|
||||
child1_id = Menus.create(cr, uid, {'name': 'Test child 1', 'parent_id': root_id})
|
||||
child2_id = Menus.create(cr, uid, {'name': 'Test child 2', 'parent_id': root_id})
|
||||
child21_id = Menus.create(cr, uid, {'name': 'Test child 2-1', 'parent_id': child2_id})
|
||||
|
||||
all_ids = [root_id, child1_id, child2_id, child21_id]
|
||||
|
||||
# delete and check that direct children are promoted to top-level
|
||||
# cfr. explanation in menu.unlink()
|
||||
Menus.unlink(cr, uid, [root_id])
|
||||
|
||||
remaining_ids = Menus.search(cr, uid, [('id', 'in', all_ids)], order="id", context=ctx)
|
||||
self.assertEqual([child1_id, child2_id, child21_id], remaining_ids)
|
||||
|
||||
orphan_ids = Menus.search(cr, uid, [('id', 'in', all_ids), ('parent_id', '=', False)], order="id", context=ctx)
|
||||
self.assertEqual([child1_id, child2_id], orphan_ids)
|
||||
@@ -2,16 +2,17 @@ import unittest2
|
||||
|
||||
import openerp.tests.common as common
|
||||
|
||||
class test_expression(common.TransactionCase):
|
||||
|
||||
def test_search_order(self):
|
||||
class test_search(common.TransactionCase):
|
||||
|
||||
def test_00_search_order(self):
|
||||
|
||||
registry, cr, uid = self.registry, self.cr, self.uid
|
||||
|
||||
# Create 6 partners with a given name, and a given creation order to
|
||||
# ensure the order of their ID. Some are set as unactive to verify they
|
||||
# are by default excluded from the searches and to provide a second
|
||||
# `order` argument.
|
||||
# Create 6 partners with a given name, and a given creation order to
|
||||
# ensure the order of their ID. Some are set as unactive to verify they
|
||||
# are by default excluded from the searches and to provide a second
|
||||
# `order` argument.
|
||||
|
||||
partners = registry('res.partner')
|
||||
c = partners.create(cr, uid, {'name': 'test_search_order_C'})
|
||||
@@ -23,9 +24,9 @@ class test_expression(common.TransactionCase):
|
||||
|
||||
# The tests.
|
||||
|
||||
# The basic searches should exclude records that have active = False.
|
||||
# The order of the returned ids should be given by the `order`
|
||||
# parameter of search().
|
||||
# The basic searches should exclude records that have active = False.
|
||||
# The order of the returned ids should be given by the `order`
|
||||
# parameter of search().
|
||||
|
||||
name_asc = partners.search(cr, uid, [('name', 'like', 'test_search_order%')], order="name asc")
|
||||
self.assertEqual([a, ab, b, c], name_asc, "Search with 'NAME ASC' order failed.")
|
||||
@@ -36,9 +37,9 @@ class test_expression(common.TransactionCase):
|
||||
id_desc = partners.search(cr, uid, [('name', 'like', 'test_search_order%')], order="id desc")
|
||||
self.assertEqual([ab, b, a, c], id_desc, "Search with 'ID DESC' order failed.")
|
||||
|
||||
# The inactive records shouldn't be excluded as soon as a condition on
|
||||
# that field is present in the domain. The `order` parameter of
|
||||
# search() should support any legal coma-separated values.
|
||||
# The inactive records shouldn't be excluded as soon as a condition on
|
||||
# that field is present in the domain. The `order` parameter of
|
||||
# search() should support any legal coma-separated values.
|
||||
|
||||
active_asc_id_asc = partners.search(cr, uid, [('name', 'like', 'test_search_order%'), '|', ('active', '=', True), ('active', '=', False)], order="active asc, id asc")
|
||||
self.assertEqual([d, e, c, a, b, ab], active_asc_id_asc, "Search with 'ACTIVE ASC, ID ASC' order failed.")
|
||||
@@ -57,4 +58,52 @@ class test_expression(common.TransactionCase):
|
||||
id_desc_active_desc = partners.search(cr, uid, [('name', 'like', 'test_search_order%'), '|', ('active', '=', True), ('active', '=', False)], order="id desc, active desc")
|
||||
self.assertEqual([e, ab, b, a, d, c], id_desc_active_desc, "Search with 'ID DESC, ACTIVE DESC' order failed.")
|
||||
|
||||
def test_10_inherits_m2order(self):
|
||||
registry, cr, uid = self.registry, self.cr, self.uid
|
||||
users_obj = registry('res.users')
|
||||
|
||||
# Find Employee group
|
||||
group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
|
||||
group_employee_id = group_employee_ref and group_employee_ref[1] or False
|
||||
|
||||
# Get country/state data
|
||||
country_us_id = registry('res.country').search(cr, uid, [('code', 'like', 'US')])[0]
|
||||
state_ids = registry('res.country.state').search(cr, uid, [('country_id', '=', country_us_id)], limit=2)
|
||||
country_be_id = registry('res.country').search(cr, uid, [('code', 'like', 'BE')])[0]
|
||||
|
||||
# Create test users
|
||||
search_user = users_obj.create(cr, uid, {'name': '__search', 'login': '__search', 'groups_id': [(6, 0, [group_employee_id])]})
|
||||
a = users_obj.create(cr, uid, {'name': '__test_A', 'login': '__test_A', 'country_id': country_be_id, 'state_id': country_be_id})
|
||||
b = users_obj.create(cr, uid, {'name': '__test_B', 'login': '__a_test_B', 'country_id': country_us_id, 'state_id': state_ids[1]})
|
||||
c = users_obj.create(cr, uid, {'name': '__test_B', 'login': '__z_test_B', 'country_id': country_us_id, 'state_id': state_ids[0]})
|
||||
|
||||
# Do: search on res.users, order on a field on res.partner to try inherits'd fields, then res.users
|
||||
user_ids = users_obj.search(cr, search_user, [], order='name asc, login desc')
|
||||
expected_ids = [search_user, a, c, b]
|
||||
test_user_ids = filter(lambda x: x in expected_ids, user_ids)
|
||||
self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order')
|
||||
|
||||
# Do: order on many2one and inherits'd fields
|
||||
user_ids = users_obj.search(cr, search_user, [], order='state_id asc, country_id desc, name asc, login desc')
|
||||
expected_ids = [c, b, a, search_user]
|
||||
test_user_ids = filter(lambda x: x in expected_ids, user_ids)
|
||||
self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order')
|
||||
|
||||
# Do: order on many2one and inherits'd fields
|
||||
user_ids = users_obj.search(cr, search_user, [], order='country_id desc, state_id desc, name asc, login desc')
|
||||
expected_ids = [search_user, b, c, a]
|
||||
test_user_ids = filter(lambda x: x in expected_ids, user_ids)
|
||||
self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order')
|
||||
|
||||
# Do: order on many2one, but not by specifying in order parameter of search, but by overriding _order of res_users
|
||||
old_order = users_obj._order
|
||||
users_obj._order = 'country_id desc, name asc, login desc'
|
||||
user_ids = users_obj.search(cr, search_user, [])
|
||||
expected_ids = [search_user, c, b, a]
|
||||
test_user_ids = filter(lambda x: x in expected_ids, user_ids)
|
||||
self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order')
|
||||
users_obj._order = old_order
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest2.main()
|
||||
|
||||
@@ -2,6 +2,8 @@ import logging
|
||||
import sys
|
||||
|
||||
import openerp
|
||||
from openerp import tools
|
||||
from openerp.modules import module
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,8 +34,25 @@ import server
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
# The only shared option is '--addons-path=' needed to discover additional
|
||||
# commands from modules
|
||||
if len(args) > 1 and args[0].startswith('--addons-path=') and not args[1].startswith("-"):
|
||||
tools.config.parse_config([args[0]])
|
||||
args = args[1:]
|
||||
|
||||
# Default legacy command
|
||||
command = "server"
|
||||
|
||||
# Subcommand discovery
|
||||
if len(args) and not args[0].startswith("-"):
|
||||
for m in module.get_modules():
|
||||
m = 'openerp.addons.' + m
|
||||
__import__(m)
|
||||
#try:
|
||||
#except Exception, e:
|
||||
# raise
|
||||
# print e
|
||||
command = args[0]
|
||||
args = args[1:]
|
||||
|
||||
|
||||
@@ -94,10 +94,8 @@ def setup_pid_file():
|
||||
def preload_registry(dbname):
|
||||
""" Preload a registry, and start the cron."""
|
||||
try:
|
||||
db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=openerp.tools.config['init'] or openerp.tools.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()
|
||||
update_module = True if openerp.tools.config['init'] or openerp.tools.config['update'] else False
|
||||
db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=update_module, pooljobs=False)
|
||||
except Exception:
|
||||
_logger.exception('Failed to initialize database `%s`.', dbname)
|
||||
|
||||
|
||||
@@ -35,10 +35,6 @@ 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 = []
|
||||
|
||||
|
||||
-212
@@ -1,212 +0,0 @@
|
||||
#!/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
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# 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()
|
||||
|
||||
# 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:
|
||||
@@ -146,6 +146,14 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
|
||||
cr.execute("select (now() at time zone 'UTC')::timestamp")
|
||||
dt_before_load = cr.fetchone()[0]
|
||||
|
||||
# Query manual fields for all models at once and save them on the registry
|
||||
# so the initialization code for each model does not have to do it
|
||||
# one model at a time.
|
||||
pool.fields_by_model = {}
|
||||
cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',))
|
||||
for field in cr.dictfetchall():
|
||||
pool.fields_by_model.setdefault(field['model'], []).append(field)
|
||||
|
||||
# register, instantiate and initialize models for each modules
|
||||
for index, package in enumerate(graph):
|
||||
module_name = package.name
|
||||
@@ -159,6 +167,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
|
||||
load_openerp_module(package.name)
|
||||
|
||||
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)
|
||||
@@ -220,6 +229,10 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
|
||||
delattr(package, kind)
|
||||
|
||||
cr.commit()
|
||||
|
||||
# The query won't be valid for models created later (i.e. custom model
|
||||
# created after the registry has been loaded), so empty its result.
|
||||
pool.fields_by_model = None
|
||||
|
||||
cr.commit()
|
||||
|
||||
@@ -239,7 +252,7 @@ def _check_module_names(cr, module_names):
|
||||
incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
|
||||
_logger.warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
|
||||
|
||||
def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules):
|
||||
def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules, perform_checks):
|
||||
"""Loads modules marked with ``states``, adding them to ``graph`` and
|
||||
``loaded_modules`` and returns a list of installed/upgraded modules."""
|
||||
processed_modules = []
|
||||
@@ -248,7 +261,7 @@ def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_m
|
||||
module_list = [name for (name,) in cr.fetchall() if name not in graph]
|
||||
graph.add_modules(cr, module_list, force)
|
||||
_logger.debug('Updating graph with %d more modules', len(module_list))
|
||||
loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules)
|
||||
loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules, perform_checks=perform_checks)
|
||||
processed_modules.extend(processed)
|
||||
loaded_modules.extend(loaded)
|
||||
if not processed: break
|
||||
@@ -293,7 +306,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
|
||||
# processed_modules: for cleanup step after install
|
||||
# loaded_modules: to avoid double loading
|
||||
report = pool._assertion_report
|
||||
loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
|
||||
loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=update_module, report=report)
|
||||
|
||||
if tools.config['load_language']:
|
||||
for lang in tools.config['load_language'].split(','):
|
||||
@@ -333,11 +346,11 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
|
||||
# be dropped in STEP 6 later, before restarting the loading
|
||||
# process.
|
||||
states_to_load = ['installed', 'to upgrade', 'to remove']
|
||||
processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules)
|
||||
processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules, update_module)
|
||||
processed_modules.extend(processed)
|
||||
if update_module:
|
||||
states_to_load = ['to install']
|
||||
processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules)
|
||||
processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules, update_module)
|
||||
processed_modules.extend(processed)
|
||||
|
||||
# load custom models
|
||||
|
||||
@@ -433,8 +433,9 @@ def get_modules():
|
||||
return name
|
||||
|
||||
def is_really_module(name):
|
||||
name = opj(dir, name)
|
||||
return os.path.isdir(name) or zipfile.is_zipfile(name)
|
||||
manifest_name = opj(dir, name, '__openerp__.py')
|
||||
zipfile_name = opj(dir, name)
|
||||
return os.path.isfile(manifest_name) or zipfile.is_zipfile(zipfile_name)
|
||||
return map(clean, filter(is_really_module, os.listdir(dir)))
|
||||
|
||||
plist = []
|
||||
|
||||
+115
-12
@@ -28,7 +28,6 @@ import threading
|
||||
|
||||
import openerp.sql_db
|
||||
import openerp.osv.orm
|
||||
import openerp.cron
|
||||
import openerp.tools
|
||||
import openerp.modules.db
|
||||
import openerp.tools.config
|
||||
@@ -51,6 +50,7 @@ class Registry(object):
|
||||
self._init = True
|
||||
self._init_parent = {}
|
||||
self._assertion_report = assertion_report.assertion_report()
|
||||
self.fields_by_model = None
|
||||
|
||||
# modules fully loaded (maintained during init phase by `loading` module)
|
||||
self._init_modules = set()
|
||||
@@ -58,6 +58,21 @@ class Registry(object):
|
||||
self.db_name = db_name
|
||||
self.db = openerp.sql_db.db_connect(db_name)
|
||||
|
||||
# In monoprocess cron jobs flag (pooljobs)
|
||||
self.cron = False
|
||||
|
||||
# Inter-process signaling (used only when openerp.multi_process is True):
|
||||
# The `base_registry_signaling` sequence indicates the whole registry
|
||||
# must be reloaded.
|
||||
# The `base_cache_signaling sequence` indicates all caches must be
|
||||
# invalidated (i.e. cleared).
|
||||
self.base_registry_signaling_sequence = 1
|
||||
self.base_cache_signaling_sequence = 1
|
||||
|
||||
# Flag indicating if at least one model cache has been cleared.
|
||||
# Useful only in a multi-process context.
|
||||
self._any_cache_cleared = False
|
||||
|
||||
cr = self.db.cursor()
|
||||
has_unaccent = openerp.modules.db.has_unaccent(cr)
|
||||
if openerp.tools.config['unaccent'] and not has_unaccent:
|
||||
@@ -112,7 +127,7 @@ class Registry(object):
|
||||
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)
|
||||
self.cron = True
|
||||
|
||||
def clear_caches(self):
|
||||
""" Clear the caches
|
||||
@@ -121,6 +136,36 @@ class Registry(object):
|
||||
"""
|
||||
for model in self.models.itervalues():
|
||||
model.clear_caches()
|
||||
# Special case for ir_ui_menu which does not use openerp.tools.ormcache.
|
||||
ir_ui_menu = self.models.get('ir.ui.menu')
|
||||
if ir_ui_menu:
|
||||
ir_ui_menu.clear_cache()
|
||||
|
||||
|
||||
# Useful only in a multi-process context.
|
||||
def reset_any_cache_cleared(self):
|
||||
self._any_cache_cleared = False
|
||||
|
||||
# Useful only in a multi-process context.
|
||||
def any_cache_cleared(self):
|
||||
return self._any_cache_cleared
|
||||
|
||||
@classmethod
|
||||
def setup_multi_process_signaling(cls, cr):
|
||||
if not openerp.multi_process:
|
||||
return
|
||||
|
||||
# Inter-process signaling:
|
||||
# The `base_registry_signaling` sequence indicates the whole registry
|
||||
# must be reloaded.
|
||||
# The `base_cache_signaling sequence` indicates all caches must be
|
||||
# invalidated (i.e. cleared).
|
||||
cr.execute("""SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'""")
|
||||
if not cr.fetchall():
|
||||
cr.execute("""CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1""")
|
||||
cr.execute("""SELECT nextval('base_registry_signaling')""")
|
||||
cr.execute("""CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1""")
|
||||
cr.execute("""SELECT nextval('base_cache_signaling')""")
|
||||
|
||||
@contextmanager
|
||||
def cursor(self, auto_commit=True):
|
||||
@@ -182,6 +227,7 @@ class RegistryManager(object):
|
||||
|
||||
cr = registry.db.cursor()
|
||||
try:
|
||||
Registry.setup_multi_process_signaling(cr)
|
||||
registry.do_parent_store(cr)
|
||||
registry.get('ir.actions.report.xml').register_all(cr)
|
||||
cr.commit()
|
||||
@@ -195,20 +241,11 @@ class RegistryManager(object):
|
||||
|
||||
@classmethod
|
||||
def delete(cls, 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.
|
||||
"""
|
||||
"""Delete the registry linked to a given database. """
|
||||
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):
|
||||
@@ -232,5 +269,71 @@ class RegistryManager(object):
|
||||
if db_name in cls.registries:
|
||||
cls.registries[db_name].clear_caches()
|
||||
|
||||
@classmethod
|
||||
def check_registry_signaling(cls, db_name):
|
||||
if openerp.multi_process and db_name in cls.registries:
|
||||
registry = cls.get(db_name, pooljobs=False)
|
||||
cr = registry.db.cursor()
|
||||
try:
|
||||
cr.execute("""
|
||||
SELECT base_registry_signaling.last_value,
|
||||
base_cache_signaling.last_value
|
||||
FROM base_registry_signaling, base_cache_signaling""")
|
||||
r, c = cr.fetchone()
|
||||
# Check if the model registry must be reloaded (e.g. after the
|
||||
# database has been updated by another process).
|
||||
if registry.base_registry_signaling_sequence != r:
|
||||
_logger.info("Reloading the model registry after database signaling.")
|
||||
# Don't run the cron in the Gunicorn worker.
|
||||
registry = cls.new(db_name, pooljobs=False)
|
||||
registry.base_registry_signaling_sequence = r
|
||||
# Check if the model caches must be invalidated (e.g. after a write
|
||||
# occured on another process). Don't clear right after a registry
|
||||
# has been reload.
|
||||
elif registry.base_cache_signaling_sequence != c:
|
||||
_logger.info("Invalidating all model caches after database signaling.")
|
||||
registry.base_cache_signaling_sequence = c
|
||||
registry.clear_caches()
|
||||
registry.reset_any_cache_cleared()
|
||||
# One possible reason caches have been invalidated is the
|
||||
# use of decimal_precision.write(), in which case we need
|
||||
# to refresh fields.float columns.
|
||||
for model in registry.models.values():
|
||||
for column in model._columns.values():
|
||||
if hasattr(column, 'digits_change'):
|
||||
column.digits_change(cr)
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
@classmethod
|
||||
def signal_caches_change(cls, db_name):
|
||||
if openerp.multi_process and db_name in cls.registries:
|
||||
# Check the registries if any cache has been cleared and signal it
|
||||
# through the database to other processes.
|
||||
registry = cls.get(db_name, pooljobs=False)
|
||||
if registry.any_cache_cleared():
|
||||
_logger.info("At least one model cache has been cleared, signaling through the database.")
|
||||
cr = registry.db.cursor()
|
||||
r = 1
|
||||
try:
|
||||
cr.execute("select nextval('base_cache_signaling')")
|
||||
r = cr.fetchone()[0]
|
||||
finally:
|
||||
cr.close()
|
||||
registry.base_cache_signaling_sequence = r
|
||||
registry.reset_any_cache_cleared()
|
||||
|
||||
@classmethod
|
||||
def signal_registry_change(cls, db_name):
|
||||
if openerp.multi_process and db_name in cls.registries:
|
||||
registry = cls.get(db_name, pooljobs=False)
|
||||
cr = registry.db.cursor()
|
||||
r = 1
|
||||
try:
|
||||
cr.execute("select nextval('base_registry_signaling')")
|
||||
r = cr.fetchone()[0]
|
||||
finally:
|
||||
cr.close()
|
||||
registry.base_registry_signaling_sequence = r
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
+7
-87
@@ -20,6 +20,9 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
#.apidoc title: Common Services: netsvc
|
||||
#.apidoc module-mods: member-order: bysource
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import logging.handlers
|
||||
@@ -40,6 +43,7 @@ import openerp
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def close_socket(sock):
|
||||
""" Closes a socket instance cleanly
|
||||
|
||||
@@ -58,18 +62,14 @@ def close_socket(sock):
|
||||
raise
|
||||
sock.close()
|
||||
|
||||
|
||||
#.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.
|
||||
raise openerp.osv.osv.except_osv(description, details)
|
||||
|
||||
class Service(object):
|
||||
""" Base class for *Local* services
|
||||
|
||||
Functionality here is trusted, no authentication.
|
||||
""" Base class for Local services
|
||||
Functionality here is trusted, no authentication.
|
||||
Workflow engine and reports subclass this.
|
||||
"""
|
||||
_services = {}
|
||||
def __init__(self, name):
|
||||
@@ -145,7 +145,6 @@ class ColoredFormatter(DBFormatter):
|
||||
record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname)
|
||||
return DBFormatter.format(self, record)
|
||||
|
||||
|
||||
def init_logger():
|
||||
from tools.translate import resetlocale
|
||||
resetlocale()
|
||||
@@ -246,85 +245,6 @@ def init_alternative_logger():
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
class Server:
|
||||
""" Generic interface for all servers with an event loop etc.
|
||||
Override this to impement http, net-rpc etc. servers.
|
||||
|
||||
Servers here must have threaded behaviour. start() must not block,
|
||||
there is no run().
|
||||
"""
|
||||
__is_started = False
|
||||
__servers = []
|
||||
__starter_threads = []
|
||||
|
||||
# we don't want blocking server calls (think select()) to
|
||||
# wait forever and possibly prevent exiting the process,
|
||||
# but instead we want a form of polling/busy_wait pattern, where
|
||||
# _server_timeout should be used as the default timeout for
|
||||
# all I/O blocking operations
|
||||
_busywait_timeout = 0.5
|
||||
|
||||
def __init__(self):
|
||||
Server.__servers.append(self)
|
||||
if Server.__is_started:
|
||||
# raise Exception('All instances of servers must be inited before the startAll()')
|
||||
# Since the startAll() won't be called again, allow this server to
|
||||
# init and then start it after 1sec (hopefully). Register that
|
||||
# timer thread in a list, so that we can abort the start if quitAll
|
||||
# is called in the meantime
|
||||
t = threading.Timer(1.0, self._late_start)
|
||||
t.name = 'Late start timer for %s' % str(self.__class__)
|
||||
Server.__starter_threads.append(t)
|
||||
t.start()
|
||||
|
||||
def start(self):
|
||||
_logger.debug("called stub Server.start")
|
||||
|
||||
def _late_start(self):
|
||||
self.start()
|
||||
for thr in Server.__starter_threads:
|
||||
if thr.finished.is_set():
|
||||
Server.__starter_threads.remove(thr)
|
||||
|
||||
def stop(self):
|
||||
_logger.debug("called stub Server.stop")
|
||||
|
||||
def stats(self):
|
||||
""" This function should return statistics about the server """
|
||||
return "%s: No statistics" % str(self.__class__)
|
||||
|
||||
@classmethod
|
||||
def startAll(cls):
|
||||
if cls.__is_started:
|
||||
return
|
||||
_logger.info("Starting %d services" % len(cls.__servers))
|
||||
for srv in cls.__servers:
|
||||
srv.start()
|
||||
cls.__is_started = True
|
||||
|
||||
@classmethod
|
||||
def quitAll(cls):
|
||||
if not cls.__is_started:
|
||||
return
|
||||
_logger.info("Stopping %d services" % len(cls.__servers))
|
||||
for thr in cls.__starter_threads:
|
||||
if not thr.finished.is_set():
|
||||
thr.cancel()
|
||||
cls.__starter_threads.remove(thr)
|
||||
|
||||
for srv in cls.__servers:
|
||||
srv.stop()
|
||||
cls.__is_started = False
|
||||
|
||||
@classmethod
|
||||
def allStats(cls):
|
||||
res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
|
||||
res.extend(srv.stats() for srv in cls.__servers)
|
||||
return '\n'.join(res)
|
||||
|
||||
def _close_socket(self):
|
||||
close_socket(self.socket)
|
||||
|
||||
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...
|
||||
|
||||
+665
-278
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,9 @@
|
||||
Fields Attributes:
|
||||
* _classic_read: is a classic sql fields
|
||||
* _type : field type
|
||||
* _auto_join: for one2many and many2one fields, tells whether select
|
||||
queries will join the relational table instead of replacing the
|
||||
field condition by an equivalent-one based on a search.
|
||||
* readonly
|
||||
* required
|
||||
* size
|
||||
@@ -67,6 +70,7 @@ class _column(object):
|
||||
"""
|
||||
_classic_read = True
|
||||
_classic_write = True
|
||||
_auto_join = False
|
||||
_prefetch = True
|
||||
_properties = False
|
||||
_type = 'unknown'
|
||||
@@ -163,7 +167,7 @@ class boolean(_column):
|
||||
_logger.debug(
|
||||
"required=True is deprecated: making a boolean field"
|
||||
" `required` has no effect, as NULL values are "
|
||||
"automatically turned into False.")
|
||||
"automatically turned into False. args: %r",args)
|
||||
|
||||
class integer(_column):
|
||||
_type = 'integer'
|
||||
@@ -427,9 +431,10 @@ class many2one(_column):
|
||||
_symbol_f = lambda x: x or None
|
||||
_symbol_set = (_symbol_c, _symbol_f)
|
||||
|
||||
def __init__(self, obj, string='unknown', **args):
|
||||
def __init__(self, obj, string='unknown', auto_join=False, **args):
|
||||
_column.__init__(self, string=string, **args)
|
||||
self._obj = obj
|
||||
self._auto_join = auto_join
|
||||
|
||||
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
|
||||
if context is None:
|
||||
@@ -496,11 +501,12 @@ class one2many(_column):
|
||||
_prefetch = False
|
||||
_type = 'one2many'
|
||||
|
||||
def __init__(self, obj, fields_id, string='unknown', limit=None, **args):
|
||||
def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
|
||||
_column.__init__(self, string=string, **args)
|
||||
self._obj = obj
|
||||
self._fields_id = fields_id
|
||||
self._limit = limit
|
||||
self._auto_join = auto_join
|
||||
#one2many can't be used as condition for defaults
|
||||
assert(self.change_default != True)
|
||||
|
||||
|
||||
+97
-81
@@ -58,7 +58,6 @@ import types
|
||||
|
||||
import psycopg2
|
||||
from lxml import etree
|
||||
import warnings
|
||||
|
||||
import fields
|
||||
import openerp
|
||||
@@ -866,10 +865,6 @@ class BaseModel(object):
|
||||
parent_names = [parent_names]
|
||||
else:
|
||||
name = cls._name
|
||||
# for res.parnter.address compatiblity, should be remove in v7
|
||||
if 'res.partner.address' in parent_names:
|
||||
parent_names.pop(parent_names.index('res.partner.address'))
|
||||
parent_names.append('res.partner')
|
||||
if not name:
|
||||
raise TypeError('_name is mandatory in case of multiple inheritance')
|
||||
|
||||
@@ -1019,45 +1014,50 @@ class BaseModel(object):
|
||||
|
||||
# Load manual fields
|
||||
|
||||
cr.execute("SELECT id FROM ir_model_fields WHERE name=%s AND model=%s", ('state', 'ir.model.fields'))
|
||||
if cr.fetchone():
|
||||
# Check the query is already done for all modules of if we need to
|
||||
# do it ourselves.
|
||||
if self.pool.fields_by_model is not None:
|
||||
manual_fields = self.pool.fields_by_model.get(self._name, [])
|
||||
else:
|
||||
cr.execute('SELECT * FROM ir_model_fields WHERE model=%s AND state=%s', (self._name, 'manual'))
|
||||
for field in cr.dictfetchall():
|
||||
if field['name'] in self._columns:
|
||||
continue
|
||||
attrs = {
|
||||
'string': field['field_description'],
|
||||
'required': bool(field['required']),
|
||||
'readonly': bool(field['readonly']),
|
||||
'domain': eval(field['domain']) if field['domain'] else None,
|
||||
'size': field['size'],
|
||||
'ondelete': field['on_delete'],
|
||||
'translate': (field['translate']),
|
||||
'manual': True,
|
||||
#'select': int(field['select_level'])
|
||||
}
|
||||
manual_fields = cr.dictfetchall()
|
||||
for field in manual_fields:
|
||||
if field['name'] in self._columns:
|
||||
continue
|
||||
attrs = {
|
||||
'string': field['field_description'],
|
||||
'required': bool(field['required']),
|
||||
'readonly': bool(field['readonly']),
|
||||
'domain': eval(field['domain']) if field['domain'] else None,
|
||||
'size': field['size'],
|
||||
'ondelete': field['on_delete'],
|
||||
'translate': (field['translate']),
|
||||
'manual': True,
|
||||
#'select': int(field['select_level'])
|
||||
}
|
||||
|
||||
if field['serialization_field_id']:
|
||||
cr.execute('SELECT name FROM ir_model_fields WHERE id=%s', (field['serialization_field_id'],))
|
||||
attrs.update({'serialization_field': cr.fetchone()[0], 'type': field['ttype']})
|
||||
if field['ttype'] in ['many2one', 'one2many', 'many2many']:
|
||||
attrs.update({'relation': field['relation']})
|
||||
self._columns[field['name']] = fields.sparse(**attrs)
|
||||
elif field['ttype'] == 'selection':
|
||||
self._columns[field['name']] = fields.selection(eval(field['selection']), **attrs)
|
||||
elif field['ttype'] == 'reference':
|
||||
self._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs)
|
||||
elif field['ttype'] == 'many2one':
|
||||
self._columns[field['name']] = fields.many2one(field['relation'], **attrs)
|
||||
elif field['ttype'] == 'one2many':
|
||||
self._columns[field['name']] = fields.one2many(field['relation'], field['relation_field'], **attrs)
|
||||
elif field['ttype'] == 'many2many':
|
||||
_rel1 = field['relation'].replace('.', '_')
|
||||
_rel2 = field['model'].replace('.', '_')
|
||||
_rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
|
||||
self._columns[field['name']] = fields.many2many(field['relation'], _rel_name, 'id1', 'id2', **attrs)
|
||||
else:
|
||||
self._columns[field['name']] = getattr(fields, field['ttype'])(**attrs)
|
||||
|
||||
if field['serialization_field_id']:
|
||||
cr.execute('SELECT name FROM ir_model_fields WHERE id=%s', (field['serialization_field_id'],))
|
||||
attrs.update({'serialization_field': cr.fetchone()[0], 'type': field['ttype']})
|
||||
if field['ttype'] in ['many2one', 'one2many', 'many2many']:
|
||||
attrs.update({'relation': field['relation']})
|
||||
self._columns[field['name']] = fields.sparse(**attrs)
|
||||
elif field['ttype'] == 'selection':
|
||||
self._columns[field['name']] = fields.selection(eval(field['selection']), **attrs)
|
||||
elif field['ttype'] == 'reference':
|
||||
self._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs)
|
||||
elif field['ttype'] == 'many2one':
|
||||
self._columns[field['name']] = fields.many2one(field['relation'], **attrs)
|
||||
elif field['ttype'] == 'one2many':
|
||||
self._columns[field['name']] = fields.one2many(field['relation'], field['relation_field'], **attrs)
|
||||
elif field['ttype'] == 'many2many':
|
||||
_rel1 = field['relation'].replace('.', '_')
|
||||
_rel2 = field['model'].replace('.', '_')
|
||||
_rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
|
||||
self._columns[field['name']] = fields.many2many(field['relation'], _rel_name, 'id1', 'id2', **attrs)
|
||||
else:
|
||||
self._columns[field['name']] = getattr(fields, field['ttype'])(**attrs)
|
||||
self._inherits_check()
|
||||
self._inherits_reload()
|
||||
if not self._sequence:
|
||||
@@ -2509,6 +2509,7 @@ class BaseModel(object):
|
||||
try:
|
||||
getattr(self, '_ormcache')
|
||||
self._ormcache = {}
|
||||
self.pool._any_cache_cleared = True
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@@ -2720,22 +2721,17 @@ class BaseModel(object):
|
||||
|
||||
return result
|
||||
|
||||
def _inherits_join_add(self, current_table, parent_model_name, query):
|
||||
def _inherits_join_add(self, current_model, parent_model_name, query):
|
||||
"""
|
||||
Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates)
|
||||
:param current_table: current model object
|
||||
:param current_model: current model object
|
||||
:param parent_model_name: name of the parent model for which the clauses should be added
|
||||
:param query: query object on which the JOIN should be added
|
||||
"""
|
||||
inherits_field = current_table._inherits[parent_model_name]
|
||||
inherits_field = current_model._inherits[parent_model_name]
|
||||
parent_model = self.pool.get(parent_model_name)
|
||||
parent_table_name = parent_model._table
|
||||
quoted_parent_table_name = '"%s"' % parent_table_name
|
||||
if quoted_parent_table_name not in query.tables:
|
||||
query.tables.append(quoted_parent_table_name)
|
||||
query.where_clause.append('(%s.%s = %s.id)' % (current_table._table, inherits_field, parent_table_name))
|
||||
|
||||
|
||||
parent_alias, parent_alias_statement = query.add_join((current_model._table, parent_model._table, inherits_field, 'id', inherits_field), implicit=True)
|
||||
return parent_alias
|
||||
|
||||
def _inherits_join_calc(self, field, query):
|
||||
"""
|
||||
@@ -2747,12 +2743,13 @@ class BaseModel(object):
|
||||
:return: qualified name of field, to be used in SELECT clause
|
||||
"""
|
||||
current_table = self
|
||||
parent_alias = '"%s"' % current_table._table
|
||||
while field in current_table._inherit_fields and not field in current_table._columns:
|
||||
parent_model_name = current_table._inherit_fields[field][0]
|
||||
parent_table = self.pool.get(parent_model_name)
|
||||
self._inherits_join_add(current_table, parent_model_name, query)
|
||||
parent_alias = self._inherits_join_add(current_table, parent_model_name, query)
|
||||
current_table = parent_table
|
||||
return '"%s".%s' % (current_table._table, field)
|
||||
return '%s."%s"' % (parent_alias, field)
|
||||
|
||||
def _parent_store_compute(self, cr):
|
||||
if not self._parent_store:
|
||||
@@ -3234,7 +3231,7 @@ class BaseModel(object):
|
||||
|
||||
|
||||
def _create_table(self, cr):
|
||||
cr.execute('CREATE TABLE "%s" (id SERIAL NOT NULL, PRIMARY KEY(id)) WITHOUT OIDS' % (self._table,))
|
||||
cr.execute('CREATE TABLE "%s" (id SERIAL NOT NULL, PRIMARY KEY(id))' % (self._table,))
|
||||
cr.execute(("COMMENT ON TABLE \"%s\" IS %%s" % self._table), (self._description,))
|
||||
_schema.debug("Table '%s': created", self._table)
|
||||
|
||||
@@ -3266,8 +3263,8 @@ class BaseModel(object):
|
||||
elif not self._columns['parent_right'].select:
|
||||
_logger.error('parent_right column on object %s must be indexed! Add select=1 to the field definition)',
|
||||
self._table)
|
||||
if self._columns[self._parent_name].ondelete != 'cascade':
|
||||
_logger.error("The column %s on object %s must be set as ondelete='cascade'",
|
||||
if self._columns[self._parent_name].ondelete not in ('cascade', 'restrict'):
|
||||
_logger.error("The column %s on object %s must be set as ondelete='cascade' or 'restrict'",
|
||||
self._parent_name, self._name)
|
||||
|
||||
cr.commit()
|
||||
@@ -3318,7 +3315,7 @@ class BaseModel(object):
|
||||
raise except_orm('Programming Error', ('Many2Many destination model does not exist: `%s`') % (f._obj,))
|
||||
dest_model = self.pool.get(f._obj)
|
||||
ref = dest_model._table
|
||||
cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s")) WITH OIDS' % (m2m_tbl, col1, col2, col1, col2))
|
||||
cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (m2m_tbl, col1, col2, col1, col2))
|
||||
# create foreign key references with ondelete=cascade, unless the targets are SQL views
|
||||
cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (ref,))
|
||||
if not cr.fetchall():
|
||||
@@ -3449,8 +3446,8 @@ class BaseModel(object):
|
||||
_logger.info('Missing many2one field definition for _inherits reference "%s" in "%s", using default one.', field_name, self._name)
|
||||
self._columns[field_name] = fields.many2one(table, string="Automatically created field to link to parent %s" % table,
|
||||
required=True, ondelete="cascade")
|
||||
elif not self._columns[field_name].required or self._columns[field_name].ondelete.lower() != "cascade":
|
||||
_logger.warning('Field definition for _inherits reference "%s" in "%s" must be marked as "required" with ondelete="cascade", forcing it.', field_name, self._name)
|
||||
elif not self._columns[field_name].required or self._columns[field_name].ondelete.lower() not in ("cascade", "restrict"):
|
||||
_logger.warning('Field definition for _inherits reference "%s" in "%s" must be marked as "required" with ondelete="cascade" or "restrict", forcing it to required + cascade.', field_name, self._name)
|
||||
self._columns[field_name].required = True
|
||||
self._columns[field_name].ondelete = "cascade"
|
||||
|
||||
@@ -3632,15 +3629,16 @@ class BaseModel(object):
|
||||
else:
|
||||
res = map(lambda x: {'id': x}, ids)
|
||||
|
||||
for f in fields_pre:
|
||||
if f == self.CONCURRENCY_CHECK_FIELD:
|
||||
continue
|
||||
if self._columns[f].translate:
|
||||
ids = [x['id'] for x in res]
|
||||
#TODO: optimize out of this loop
|
||||
res_trans = self.pool.get('ir.translation')._get_ids(cr, user, self._name+','+f, 'model', context.get('lang', False) or 'en_US', ids)
|
||||
for r in res:
|
||||
r[f] = res_trans.get(r['id'], False) or r[f]
|
||||
if context.get('lang'):
|
||||
for f in fields_pre:
|
||||
if f == self.CONCURRENCY_CHECK_FIELD:
|
||||
continue
|
||||
if self._columns[f].translate:
|
||||
ids = [x['id'] for x in res]
|
||||
#TODO: optimize out of this loop
|
||||
res_trans = self.pool.get('ir.translation')._get_ids(cr, user, self._name+','+f, 'model', context['lang'], ids)
|
||||
for r in res:
|
||||
r[f] = res_trans.get(r['id'], False) or r[f]
|
||||
|
||||
for table in self._inherits:
|
||||
col = self._inherits[table]
|
||||
@@ -4661,11 +4659,27 @@ class BaseModel(object):
|
||||
:param query: the current query object
|
||||
"""
|
||||
def apply_rule(added_clause, added_params, added_tables, parent_model=None, child_object=None):
|
||||
""" :param string parent_model: string of the parent model
|
||||
:param model child_object: model object, base of the rule application
|
||||
"""
|
||||
if added_clause:
|
||||
if parent_model and child_object:
|
||||
# as inherited rules are being applied, we need to add the missing JOIN
|
||||
# to reach the parent table (if it was not JOINed yet in the query)
|
||||
child_object._inherits_join_add(child_object, parent_model, query)
|
||||
parent_alias = child_object._inherits_join_add(child_object, parent_model, query)
|
||||
# inherited rules are applied on the external table -> need to get the alias and replace
|
||||
parent_table = self.pool.get(parent_model)._table
|
||||
added_clause = [clause.replace('"%s"' % parent_table, '"%s"' % parent_alias) for clause in added_clause]
|
||||
# change references to parent_table to parent_alias, because we now use the alias to refer to the table
|
||||
new_tables = []
|
||||
for table in added_tables:
|
||||
# table is just a table name -> switch to the full alias
|
||||
if table == '"%s"' % (parent_table):
|
||||
new_tables.append('"%s" as "%s"' % (parent_table, parent_alias))
|
||||
# table is already a full statement -> replace reference to the table to its alias, is correct with the way aliases are generated
|
||||
else:
|
||||
new_tables.append(table.replace('"%s"' % parent_table, '"%s"' % parent_alias))
|
||||
added_tables = new_tables
|
||||
query.where_clause += added_clause
|
||||
query.where_clause_params += added_params
|
||||
for table in added_tables:
|
||||
@@ -4676,12 +4690,14 @@ class BaseModel(object):
|
||||
|
||||
# apply main rules on the object
|
||||
rule_obj = self.pool.get('ir.rule')
|
||||
apply_rule(*rule_obj.domain_get(cr, uid, self._name, mode, context=context))
|
||||
rule_where_clause, rule_where_clause_params, rule_tables = rule_obj.domain_get(cr, uid, self._name, mode, context=context)
|
||||
apply_rule(rule_where_clause, rule_where_clause_params, rule_tables)
|
||||
|
||||
# apply ir.rules from the parents (through _inherits)
|
||||
for inherited_model in self._inherits:
|
||||
kwargs = dict(parent_model=inherited_model, child_object=self) #workaround for python2.5
|
||||
apply_rule(*rule_obj.domain_get(cr, uid, inherited_model, mode, context=context), **kwargs)
|
||||
rule_where_clause, rule_where_clause_params, rule_tables = rule_obj.domain_get(cr, uid, inherited_model, mode, context=context)
|
||||
apply_rule(rule_where_clause, rule_where_clause_params, rule_tables,
|
||||
parent_model=inherited_model, child_object=self)
|
||||
|
||||
def _generate_m2o_order_by(self, order_field, query):
|
||||
"""
|
||||
@@ -4716,17 +4732,16 @@ class BaseModel(object):
|
||||
# extract the field names, to be able to qualify them and add desc/asc
|
||||
m2o_order_list = []
|
||||
for order_part in m2o_order.split(","):
|
||||
m2o_order_list.append(order_part.strip().split(" ",1)[0].strip())
|
||||
m2o_order_list.append(order_part.strip().split(" ", 1)[0].strip())
|
||||
m2o_order = m2o_order_list
|
||||
|
||||
# Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here
|
||||
# as we don't want to exclude results that have NULL values for the m2o
|
||||
src_table, src_field = qualified_field.replace('"','').split('.', 1)
|
||||
query.join((src_table, dest_model._table, src_field, 'id'), outer=True)
|
||||
qualify = lambda field: '"%s"."%s"' % (dest_model._table, field)
|
||||
src_table, src_field = qualified_field.replace('"', '').split('.', 1)
|
||||
dst_alias, dst_alias_statement = query.add_join((src_table, dest_model._table, src_field, 'id', src_field), implicit=False, outer=True)
|
||||
qualify = lambda field: '"%s"."%s"' % (dst_alias, field)
|
||||
return map(qualify, m2o_order) if isinstance(m2o_order, list) else qualify(m2o_order)
|
||||
|
||||
|
||||
def _generate_order_by(self, order_spec, query):
|
||||
"""
|
||||
Attempt to consruct an appropriate ORDER BY clause based on order_spec, which must be
|
||||
@@ -4734,7 +4749,8 @@ class BaseModel(object):
|
||||
|
||||
:raise" except_orm in case order_spec is malformed
|
||||
"""
|
||||
order_by_clause = self._order
|
||||
order_by_clause = ''
|
||||
order_spec = order_spec or self._order
|
||||
if order_spec:
|
||||
order_by_elements = []
|
||||
self._check_qorder(order_spec)
|
||||
@@ -4752,7 +4768,7 @@ class BaseModel(object):
|
||||
elif order_column._type == 'many2one':
|
||||
inner_clause = self._generate_m2o_order_by(order_field, query)
|
||||
else:
|
||||
continue # ignore non-readable or "non-joinable" fields
|
||||
continue # ignore non-readable or "non-joinable" fields
|
||||
elif order_field in self._inherit_fields:
|
||||
parent_obj = self.pool.get(self._inherit_fields[order_field][3])
|
||||
order_column = parent_obj._columns[order_field]
|
||||
@@ -4761,7 +4777,7 @@ class BaseModel(object):
|
||||
elif order_column._type == 'many2one':
|
||||
inner_clause = self._generate_m2o_order_by(order_field, query)
|
||||
else:
|
||||
continue # ignore non-readable or "non-joinable" fields
|
||||
continue # ignore non-readable or "non-joinable" fields
|
||||
if inner_clause:
|
||||
if isinstance(inner_clause, list):
|
||||
for clause in inner_clause:
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
from functools import wraps
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from psycopg2 import IntegrityError, errorcodes
|
||||
|
||||
import orm
|
||||
@@ -168,6 +170,7 @@ class object_proxy(object):
|
||||
|
||||
@check
|
||||
def execute(self, db, uid, obj, method, *args, **kw):
|
||||
threading.currentThread().dbname = db
|
||||
cr = pooler.get_db(db).cursor()
|
||||
try:
|
||||
try:
|
||||
|
||||
+61
-25
@@ -21,6 +21,7 @@
|
||||
|
||||
#.apidoc title: Query object
|
||||
|
||||
|
||||
def _quote(to_quote):
|
||||
if '"' not in to_quote:
|
||||
return '"%s"' % to_quote
|
||||
@@ -67,15 +68,35 @@ class Query(object):
|
||||
# LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col")
|
||||
self.joins = joins or {}
|
||||
|
||||
def join(self, connection, outer=False):
|
||||
"""Adds the JOIN specified in ``connection``.
|
||||
def _get_table_aliases(self):
|
||||
from openerp.osv.expression import get_alias_from_query
|
||||
return [get_alias_from_query(from_statement)[1] for from_statement in self.tables]
|
||||
|
||||
:param connection: a tuple ``(lhs, table, lhs_col, col)``.
|
||||
The join corresponds to the SQL equivalent of::
|
||||
def _get_alias_mapping(self):
|
||||
from openerp.osv.expression import get_alias_from_query
|
||||
mapping = {}
|
||||
for table in self.tables:
|
||||
alias, statement = get_alias_from_query(table)
|
||||
mapping[statement] = table
|
||||
return mapping
|
||||
|
||||
(lhs.lhs_col = table.col)
|
||||
def add_join(self, connection, implicit=True, outer=False):
|
||||
""" Join a destination table to the current table.
|
||||
|
||||
:param outer: True if a LEFT OUTER JOIN should be used, if possible
|
||||
:param implicit: False if the join is an explicit join. This allows
|
||||
to fall back on the previous implementation of ``join`` before
|
||||
OpenERP 7.0. It therefore adds the JOIN specified in ``connection``
|
||||
If True, the join is done implicitely, by adding the table alias
|
||||
in the from clause and the join condition in the where clause
|
||||
of the query. Implicit joins do not handle outer parameter.
|
||||
:param connection: a tuple ``(lhs, table, lhs_col, col, link)``.
|
||||
The join corresponds to the SQL equivalent of::
|
||||
|
||||
(lhs.lhs_col = table.col)
|
||||
|
||||
Note that all connection elements are strings. Please refer to expression.py for more details about joins.
|
||||
|
||||
:param outer: True if a LEFT OUTER JOIN should be used, if possible
|
||||
(no promotion to OUTER JOIN is supported in case the JOIN
|
||||
was already present in the query, as for the moment
|
||||
implicit INNER JOINs are only connected from NON-NULL
|
||||
@@ -83,38 +104,53 @@ class Query(object):
|
||||
``_inherits`` or when a domain criterion explicitly
|
||||
adds filtering)
|
||||
"""
|
||||
(lhs, table, lhs_col, col) = connection
|
||||
lhs = _quote(lhs)
|
||||
table = _quote(table)
|
||||
assert lhs in self.tables, "Left-hand-side table must already be part of the query!"
|
||||
if table in self.tables:
|
||||
# already joined, must ignore (promotion to outer and multiple joins not supported yet)
|
||||
pass
|
||||
from openerp.osv.expression import generate_table_alias
|
||||
(lhs, table, lhs_col, col, link) = connection
|
||||
alias, alias_statement = generate_table_alias(lhs, [(table, link)])
|
||||
|
||||
if implicit:
|
||||
if alias_statement not in self.tables:
|
||||
self.tables.append(alias_statement)
|
||||
condition = '("%s"."%s" = "%s"."%s")' % (lhs, lhs_col, alias, col)
|
||||
self.where_clause.append(condition)
|
||||
else:
|
||||
# already joined
|
||||
pass
|
||||
return alias, alias_statement
|
||||
else:
|
||||
# add JOIN
|
||||
self.tables.append(table)
|
||||
self.joins.setdefault(lhs, []).append((table, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN'))
|
||||
return self
|
||||
aliases = self._get_table_aliases()
|
||||
assert lhs in aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self.tables))
|
||||
if alias_statement in self.tables:
|
||||
# already joined, must ignore (promotion to outer and multiple joins not supported yet)
|
||||
pass
|
||||
else:
|
||||
# add JOIN
|
||||
self.tables.append(alias_statement)
|
||||
self.joins.setdefault(lhs, []).append((alias, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN'))
|
||||
return alias, alias_statement
|
||||
|
||||
def get_sql(self):
|
||||
"""Returns (query_from, query_where, query_params)"""
|
||||
""" Returns (query_from, query_where, query_params). """
|
||||
from openerp.osv.expression import get_alias_from_query
|
||||
query_from = ''
|
||||
tables_to_process = list(self.tables)
|
||||
alias_mapping = self._get_alias_mapping()
|
||||
|
||||
def add_joins_for_table(table, query_from):
|
||||
for (dest_table, lhs_col, col, join) in self.joins.get(table,[]):
|
||||
tables_to_process.remove(dest_table)
|
||||
query_from += ' %s %s ON (%s."%s" = %s."%s")' % \
|
||||
(join, dest_table, table, lhs_col, dest_table, col)
|
||||
for (dest_table, lhs_col, col, join) in self.joins.get(table, []):
|
||||
tables_to_process.remove(alias_mapping[dest_table])
|
||||
query_from += ' %s %s ON ("%s"."%s" = "%s"."%s")' % \
|
||||
(join, alias_mapping[dest_table], table, lhs_col, dest_table, col)
|
||||
query_from = add_joins_for_table(dest_table, query_from)
|
||||
return query_from
|
||||
|
||||
for table in tables_to_process:
|
||||
query_from += table
|
||||
if table in self.joins:
|
||||
query_from = add_joins_for_table(table, query_from)
|
||||
table_alias = get_alias_from_query(table)[1]
|
||||
if table_alias in self.joins:
|
||||
query_from = add_joins_for_table(table_alias, query_from)
|
||||
query_from += ','
|
||||
query_from = query_from[:-1] # drop last comma
|
||||
query_from = query_from[:-1] # drop last comma
|
||||
return (query_from, " AND ".join(self.where_clause), self.where_clause_params)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
+15
-19
@@ -24,15 +24,16 @@ import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import cron
|
||||
import netrpc_server
|
||||
import web_services
|
||||
import web_services
|
||||
import wsgi_server
|
||||
|
||||
import openerp.cron
|
||||
import openerp.modules
|
||||
import openerp.netsvc
|
||||
import openerp.osv
|
||||
import openerp.tools
|
||||
import openerp.service.wsgi_server
|
||||
|
||||
#.apidoc title: RPC Services
|
||||
|
||||
@@ -68,37 +69,32 @@ def start_internal():
|
||||
return
|
||||
openerp.netsvc.init_logger()
|
||||
openerp.modules.loading.open_openerp_namespace()
|
||||
|
||||
# Instantiate local services (this is a legacy design).
|
||||
openerp.osv.osv.start_object_proxy()
|
||||
# Export (for RPC) services.
|
||||
web_services.start_web_services()
|
||||
web_services.start_service()
|
||||
|
||||
load_server_wide_modules()
|
||||
start_internal_done = True
|
||||
|
||||
def start_services():
|
||||
""" Start all services including http, netrpc and cron """
|
||||
start_internal()
|
||||
|
||||
# Initialize the HTTP stack.
|
||||
netrpc_server.init_servers()
|
||||
|
||||
# Start the main cron thread.
|
||||
if openerp.conf.max_cron_threads:
|
||||
openerp.cron.start_master_thread()
|
||||
|
||||
# Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC).
|
||||
openerp.netsvc.Server.startAll()
|
||||
|
||||
# Initialize the NETRPC server.
|
||||
netrpc_server.start_service()
|
||||
# Start the WSGI server.
|
||||
openerp.service.wsgi_server.start_server()
|
||||
wsgi_server.start_service()
|
||||
# Start the main cron thread.
|
||||
cron.start_service()
|
||||
|
||||
def stop_services():
|
||||
""" Stop all services. """
|
||||
# stop scheduling new jobs; we will have to wait for the jobs to complete below
|
||||
openerp.cron.cancel_all()
|
||||
# stop services
|
||||
cron.stop_service()
|
||||
netrpc_server.stop_service()
|
||||
wsgi_server.stop_service()
|
||||
|
||||
openerp.netsvc.Server.quitAll()
|
||||
openerp.service.wsgi_server.stop_server()
|
||||
_logger.info("Initiating shutdown")
|
||||
_logger.info("Hit CTRL-C again or send a second signal to force the shutdown.")
|
||||
logging.shutdown()
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/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.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import openerp
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
SLEEP_INTERVAL = 60 # 1 min
|
||||
|
||||
def cron_runner(number):
|
||||
while True:
|
||||
time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style
|
||||
registries = openerp.modules.registry.RegistryManager.registries
|
||||
_logger.debug('cron%d polling for jobs', number)
|
||||
for db_name, registry in registries.items():
|
||||
while True and registry.cron:
|
||||
# acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
# TODO why isnt openerp.addons.base defined ?
|
||||
import sys
|
||||
base = sys.modules['addons.base']
|
||||
acquired = base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
if not acquired:
|
||||
break
|
||||
|
||||
def start_service():
|
||||
""" 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).
|
||||
|
||||
"""
|
||||
for i in range(openerp.tools.config['max_cron_threads']):
|
||||
def target():
|
||||
cron_runner(i)
|
||||
t = threading.Thread(target=target, name="openerp.service.cron.cron%d" % i)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
_logger.debug("cron%d started!" % i)
|
||||
|
||||
def stop_service():
|
||||
pass
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
@@ -176,6 +176,4 @@ class OpenERPAuthProvider(AuthProvider):
|
||||
self.auth_tries += 1
|
||||
raise AuthRequiredExc(atype='Basic', realm=self.realm)
|
||||
|
||||
#eof
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
#.apidoc title: NET-RPC Server
|
||||
|
||||
""" This file contains instance of the net-rpc server
|
||||
|
||||
|
||||
"""
|
||||
import logging
|
||||
import select
|
||||
@@ -38,6 +36,85 @@ import openerp.tools as tools
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class Server:
|
||||
""" Generic interface for all servers with an event loop etc.
|
||||
Override this to impement http, net-rpc etc. servers.
|
||||
|
||||
Servers here must have threaded behaviour. start() must not block,
|
||||
there is no run().
|
||||
"""
|
||||
__is_started = False
|
||||
__servers = []
|
||||
__starter_threads = []
|
||||
|
||||
# we don't want blocking server calls (think select()) to
|
||||
# wait forever and possibly prevent exiting the process,
|
||||
# but instead we want a form of polling/busy_wait pattern, where
|
||||
# _server_timeout should be used as the default timeout for
|
||||
# all I/O blocking operations
|
||||
_busywait_timeout = 0.5
|
||||
|
||||
def __init__(self):
|
||||
Server.__servers.append(self)
|
||||
if Server.__is_started:
|
||||
# raise Exception('All instances of servers must be inited before the startAll()')
|
||||
# Since the startAll() won't be called again, allow this server to
|
||||
# init and then start it after 1sec (hopefully). Register that
|
||||
# timer thread in a list, so that we can abort the start if quitAll
|
||||
# is called in the meantime
|
||||
t = threading.Timer(1.0, self._late_start)
|
||||
t.name = 'Late start timer for %s' % str(self.__class__)
|
||||
Server.__starter_threads.append(t)
|
||||
t.start()
|
||||
|
||||
def start(self):
|
||||
_logger.debug("called stub Server.start")
|
||||
|
||||
def _late_start(self):
|
||||
self.start()
|
||||
for thr in Server.__starter_threads:
|
||||
if thr.finished.is_set():
|
||||
Server.__starter_threads.remove(thr)
|
||||
|
||||
def stop(self):
|
||||
_logger.debug("called stub Server.stop")
|
||||
|
||||
def stats(self):
|
||||
""" This function should return statistics about the server """
|
||||
return "%s: No statistics" % str(self.__class__)
|
||||
|
||||
@classmethod
|
||||
def startAll(cls):
|
||||
if cls.__is_started:
|
||||
return
|
||||
_logger.info("Starting %d services" % len(cls.__servers))
|
||||
for srv in cls.__servers:
|
||||
srv.start()
|
||||
cls.__is_started = True
|
||||
|
||||
@classmethod
|
||||
def quitAll(cls):
|
||||
if not cls.__is_started:
|
||||
return
|
||||
_logger.info("Stopping %d services" % len(cls.__servers))
|
||||
for thr in cls.__starter_threads:
|
||||
if not thr.finished.is_set():
|
||||
thr.cancel()
|
||||
cls.__starter_threads.remove(thr)
|
||||
|
||||
for srv in cls.__servers:
|
||||
srv.stop()
|
||||
cls.__is_started = False
|
||||
|
||||
@classmethod
|
||||
def allStats(cls):
|
||||
res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
|
||||
res.extend(srv.stats() for srv in cls.__servers)
|
||||
return '\n'.join(res)
|
||||
|
||||
def _close_socket(self):
|
||||
netsvc.close_socket(self.socket)
|
||||
|
||||
class TinySocketClientThread(threading.Thread):
|
||||
def __init__(self, sock, threads):
|
||||
spn = sock and sock.getpeername()
|
||||
@@ -99,10 +176,10 @@ def netrpc_handle_exception_legacy(e):
|
||||
return 'AccessDenied ' + str(e)
|
||||
return openerp.tools.exception_to_unicode(e)
|
||||
|
||||
class TinySocketServerThread(threading.Thread,netsvc.Server):
|
||||
class TinySocketServerThread(threading.Thread,Server):
|
||||
def __init__(self, interface, port, secure=False):
|
||||
threading.Thread.__init__(self, name="NetRPCDaemon-%d"%port)
|
||||
netsvc.Server.__init__(self)
|
||||
Server.__init__(self)
|
||||
self.__port = port
|
||||
self.__interface = interface
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@@ -157,11 +234,12 @@ class TinySocketServerThread(threading.Thread,netsvc.Server):
|
||||
|
||||
netrpcd = None
|
||||
|
||||
def init_servers():
|
||||
def start_service():
|
||||
global netrpcd
|
||||
if tools.config.get('netrpc', False):
|
||||
netrpcd = TinySocketServerThread(
|
||||
tools.config.get('netrpc_interface', ''),
|
||||
int(tools.config.get('netrpc_port', 8070)))
|
||||
netrpcd = TinySocketServerThread(tools.config.get('netrpc_interface', ''), int(tools.config.get('netrpc_port', 8070)))
|
||||
|
||||
def stop_service():
|
||||
Server.quitAll()
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -159,6 +159,7 @@ class db(netsvc.ExportService):
|
||||
|
||||
def exp_duplicate_database(self, db_original_name, db_name):
|
||||
_logger.info('Duplicate database `%s` to `%s`.', db_original_name, db_name)
|
||||
sql_db.close_db(db_original_name)
|
||||
db = sql_db.db_connect('postgres')
|
||||
cr = db.cursor()
|
||||
try:
|
||||
@@ -597,68 +598,13 @@ class objects_proxy(netsvc.ExportService):
|
||||
raise NameError("Method not available %s" % method)
|
||||
security.check(db,uid,passwd)
|
||||
assert openerp.osv.osv.service, "The object_proxy class must be started with start_object_proxy."
|
||||
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
|
||||
fn = getattr(openerp.osv.osv.service, method)
|
||||
res = fn(db, uid, *params)
|
||||
openerp.modules.registry.RegistryManager.signal_caches_change(db)
|
||||
return res
|
||||
|
||||
|
||||
#
|
||||
# Wizard ID: 1
|
||||
# - None = end of wizard
|
||||
#
|
||||
# Wizard Type: 'form'
|
||||
# - form
|
||||
# - print
|
||||
#
|
||||
# Wizard datas: {}
|
||||
# TODO: change local request to OSE request/reply pattern
|
||||
#
|
||||
class wizard(netsvc.ExportService):
|
||||
def __init__(self, name='wizard'):
|
||||
netsvc.ExportService.__init__(self,name)
|
||||
self.id = 0
|
||||
self.wiz_datas = {}
|
||||
self.wiz_name = {}
|
||||
self.wiz_uid = {}
|
||||
|
||||
def dispatch(self, method, params):
|
||||
(db, uid, passwd ) = params[0:3]
|
||||
threading.current_thread().uid = uid
|
||||
params = params[3:]
|
||||
if method not in ['execute','create']:
|
||||
raise KeyError("Method not supported %s" % method)
|
||||
security.check(db,uid,passwd)
|
||||
fn = getattr(self, 'exp_'+method)
|
||||
res = fn(db, uid, *params)
|
||||
return res
|
||||
|
||||
def _execute(self, db, uid, wiz_id, datas, action, context):
|
||||
self.wiz_datas[wiz_id].update(datas)
|
||||
wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
|
||||
return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
|
||||
|
||||
def exp_create(self, db, uid, wiz_name, datas=None):
|
||||
if not datas:
|
||||
datas={}
|
||||
#FIXME: this is not thread-safe
|
||||
self.id += 1
|
||||
self.wiz_datas[self.id] = {}
|
||||
self.wiz_name[self.id] = wiz_name
|
||||
self.wiz_uid[self.id] = uid
|
||||
return self.id
|
||||
|
||||
def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
|
||||
if not context:
|
||||
context={}
|
||||
|
||||
if wiz_id in self.wiz_uid:
|
||||
if self.wiz_uid[wiz_id] == uid:
|
||||
return self._execute(db, uid, wiz_id, datas, action, context)
|
||||
else:
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
else:
|
||||
raise openerp.exceptions.Warning('Wizard not found.')
|
||||
|
||||
#
|
||||
# TODO: set a maximum report number per user to avoid DOS attacks
|
||||
#
|
||||
@@ -680,8 +626,10 @@ class report_spool(netsvc.ExportService):
|
||||
if method not in ['report', 'report_get', 'render_report']:
|
||||
raise KeyError("Method not supported %s" % method)
|
||||
security.check(db,uid,passwd)
|
||||
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
|
||||
fn = getattr(self, 'exp_' + method)
|
||||
res = fn(db, uid, *params)
|
||||
openerp.modules.registry.RegistryManager.signal_caches_change(db)
|
||||
return res
|
||||
|
||||
def exp_render_report(self, db, uid, object, ids, datas=None, context=None):
|
||||
@@ -793,13 +741,11 @@ class report_spool(netsvc.ExportService):
|
||||
raise Exception, 'ReportNotFound'
|
||||
|
||||
|
||||
def start_web_services():
|
||||
def start_service():
|
||||
db()
|
||||
common()
|
||||
objects_proxy()
|
||||
wizard()
|
||||
report_spool()
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
||||
+23
-10
@@ -16,6 +16,10 @@ import sys
|
||||
import time
|
||||
|
||||
import werkzeug.serving
|
||||
try:
|
||||
from setproctitle import setproctitle
|
||||
except ImportError:
|
||||
setproctitle = lambda x: None
|
||||
|
||||
import openerp
|
||||
import openerp.tools.config as config
|
||||
@@ -134,7 +138,7 @@ class Multicorn(object):
|
||||
def process_spawn(self):
|
||||
while len(self.workers_http) < self.population:
|
||||
self.worker_spawn(WorkerHTTP, self.workers_http)
|
||||
while len(self.workers_cron) < 1: # config option ?
|
||||
while len(self.workers_cron) < config['max_cron_threads']:
|
||||
self.worker_spawn(WorkerCron, self.workers_cron)
|
||||
|
||||
def sleep(self):
|
||||
@@ -189,8 +193,7 @@ class Multicorn(object):
|
||||
for pid in self.workers.keys():
|
||||
self.worker_kill(pid, signal.SIGTERM)
|
||||
self.socket.close()
|
||||
import __main__
|
||||
__main__.quit_signals_received = 1
|
||||
openerp.cli.server.quit_signals_received = 1
|
||||
|
||||
def run(self):
|
||||
self.start()
|
||||
@@ -252,7 +255,7 @@ class Worker(object):
|
||||
# Reset the worker if it consumes too much memory (e.g. caused by a memory leak).
|
||||
rss, vms = psutil.Process(os.getpid()).get_memory_info()
|
||||
if vms > config['limit_memory_soft']:
|
||||
_logger.info('Virtual memory consumption too high, rebooting the worker.')
|
||||
_logger.info('Worker (%d) virtual memory limit (%s) reached.', self.pid, vms)
|
||||
self.alive = False # Commit suicide after the request.
|
||||
|
||||
# VMS and RLIMIT_AS are the same thing: virtual memory, a.k.a. address space
|
||||
@@ -263,7 +266,8 @@ class Worker(object):
|
||||
r = resource.getrusage(resource.RUSAGE_SELF)
|
||||
cpu_time = r.ru_utime + r.ru_stime
|
||||
def time_expired(n, stack):
|
||||
_logger.info('CPU time limit exceeded.')
|
||||
_logger.info('Worker (%d) CPU time limit (%s) reached.', config['limit_time_cpu'])
|
||||
# We dont suicide in such case
|
||||
raise Exception('CPU time limit exceeded.')
|
||||
signal.signal(signal.SIGXCPU, time_expired)
|
||||
soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
|
||||
@@ -274,6 +278,7 @@ class Worker(object):
|
||||
|
||||
def start(self):
|
||||
self.pid = os.getpid()
|
||||
setproctitle('openerp: %s %s' % (self.__class__.__name__, self.pid))
|
||||
_logger.info("Worker %s (%s) alive", self.__class__.__name__, self.pid)
|
||||
# Reseed the random number generator
|
||||
random.seed()
|
||||
@@ -297,10 +302,10 @@ class Worker(object):
|
||||
self.multi.pipe_ping(self.watchdog_pipe)
|
||||
self.sleep()
|
||||
self.process_work()
|
||||
_logger.info("Worker (%s) exiting...",self.pid)
|
||||
_logger.info("Worker (%s) exiting. request_count: %s.", self.pid, self.request_count)
|
||||
self.stop()
|
||||
except Exception,e:
|
||||
_logger.exception("Worker (%s) Exception occured, exiting..."%self.pid)
|
||||
_logger.exception("Worker (%s) Exception occured, exiting..." % self.pid)
|
||||
# should we use 3 to abort everything ?
|
||||
sys.exit(1)
|
||||
|
||||
@@ -346,19 +351,27 @@ class WorkerBaseWSGIServer(werkzeug.serving.BaseWSGIServer):
|
||||
class WorkerCron(Worker):
|
||||
""" Cron workers """
|
||||
def sleep(self):
|
||||
time.sleep(60)
|
||||
interval = 60 + self.pid % 10 # chorus effect
|
||||
time.sleep(interval)
|
||||
|
||||
def process_work(self):
|
||||
_logger.debug("WorkerCron (%s) polling for jobs", self.pid)
|
||||
if config['db_name']:
|
||||
db_names = config['db_name'].split(',')
|
||||
else:
|
||||
db_names = openerp.netsvc.ExportService._services['db'].exp_list(True)
|
||||
for db_name in db_names:
|
||||
while True:
|
||||
# TODO Each job should be considered as one request in multiprocessing
|
||||
acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
# acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
# TODO why isnt openerp.addons.base defined ?
|
||||
import base
|
||||
acquired = base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
if not acquired:
|
||||
break
|
||||
# dont keep cursors in multi database mode
|
||||
if len(db_names) > 1:
|
||||
openerp.sql_db.close_db(db_name)
|
||||
# TODO Each job should be considered as one request instead of each db
|
||||
self.request_count += 1
|
||||
|
||||
def start(self):
|
||||
|
||||
@@ -428,14 +428,14 @@ def serve():
|
||||
_logger.info('HTTP service (werkzeug) running on %s:%s', interface, port)
|
||||
httpd.serve_forever()
|
||||
|
||||
def start_server():
|
||||
def start_service():
|
||||
""" Call serve() in its own thread.
|
||||
|
||||
The WSGI server can be shutdown with stop_server() below.
|
||||
"""
|
||||
threading.Thread(target=serve).start()
|
||||
|
||||
def stop_server():
|
||||
def stop_service():
|
||||
""" Initiate the shutdown of the WSGI server.
|
||||
|
||||
The server is supposed to have been started by start_server() above.
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2010 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/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from test_osv import *
|
||||
from test_translate import *
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
@@ -8,9 +8,10 @@ Tests can be explicitely added to the `fast_suite` or `checks` lists or not.
|
||||
See the :ref:`test-framework` section in the :ref:`features` list.
|
||||
"""
|
||||
|
||||
from . import test_expression, test_mail, test_ir_sequence, test_orm,\
|
||||
from . import test_expression, test_mail, test_ir_sequence, test_orm, \
|
||||
test_fields, test_basecase, \
|
||||
test_view_validation, test_uninstall, test_misc, test_db_cursor
|
||||
test_view_validation, test_uninstall, test_misc, test_db_cursor, \
|
||||
test_osv, test_translate
|
||||
from . import test_ir_filters
|
||||
|
||||
fast_suite = [
|
||||
@@ -27,6 +28,8 @@ checks = [
|
||||
test_basecase,
|
||||
test_view_validation,
|
||||
test_misc,
|
||||
test_osv,
|
||||
test_translate,
|
||||
]
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -2,11 +2,12 @@ import unittest2
|
||||
|
||||
import openerp
|
||||
|
||||
|
||||
class test_domain_normalization(unittest2.TestCase):
|
||||
def test_normalize_domain(self):
|
||||
expression = openerp.osv.expression
|
||||
norm_domain = domain = ['&',(1,'=',1),('a','=','b')]
|
||||
assert norm_domain == expression.normalize(domain), "Normalized domains should be left untouched"
|
||||
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"
|
||||
norm_domain = domain = ['&', (1, '=', 1), ('a', '=', 'b')]
|
||||
assert norm_domain == expression.normalize_domain(domain), "Normalized domains should be left untouched"
|
||||
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(domain), "Non-normalized domains should be properly normalized"
|
||||
|
||||
@@ -182,7 +182,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
def test_no_command(self):
|
||||
" empty list of commands yields an empty list of records "
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', [])
|
||||
self.cr, UID, 'child_ids', [])
|
||||
|
||||
self.assertEqual(results, [])
|
||||
|
||||
@@ -190,7 +190,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
" returns the VALUES dict as-is "
|
||||
values = [{'foo': 'bar'}, {'foo': 'baz'}, {'foo': 'baq'}]
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', map(CREATE, values))
|
||||
self.cr, UID, 'child_ids', map(CREATE, values))
|
||||
|
||||
self.assertEqual(results, values)
|
||||
|
||||
@@ -204,7 +204,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
commands = map(LINK_TO, ids)
|
||||
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', commands, ['name'])
|
||||
self.cr, UID, 'child_ids', commands, ['name'])
|
||||
|
||||
self.assertEqual(sorted_by_id(results), sorted_by_id([
|
||||
{'id': ids[0], 'name': 'foo'},
|
||||
@@ -221,7 +221,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
]
|
||||
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', ids, ['name'])
|
||||
self.cr, UID, 'child_ids', ids, ['name'])
|
||||
|
||||
self.assertEqual(sorted_by_id(results), sorted_by_id([
|
||||
{'id': ids[0], 'name': 'foo'},
|
||||
@@ -236,7 +236,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
id_baz = self.partner.create(self.cr, UID, {'name': 'baz', 'city': 'tag'})
|
||||
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', [
|
||||
self.cr, UID, 'child_ids', [
|
||||
LINK_TO(id_foo),
|
||||
UPDATE(id_bar, {'name': 'qux', 'city': 'tagtag'}),
|
||||
UPDATE(id_baz, {'name': 'quux'})
|
||||
@@ -258,7 +258,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
commands = [DELETE(ids[0]), DELETE(ids[1]), DELETE(ids[2])]
|
||||
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', commands, ['name'])
|
||||
self.cr, UID, 'child_ids', commands, ['name'])
|
||||
|
||||
self.assertEqual(results, [])
|
||||
|
||||
@@ -269,7 +269,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
]
|
||||
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', [
|
||||
self.cr, UID, 'child_ids', [
|
||||
CREATE({'name': 'foo'}),
|
||||
UPDATE(ids[0], {'name': 'bar'}),
|
||||
LINK_TO(ids[1]),
|
||||
@@ -300,7 +300,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
commands = map(lambda id: (4, id), ids)
|
||||
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', commands, ['name'])
|
||||
self.cr, UID, 'child_ids', commands, ['name'])
|
||||
|
||||
self.assertEqual(sorted_by_id(results), sorted_by_id([
|
||||
{'id': ids[0], 'name': 'foo'},
|
||||
@@ -311,7 +311,7 @@ class TestO2MSerialization(common.TransactionCase):
|
||||
def test_singleton_commands(self):
|
||||
"DELETE_ALL can appear as a singleton"
|
||||
results = self.partner.resolve_2many_commands(
|
||||
self.cr, UID, 'address', [DELETE_ALL()], ['name'])
|
||||
self.cr, UID, 'child_ids', [DELETE_ALL()], ['name'])
|
||||
|
||||
self.assertEqual(results, [])
|
||||
|
||||
|
||||
@@ -22,45 +22,45 @@
|
||||
import unittest
|
||||
from openerp.osv.query import Query
|
||||
|
||||
|
||||
class QueryTestCase(unittest.TestCase):
|
||||
|
||||
def test_basic_query(self):
|
||||
query = Query()
|
||||
query.tables.extend(['"product_product"','"product_template"'])
|
||||
query.tables.extend(['"product_product"', '"product_template"'])
|
||||
query.where_clause.append("product_product.template_id = product_template.id")
|
||||
query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
|
||||
query.join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join
|
||||
query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join
|
||||
query.add_join(("product_product", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # outer join
|
||||
self.assertEquals(query.get_sql()[0].strip(),
|
||||
""""product_product" LEFT JOIN "res_user" ON ("product_product"."user_id" = "res_user"."id"),"product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") """.strip())
|
||||
""""product_product" LEFT JOIN "res_user" as "product_product__user_id" ON ("product_product"."user_id" = "product_product__user_id"."id"),"product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") """.strip())
|
||||
self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
|
||||
|
||||
def test_query_chained_explicit_joins(self):
|
||||
query = Query()
|
||||
query.tables.extend(['"product_product"','"product_template"'])
|
||||
query.tables.extend(['"product_product"', '"product_template"'])
|
||||
query.where_clause.append("product_product.template_id = product_template.id")
|
||||
query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
|
||||
query.join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join
|
||||
query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join
|
||||
query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join
|
||||
self.assertEquals(query.get_sql()[0].strip(),
|
||||
""""product_product","product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") LEFT JOIN "res_user" ON ("product_category"."user_id" = "res_user"."id")""".strip())
|
||||
""""product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id")""".strip())
|
||||
self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
|
||||
|
||||
def test_mixed_query_chained_explicit_implicit_joins(self):
|
||||
query = Query()
|
||||
query.tables.extend(['"product_product"','"product_template"'])
|
||||
query.tables.extend(['"product_product"', '"product_template"'])
|
||||
query.where_clause.append("product_product.template_id = product_template.id")
|
||||
query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
|
||||
query.join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join
|
||||
query.add_join(("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False) # add normal join
|
||||
query.add_join(("product_template__categ_id", "res_user", "user_id", "id", "user_id"), implicit=False, outer=True) # CHAINED outer join
|
||||
query.tables.append('"account.account"')
|
||||
query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join
|
||||
query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join
|
||||
self.assertEquals(query.get_sql()[0].strip(),
|
||||
""""product_product","product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") LEFT JOIN "res_user" ON ("product_category"."user_id" = "res_user"."id"),"account.account" """.strip())
|
||||
""""product_product","product_template" JOIN "product_category" as "product_template__categ_id" ON ("product_template"."categ_id" = "product_template__categ_id"."id") LEFT JOIN "res_user" as "product_template__categ_id__user_id" ON ("product_template__categ_id"."user_id" = "product_template__categ_id__user_id"."id"),"account.account" """.strip())
|
||||
self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id""".strip())
|
||||
|
||||
|
||||
def test_raise_missing_lhs(self):
|
||||
query = Query()
|
||||
query.tables.append('"product_product"')
|
||||
self.assertRaises(AssertionError, query.join, ("product_template", "product_category", "categ_id", "id"), outer=False)
|
||||
self.assertRaises(AssertionError, query.add_join, ("product_template", "product_category", "categ_id", "id", "categ_id"), implicit=False, outer=False)
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
@@ -57,10 +57,12 @@ class ormcache(object):
|
||||
try:
|
||||
key = args[self.skiparg-2:]
|
||||
del d[key]
|
||||
self2.pool._any_cache_cleared = True
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
d.clear()
|
||||
self2.pool._any_cache_cleared = True
|
||||
|
||||
class ormcache_multi(ormcache):
|
||||
def __init__(self, skiparg=2, size=8192, multi=3):
|
||||
|
||||
+11
-12
@@ -146,8 +146,9 @@ class configmanager(object):
|
||||
help="specify the TCP IP address for the NETRPC protocol")
|
||||
group.add_option("--netrpc-port", dest="netrpc_port", my_default=8070,
|
||||
help="specify the TCP port for the NETRPC protocol", type="int")
|
||||
group.add_option("--no-netrpc", dest="netrpc", action="store_false", my_default=True,
|
||||
help="disable the NETRPC protocol")
|
||||
# Needed a few day for runbot and saas
|
||||
group.add_option("--no-netrpc", dest="netrpc", action="store_false", my_default=False, help="disable the NETRPC protocol")
|
||||
group.add_option("--netrpc", dest="netrpc", action="store_true", my_default=False, help="enable the NETRPC protocol")
|
||||
parser.add_option_group(group)
|
||||
|
||||
# WEB
|
||||
@@ -269,8 +270,8 @@ class configmanager(object):
|
||||
"osv_memory tables. This is a decimal value expressed in hours, "
|
||||
"and the default is 1 hour.",
|
||||
type="float")
|
||||
group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=4,
|
||||
help="Maximum number of threads processing concurrently cron jobs.",
|
||||
group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=2,
|
||||
help="Maximum number of threads processing concurrently cron jobs (default 2).",
|
||||
type="int")
|
||||
group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true",
|
||||
help="Use the unaccent function provided by the database when available.")
|
||||
@@ -282,19 +283,19 @@ class configmanager(object):
|
||||
help="Specify the number of workers, 0 disable prefork mode.",
|
||||
type="int")
|
||||
group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=640 * 1024 * 1024,
|
||||
help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request.",
|
||||
help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 640M).",
|
||||
type="int")
|
||||
group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=768 * 1024 * 1024,
|
||||
help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail.",
|
||||
help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 768M).",
|
||||
type="int")
|
||||
group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60,
|
||||
help="Maximum allowed CPU time per request.",
|
||||
help="Maximum allowed CPU time per request (default 60).",
|
||||
type="int")
|
||||
group.add_option("--limit-time-real", dest="limit_time_real", my_default=60,
|
||||
help="Maximum allowed Real time per request. ",
|
||||
group.add_option("--limit-time-real", dest="limit_time_real", my_default=120,
|
||||
help="Maximum allowed Real time per request (default 120).",
|
||||
type="int")
|
||||
group.add_option("--limit-request", dest="limit_request", my_default=8192,
|
||||
help="Maximum number of request to be processed per worker.",
|
||||
help="Maximum number of request to be processed per worker (default 8192).",
|
||||
type="int")
|
||||
parser.add_option_group(group)
|
||||
|
||||
@@ -479,8 +480,6 @@ class configmanager(object):
|
||||
if opt.save:
|
||||
self.save()
|
||||
|
||||
openerp.conf.max_cron_threads = self.options['max_cron_threads']
|
||||
|
||||
openerp.conf.addons_paths = self.options['addons_path'].split(',')
|
||||
if opt.server_wide_modules:
|
||||
openerp.conf.server_wide_modules = map(lambda m: m.strip(), opt.server_wide_modules.split(','))
|
||||
|
||||
+29
-19
@@ -23,7 +23,7 @@ import io
|
||||
import StringIO
|
||||
|
||||
from PIL import Image
|
||||
from PIL import ImageEnhance
|
||||
from PIL import ImageEnhance, ImageOps
|
||||
from random import random
|
||||
|
||||
# ----------------------------------------
|
||||
@@ -36,6 +36,7 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
|
||||
filled with transparent background. The image will not be stretched if
|
||||
smaller than the expected size.
|
||||
Steps of the resizing:
|
||||
- Compute width and height if not specified.
|
||||
- if avoid_if_small: if both image sizes are smaller than the requested
|
||||
sizes, the original image is returned. This is used to avoid adding
|
||||
transparent content around images that we do not want to alter but
|
||||
@@ -46,14 +47,14 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
|
||||
function. Aspect ratios are preserved when using it. Note that if the
|
||||
source image is smaller than the expected size, it will not be
|
||||
extended, but filled to match the size.
|
||||
- create a transparent background that will hold the final
|
||||
image.
|
||||
- past the thumbnail on the transparent background and center
|
||||
it.
|
||||
- create a transparent background that will hold the final image.
|
||||
- paste the thumbnail on the transparent background and center it.
|
||||
|
||||
:param base64_source: base64-encoded version of the source
|
||||
image; if False, returns False
|
||||
:param size: tuple(height, width)
|
||||
:param size: 2-tuple(width, height). A None value for any of width or
|
||||
height mean an automatically computed value based respectivelly
|
||||
on height or width of the source image.
|
||||
:param encoding: the output encoding
|
||||
:param filetype: the output filetype
|
||||
:param avoid_if_small: do not resize if image height and width
|
||||
@@ -61,22 +62,21 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
|
||||
"""
|
||||
if not base64_source:
|
||||
return False
|
||||
if size == (None, None):
|
||||
return base64_source
|
||||
image_stream = io.BytesIO(base64_source.decode(encoding))
|
||||
image = Image.open(image_stream)
|
||||
# check image size: do not create a thumbnail if avoiding smaller images
|
||||
if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]:
|
||||
return base64_source
|
||||
# create a thumbnail: will resize and keep ratios, then sharpen for better looking result
|
||||
image.thumbnail(size, Image.ANTIALIAS)
|
||||
sharpener = ImageEnhance.Sharpness(image.convert('RGB'))
|
||||
image = sharpener.enhance(2.0)
|
||||
# create a transparent image for background
|
||||
background = Image.new('RGBA', size, (255, 255, 255, 0))
|
||||
# past the resized image on the background
|
||||
background.paste(image, ((size[0] - image.size[0]) / 2, (size[1] - image.size[1]) / 2))
|
||||
# return an encoded image
|
||||
|
||||
asked_width, asked_height = size
|
||||
if asked_width is None:
|
||||
asked_width = int(image.size[0] * (float(asked_height) / image.size[1]))
|
||||
if asked_height is None:
|
||||
asked_height = int(image.size[1] * (float(asked_width) / image.size[0]))
|
||||
size = asked_width, asked_height
|
||||
if image.size <> size:
|
||||
image = ImageOps.fit(image, size, Image.ANTIALIAS)
|
||||
background_stream = StringIO.StringIO()
|
||||
background.save(background_stream, filetype)
|
||||
image.save(background_stream, filetype)
|
||||
return background_stream.getvalue().encode(encoding)
|
||||
|
||||
def image_resize_image_big(base64_source, size=(1204, 1204), encoding='base64', filetype='PNG', avoid_if_small=True):
|
||||
@@ -158,3 +158,13 @@ def image_get_resized_images(base64_source, return_big=False, return_medium=True
|
||||
return_dict[small_name] = image_resize_image_small(base64_source, avoid_if_small=avoid_resize_small)
|
||||
return return_dict
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
import sys
|
||||
|
||||
assert len(sys.argv)==3, 'Usage to Test: image.py SRC.png DEST.png'
|
||||
|
||||
img = file(sys.argv[1],'rb').read().encode('base64')
|
||||
new = image_resize_image(img, (128,100))
|
||||
file(sys.argv[2], 'wb').write(new.decode('base64'))
|
||||
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#Copyright (c) 2004-2005, CherryPy Team (team@cherrypy.org)
|
||||
#All rights reserved.
|
||||
#
|
||||
#Redistribution and use in source and binary forms, with or without
|
||||
#modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of the CherryPy Team nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
#DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
#FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
#DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
#OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
#OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# This is a backport of Python-2.4's threading.local() implementation
|
||||
|
||||
"""Thread-local objects
|
||||
|
||||
(Note that this module provides a Python version of thread
|
||||
threading.local class. Depending on the version of Python you're
|
||||
using, there may be a faster one available. You should always import
|
||||
the local class from threading.)
|
||||
|
||||
Thread-local objects support the management of thread-local data.
|
||||
If you have data that you want to be local to a thread, simply create
|
||||
a thread-local object and use its attributes:
|
||||
|
||||
>>> mydata = local()
|
||||
>>> mydata.number = 42
|
||||
>>> mydata.number
|
||||
42
|
||||
|
||||
You can also access the local-object's dictionary:
|
||||
|
||||
>>> mydata.__dict__
|
||||
{'number': 42}
|
||||
>>> mydata.__dict__.setdefault('widgets', [])
|
||||
[]
|
||||
>>> mydata.widgets
|
||||
[]
|
||||
|
||||
What's important about thread-local objects is that their data are
|
||||
local to a thread. If we access the data in a different thread:
|
||||
|
||||
>>> log = []
|
||||
>>> def f():
|
||||
... items = mydata.__dict__.items()
|
||||
... items.sort()
|
||||
... log.append(items)
|
||||
... mydata.number = 11
|
||||
... log.append(mydata.number)
|
||||
|
||||
>>> import threading
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
>>> log
|
||||
[[], 11]
|
||||
|
||||
we get different data. Furthermore, changes made in the other thread
|
||||
don't affect data seen in this thread:
|
||||
|
||||
>>> mydata.number
|
||||
42
|
||||
|
||||
Of course, values you get from a local object, including a __dict__
|
||||
attribute, are for whatever thread was current at the time the
|
||||
attribute was read. For that reason, you generally don't want to save
|
||||
these values across threads, as they apply only to the thread they
|
||||
came from.
|
||||
|
||||
You can create custom local objects by subclassing the local class:
|
||||
|
||||
>>> class MyLocal(local):
|
||||
... number = 2
|
||||
... initialized = False
|
||||
... def __init__(self, **kw):
|
||||
... if self.initialized:
|
||||
... raise SystemError('__init__ called too many times')
|
||||
... self.initialized = True
|
||||
... self.__dict__.update(kw)
|
||||
... def squared(self):
|
||||
... return self.number ** 2
|
||||
|
||||
This can be useful to support default values, methods and
|
||||
initialization. Note that if you define an __init__ method, it will be
|
||||
called each time the local object is used in a separate thread. This
|
||||
is necessary to initialize each thread's dictionary.
|
||||
|
||||
Now if we create a local object:
|
||||
|
||||
>>> mydata = MyLocal(color='red')
|
||||
|
||||
Now we have a default number:
|
||||
|
||||
>>> mydata.number
|
||||
2
|
||||
|
||||
an initial color:
|
||||
|
||||
>>> mydata.color
|
||||
'red'
|
||||
>>> del mydata.color
|
||||
|
||||
And a method that operates on the data:
|
||||
|
||||
>>> mydata.squared()
|
||||
4
|
||||
|
||||
As before, we can access the data in a separate thread:
|
||||
|
||||
>>> log = []
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
>>> log
|
||||
[[('color', 'red'), ('initialized', True)], 11]
|
||||
|
||||
without affecting this thread's data:
|
||||
|
||||
>>> mydata.number
|
||||
2
|
||||
>>> mydata.color
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: 'MyLocal' object has no attribute 'color'
|
||||
|
||||
Note that subclasses can define slots, but they are not thread
|
||||
local. They are shared across threads:
|
||||
|
||||
>>> class MyLocal(local):
|
||||
... __slots__ = 'number'
|
||||
|
||||
>>> mydata = MyLocal()
|
||||
>>> mydata.number = 42
|
||||
>>> mydata.color = 'red'
|
||||
|
||||
So, the separate thread:
|
||||
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
|
||||
affects what we see:
|
||||
|
||||
>>> mydata.number
|
||||
11
|
||||
|
||||
>>> del mydata
|
||||
"""
|
||||
|
||||
# Threading import is at end
|
||||
|
||||
class _localbase(object):
|
||||
__slots__ = '_local__key', '_local__args', '_local__lock'
|
||||
|
||||
def __new__(cls, *args, **kw):
|
||||
self = object.__new__(cls)
|
||||
key = '_local__key', 'thread.local.' + str(id(self))
|
||||
object.__setattr__(self, '_local__key', key)
|
||||
object.__setattr__(self, '_local__args', (args, kw))
|
||||
object.__setattr__(self, '_local__lock', RLock())
|
||||
|
||||
if args or kw and (cls.__init__ is object.__init__):
|
||||
raise TypeError("Initialization arguments are not supported")
|
||||
|
||||
# We need to create the thread dict in anticipation of
|
||||
# __init__ being called, to make sure we don't call it
|
||||
# again ourselves.
|
||||
dict = object.__getattribute__(self, '__dict__')
|
||||
currentThread().__dict__[key] = dict
|
||||
|
||||
return self
|
||||
|
||||
def _patch(self):
|
||||
key = object.__getattribute__(self, '_local__key')
|
||||
d = currentThread().__dict__.get(key)
|
||||
if d is None:
|
||||
d = {}
|
||||
currentThread().__dict__[key] = d
|
||||
object.__setattr__(self, '__dict__', d)
|
||||
|
||||
# we have a new instance dict, so call out __init__ if we have
|
||||
# one
|
||||
cls = type(self)
|
||||
if cls.__init__ is not object.__init__:
|
||||
args, kw = object.__getattribute__(self, '_local__args')
|
||||
cls.__init__(self, *args, **kw)
|
||||
else:
|
||||
object.__setattr__(self, '__dict__', d)
|
||||
|
||||
class local(_localbase):
|
||||
|
||||
def __getattribute__(self, name):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__getattribute__(self, name)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__setattr__(self, name, value)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
def __delattr__(self, name):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__delattr__(self, name)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
|
||||
def __del__():
|
||||
threading_enumerate = enumerate
|
||||
__getattribute__ = object.__getattribute__
|
||||
|
||||
def __del__(self):
|
||||
key = __getattribute__(self, '_local__key')
|
||||
|
||||
try:
|
||||
threads = list(threading_enumerate())
|
||||
except:
|
||||
# if enumerate fails, as it seems to do during
|
||||
# shutdown, we'll skip cleanup under the assumption
|
||||
# that there is nothing to clean up
|
||||
return
|
||||
|
||||
for thread in threads:
|
||||
try:
|
||||
__dict__ = thread.__dict__
|
||||
except AttributeError:
|
||||
# Thread is dying, rest in peace
|
||||
continue
|
||||
|
||||
if key in __dict__:
|
||||
try:
|
||||
del __dict__[key]
|
||||
except KeyError:
|
||||
pass # didn't have anything in this thread
|
||||
|
||||
return __del__
|
||||
__del__ = __del__()
|
||||
|
||||
from threading import currentThread, enumerate, RLock
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -1,191 +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 copy
|
||||
import logging
|
||||
|
||||
import openerp.netsvc as netsvc
|
||||
from openerp.tools.misc import UpdateableStr, UpdateableDict
|
||||
from openerp.tools.translate import translate
|
||||
from lxml import etree
|
||||
|
||||
import openerp.pooler as pooler
|
||||
|
||||
from openerp.osv.osv import except_osv
|
||||
from openerp.osv.orm import except_orm
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class except_wizard(Exception):
|
||||
def __init__(self, name, value):
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.args = (name, value)
|
||||
|
||||
class interface(netsvc.Service):
|
||||
"""
|
||||
This is the base class used to implement Wizards. This class is deprecated
|
||||
and `openerp.osv.TransientModel` must be used instead.
|
||||
"""
|
||||
states = {}
|
||||
|
||||
def __init__(self, name):
|
||||
assert not self.exists('wizard.'+name), 'The wizard "%s" already exists!' % (name,)
|
||||
_logger.warning(
|
||||
"The wizard %s uses the deprecated openerp.wizard.interface class.\n"
|
||||
"It must use the openerp.osv.TransientModel class instead." % \
|
||||
name)
|
||||
super(interface, self).__init__('wizard.'+name)
|
||||
self.wiz_name = name
|
||||
|
||||
def translate_view(self, cr, node, state, lang):
|
||||
if node.get('string'):
|
||||
trans = translate(cr, self.wiz_name+','+state, 'wizard_view', lang, node.get('string').encode('utf8'))
|
||||
if trans:
|
||||
node.set('string', trans)
|
||||
for n in node:
|
||||
self.translate_view(cr, n, state, lang)
|
||||
|
||||
def execute_cr(self, cr, uid, data, state='init', context=None):
|
||||
if not context:
|
||||
context={}
|
||||
res = {}
|
||||
try:
|
||||
state_def = self.states[state]
|
||||
|
||||
result_def = state_def.get('result', {})
|
||||
|
||||
actions_res = {}
|
||||
# iterate through the list of actions defined for this state
|
||||
for action in state_def.get('actions', []):
|
||||
# execute them
|
||||
action_res = action(self, cr, uid, data, context)
|
||||
assert isinstance(action_res, dict), 'The return value of wizard actions should be a dictionary'
|
||||
actions_res.update(action_res)
|
||||
|
||||
res = copy.copy(result_def)
|
||||
res['datas'] = actions_res
|
||||
|
||||
lang = context.get('lang', False)
|
||||
if result_def['type'] == 'action':
|
||||
res['action'] = result_def['action'](self, cr, uid, data, context)
|
||||
elif result_def['type'] == 'form':
|
||||
fields = copy.deepcopy(result_def['fields'])
|
||||
arch = copy.copy(result_def['arch'])
|
||||
button_list = copy.copy(result_def['state'])
|
||||
|
||||
if isinstance(fields, UpdateableDict):
|
||||
fields = fields.dict
|
||||
if isinstance(arch, UpdateableStr):
|
||||
arch = arch.string
|
||||
|
||||
# fetch user-set defaut values for the field... shouldn't we pass it the uid?
|
||||
ir_values_obj = pooler.get_pool(cr.dbname).get('ir.values')
|
||||
defaults = ir_values_obj.get(cr, uid, 'default', False, [('wizard.'+self.wiz_name, False)])
|
||||
default_values = dict([(x[1], x[2]) for x in defaults])
|
||||
for val in fields.keys():
|
||||
if 'default' in fields[val]:
|
||||
# execute default method for this field
|
||||
if callable(fields[val]['default']):
|
||||
fields[val]['value'] = fields[val]['default'](uid, data, state)
|
||||
else:
|
||||
fields[val]['value'] = fields[val]['default']
|
||||
del fields[val]['default']
|
||||
else:
|
||||
# if user has set a default value for the field, use it
|
||||
if val in default_values:
|
||||
fields[val]['value'] = default_values[val]
|
||||
if 'selection' in fields[val]:
|
||||
if not isinstance(fields[val]['selection'], (tuple, list)):
|
||||
fields[val] = copy.copy(fields[val])
|
||||
fields[val]['selection'] = fields[val]['selection'](self, cr, uid, context)
|
||||
elif lang:
|
||||
res_name = "%s,%s,%s" % (self.wiz_name, state, val)
|
||||
trans = lambda x: translate(cr, res_name, 'selection', lang, x) or x
|
||||
for idx, (key, val2) in enumerate(fields[val]['selection']):
|
||||
fields[val]['selection'][idx] = (key, trans(val2))
|
||||
|
||||
if lang:
|
||||
# translate fields
|
||||
for field in fields:
|
||||
res_name = "%s,%s,%s" % (self.wiz_name, state, field)
|
||||
|
||||
trans = translate(cr, res_name, 'wizard_field', lang)
|
||||
if trans:
|
||||
fields[field]['string'] = trans
|
||||
|
||||
if 'help' in fields[field]:
|
||||
t = translate(cr, res_name, 'help', lang, fields[field]['help'])
|
||||
if t:
|
||||
fields[field]['help'] = t
|
||||
|
||||
# translate arch
|
||||
if not isinstance(arch, UpdateableStr):
|
||||
doc = etree.XML(arch)
|
||||
self.translate_view(cr, doc, state, lang)
|
||||
arch = etree.tostring(doc)
|
||||
|
||||
# translate buttons
|
||||
button_list = list(button_list)
|
||||
for i, aa in enumerate(button_list):
|
||||
button_name = aa[0]
|
||||
trans = translate(cr, self.wiz_name+','+state+','+button_name, 'wizard_button', lang)
|
||||
if trans:
|
||||
aa = list(aa)
|
||||
aa[1] = trans
|
||||
button_list[i] = aa
|
||||
|
||||
res['fields'] = fields
|
||||
res['arch'] = arch
|
||||
res['state'] = button_list
|
||||
|
||||
elif result_def['type'] == 'choice':
|
||||
next_state = result_def['next_state'](self, cr, uid, data, context)
|
||||
return self.execute_cr(cr, uid, data, next_state, context)
|
||||
|
||||
except Exception, e:
|
||||
if isinstance(e, except_wizard) \
|
||||
or isinstance(e, except_osv) \
|
||||
or isinstance(e, except_orm):
|
||||
netsvc.abort_response(2, e.name, 'warning', e.value)
|
||||
else:
|
||||
_logger.exception('Exception in call:')
|
||||
raise
|
||||
|
||||
return res
|
||||
|
||||
def execute(self, db, uid, data, state='init', context=None):
|
||||
if not context:
|
||||
context={}
|
||||
cr = pooler.get_db(db).cursor()
|
||||
try:
|
||||
try:
|
||||
res = self.execute_cr(cr, uid, data, state, context)
|
||||
cr.commit()
|
||||
except Exception:
|
||||
cr.rollback()
|
||||
raise
|
||||
finally:
|
||||
cr.close()
|
||||
return res
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -106,8 +106,9 @@ setuptools.setup(
|
||||
'docutils',
|
||||
'feedparser',
|
||||
'gdata',
|
||||
'lxml < 3', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
|
||||
'lxml', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
|
||||
'mako',
|
||||
'mock',
|
||||
'PIL', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
|
||||
'psutil', # windows binary code.google.com/p/psutil/downloads/list
|
||||
'psycopg2',
|
||||
@@ -120,6 +121,7 @@ setuptools.setup(
|
||||
'pyyaml',
|
||||
'reportlab', # windows binary pypi.python.org/pypi/reportlab
|
||||
'simplejson',
|
||||
'unittest2',
|
||||
'vatnumber',
|
||||
'vobject',
|
||||
'werkzeug',
|
||||
|
||||
Reference in New Issue
Block a user