[MERGE] trunk

bzr revid: al@openerp.com-20110924145258-16r9qi7hr3dip1jz
This commit is contained in:
Antony Lesuisse 2011-09-24 16:52:58 +02:00
commit 7a88d9058d
56 changed files with 11680 additions and 1134 deletions

View File

@ -27,7 +27,7 @@ OpenERP is an ERP+CRM program for small and medium businesses.
The whole source code is distributed under the terms of the The whole source code is distributed under the terms of the
GNU Public Licence. GNU Public Licence.
(c) 2003-TODAY, Fabien Pinckaers - OpenERP s.a. (c) 2003-TODAY, Fabien Pinckaers - OpenERP SA
""" """
import logging import logging
@ -88,8 +88,11 @@ def setup_pid_file():
def preload_registry(dbname): def preload_registry(dbname):
""" Preload a registry, and start the cron.""" """ Preload a registry, and start the cron."""
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False) try:
pool.get('ir.cron').restart(db.dbname) db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
pool.get('ir.cron').restart(db.dbname)
except Exception:
logging.exception('Failed to initialize database `%s`.', dbname)
def run_test_file(dbname, test_file): def run_test_file(dbname, test_file):
""" Preload a registry, possibly run a test file, and start the cron.""" """ Preload a registry, possibly run a test file, and start the cron."""
@ -233,6 +236,8 @@ def quit_on_signals():
if __name__ == "__main__": if __name__ == "__main__":
os.environ["TZ"] = "UTC"
check_root_user() check_root_user()
openerp.tools.config.parse_config(sys.argv[1:]) openerp.tools.config.parse_config(sys.argv[1:])
check_postgres_user() check_postgres_user()

View File

@ -67,7 +67,7 @@
'res/res_currency_view.xml', 'res/res_currency_view.xml',
'res/res_partner_event_view.xml', 'res/res_partner_event_view.xml',
'res/wizard/partner_sms_send_view.xml', 'res/wizard/partner_sms_send_view.xml',
'res/wizard/partner_wizard_spam_view.xml', 'res/wizard/partner_wizard_massmail_view.xml',
'res/wizard/partner_clear_ids_view.xml', 'res/wizard/partner_clear_ids_view.xml',
'res/wizard/partner_wizard_ean_check_view.xml', 'res/wizard/partner_wizard_ean_check_view.xml',
'res/res_partner_data.xml', 'res/res_partner_data.xml',

View File

@ -1086,16 +1086,17 @@
<field eval="time.strftime('%Y-01-01')" name="name"/> <field eval="time.strftime('%Y-01-01')" name="name"/>
</record> </record>
<record id="VEB" model="res.currency"> <!-- VEF was previously VEB -->
<field name="name">VEB</field> <record id="VEF" model="res.currency">
<field name="symbol">Bs</field> <field name="name">VEF</field>
<field name="rounding">2.95</field> <field name="symbol">Bs.F</field>
<field name="rounding">0.0001</field>
<field name="accuracy">4</field> <field name="accuracy">4</field>
<field name="company_id" ref="main_company"/> <field name="company_id" ref="main_company"/>
</record> </record>
<record id="rateVEB" model="res.currency.rate"> <record id="rateVEF" model="res.currency.rate">
<field name="rate">2768.45</field> <field name="rate">5.864</field>
<field name="currency_id" ref="VEB"/> <field name="currency_id" ref="VEF"/>
<field eval="time.strftime('%Y-01-01')" name="name"/> <field eval="time.strftime('%Y-01-01')" name="name"/>
</record> </record>
@ -1599,12 +1600,20 @@
<field name="rounding">0.01</field> <field name="rounding">0.01</field>
<field name="accuracy">4</field> <field name="accuracy">4</field>
<field name="symbol">¢</field> <field name="symbol">¢</field>
<field name="company_id" ref="main_company"/>
</record> </record>
<record id="rateCRC" model="res.currency.rate"> <record id="rateCRC" model="res.currency.rate">
<field name="rate">691.3153</field> <field name="rate">691.3153</field>
<field name="currency_id" ref="CRC"/> <field name="currency_id" ref="CRC"/>
<field eval="time.strftime('%Y-01-01')" name="name"/> <field eval="time.strftime('%Y-01-01')" name="name"/>
</record> </record>
<record id="ir_mail_server_localhost0" model="ir.mail_server">
<field name="name">localhost</field>
<field name="smtp_host">localhost</field>
<field eval="25" name="smtp_port"/>
<field eval="10" name="priority"/>
</record>
<record id="MUR" model="res.currency"> <record id="MUR" model="res.currency">
<field name="name">MUR</field> <field name="name">MUR</field>

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## ##############################################################################
# #
# OpenERP, Open Source Management Solution # OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
# #
@ -15,7 +15,7 @@
# GNU Affero General Public License for more details. # GNU Affero General Public License for more details.
# #
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
############################################################################## ##############################################################################
@ -36,6 +36,7 @@ import ir_rule
import wizard import wizard
import ir_config_parameter import ir_config_parameter
import osv_memory_autovacuum import osv_memory_autovacuum
import ir_mail_server
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -28,7 +28,7 @@
<group col="2" colspan="2"> <group col="2" colspan="2">
<separator string="Values for Event Type" colspan="2"/> <separator string="Values for Event Type" colspan="2"/>
<label string="client_action_multi, client_action_relate" colspan="2"/> <label string="client_action_multi, client_action_relate" colspan="2"/>
<label string="tree_but_action, client_print_multi" colspan="2"/> <label string="tree_but_open, client_print_multi" colspan="2"/>
</group> </group>
<group col="2" colspan="2"> <group col="2" colspan="2">
<separator colspan="2" string="Value"/> <separator colspan="2" string="Value"/>
@ -1274,7 +1274,7 @@
<record id="action_model_model" model="ir.actions.act_window"> <record id="action_model_model" model="ir.actions.act_window">
<field name="name">Objects</field> <field name="name">Models</field>
<field name="res_model">ir.model</field> <field name="res_model">ir.model</field>
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="context">{'manual':True}</field> <field name="context">{'manual':True}</field>
@ -1740,7 +1740,7 @@
<field name="name">Property multi-company</field> <field name="name">Property multi-company</field>
<field model="ir.model" name="model_id" ref="model_ir_property"/> <field model="ir.model" name="model_id" ref="model_ir_property"/>
<field eval="True" name="global"/> <field eval="True" name="global"/>
<field name="domain_force">['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field> <field name="domain_force">['|',('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
</record> </record>
<!--server action view--> <!--server action view-->
@ -1766,7 +1766,9 @@
<page string="Trigger" attrs="{'invisible':[('state','!=','trigger')]}"> <page string="Trigger" attrs="{'invisible':[('state','!=','trigger')]}">
<separator colspan="4" string="Trigger Configuration"/> <separator colspan="4" string="Trigger Configuration"/>
<field name="wkf_model_id" attrs="{'required':[('state','=','trigger')]}"/> <field name="wkf_model_id" attrs="{'required':[('state','=','trigger')]}"/>
<field name="trigger_obj_id" context="{'key':''}" domain="[('model_id','=',model_id)]" attrs="{'required':[('state','=','trigger')]}"/> <field name="trigger_obj_id" context="{'key':''}"
domain="[('model_id','=',model_id),('ttype','in',['many2one','int'])]"
attrs="{'required':[('state','=','trigger')]}"/>
<field name="trigger_name" attrs="{'required':[('state','=','trigger')]}"/> <field name="trigger_name" attrs="{'required':[('state','=','trigger')]}"/>
</page> </page>
<page string="Action to Launch" attrs="{'invisible':[('state','!=','client_action')]}"> <page string="Action to Launch" attrs="{'invisible':[('state','!=','client_action')]}">
@ -1995,5 +1997,76 @@
<field name="sequence">5</field> <field name="sequence">5</field>
</record> </record>
<!-- ir.mail.server -->
<record model="ir.ui.view" id="ir_mail_server_form">
<field name="name">ir.mail.server.form</field>
<field name="model">ir.mail_server</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Outgoing Mail Servers">
<group colspan="4">
<field name="name"/>
<field name="sequence"/>
</group>
<notebook colspan="4">
<page string="Configuration">
<group col="2" colspan="4">
<separator string="Connection Information" colspan="4"/>
<field name="smtp_host"/>
<field name="smtp_port"/>
<field name="smtp_debug"/>
</group>
<group col="2" colspan="4">
<separator string="Security and Authentication" colspan="2"/>
<field name="smtp_encryption" on_change="on_change_encryption(smtp_encryption)"/>
<field name="smtp_user"/>
<field name="smtp_pass" password="True"/>
<button name="test_smtp_connection" type="object" string="Test Connection" icon="gtk-network" colspan="2"/>
</group>
</page>
</notebook>
</form>
</field>
</record>
<record model="ir.ui.view" id="ir_mail_server_list">
<field name="name">ir.mail.server.list</field>
<field name="model">ir.mail_server</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Outgoing Mail Servers">
<field name="sequence"/>
<field name="name"/>
<field name="smtp_host"/>
<field name="smtp_user"/>
<field name="smtp_encryption"/>
</tree>
</field>
</record>
<record id="view_ir_mail_server_search" model="ir.ui.view">
<field name="name">ir.mail.server.search</field>
<field name="model">ir.mail_server</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Outgoing Mail Servers">
<field name="name"/>
<field name="smtp_host"/>
<field name="smtp_user"/>
<field name="smtp_encryption"/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="action_ir_mail_server_list">
<field name="name">Outgoing Mail Servers</field>
<field name="res_model">ir.mail_server</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="ir_mail_server_list" />
<field name="search_view_id" ref="view_ir_mail_server_search"/>
</record>
<menuitem id="next_id_15" name="Parameters" parent="base.menu_config" groups="base.group_extended" />
<menuitem id="menu_mail_servers" parent="base.next_id_15" action="action_ir_mail_server_list" sequence="15"/>
</data> </data>
</openerp> </openerp>

View File

@ -418,7 +418,10 @@ class server_object_lines(osv.osv):
_columns = { _columns = {
'server_id': fields.many2one('ir.actions.server', 'Object Mapping'), 'server_id': fields.many2one('ir.actions.server', 'Object Mapping'),
'col1': fields.many2one('ir.model.fields', 'Destination', required=True), 'col1': fields.many2one('ir.model.fields', 'Destination', required=True),
'value': fields.text('Value', required=True), 'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
"When Formula type is selected, this field may be a Python expression "
" that can use the same values as for the condition field on the server action.\n"
"If Value type is selected, the value will be used directly without evaluation."),
'type': fields.selection([ 'type': fields.selection([
('value','Value'), ('value','Value'),
('equation','Formula') ('equation','Formula')
@ -435,14 +438,15 @@ server_object_lines()
class actions_server(osv.osv): class actions_server(osv.osv):
def _select_signals(self, cr, uid, context=None): def _select_signals(self, cr, uid, context=None):
cr.execute("SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t \ cr.execute("""SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t
WHERE w.id = a.wkf_id AND t.act_from = a.id OR t.act_to = a.id AND t.signal!='' \ WHERE w.id = a.wkf_id AND
AND t.signal NOT IN (null, NULL)") (t.act_from = a.id OR t.act_to = a.id) AND
t.signal IS NOT NULL""")
result = cr.fetchall() or [] result = cr.fetchall() or []
res = [] res = []
for rs in result: for rs in result:
if rs[0] is not None and rs[1] is not None: if rs[0] is not None and rs[1] is not None:
line = rs[0], "%s - (%s)" % (rs[1], rs[0]) line = rs[1], "%s - (%s)" % (rs[1], rs[0])
res.append(line) res.append(line)
return res return res
@ -453,13 +457,15 @@ class actions_server(osv.osv):
return [(r['model'], r['name']) for r in res] + [('','')] return [(r['model'], r['name']) for r in res] + [('','')]
def change_object(self, cr, uid, ids, copy_object, state, context=None): def change_object(self, cr, uid, ids, copy_object, state, context=None):
if state == 'object_copy': if state == 'object_copy' and copy_object:
if context is None:
context = {}
model_pool = self.pool.get('ir.model') model_pool = self.pool.get('ir.model')
model = copy_object.split(',')[0] model = copy_object.split(',')[0]
mid = model_pool.search(cr, uid, [('model','=',model)]) mid = model_pool.search(cr, uid, [('model','=',model)])
return { return {
'value':{'srcmodel_id':mid[0]}, 'value': {'srcmodel_id': mid[0]},
'context':context 'context': context
} }
else: else:
return {} return {}
@ -469,8 +475,19 @@ class actions_server(osv.osv):
_sequence = 'ir_actions_id_seq' _sequence = 'ir_actions_id_seq'
_order = 'sequence,name' _order = 'sequence,name'
_columns = { _columns = {
'name': fields.char('Action Name', required=True, size=64, help="Easy to Refer action by name e.g. One Sales Order -> Many Invoices", translate=True), 'name': fields.char('Action Name', required=True, size=64, translate=True),
'condition' : fields.char('Condition', size=256, required=True, help="Condition that is to be tested before action is executed, e.g. object.list_price > object.cost_price"), 'condition' : fields.char('Condition', size=256, required=True,
help="Condition that is tested before the action is executed, "
"and prevent execution if it is not verified.\n"
"Example: object.list_price > 5000\n"
"It is a Python expression that can use the following values:\n"
" - self: ORM model of the record on which the action is triggered\n"
" - object or obj: browse_record of the record on which the action is triggered\n"
" - pool: ORM model pool (i.e. self.pool)\n"
" - time: Python time module\n"
" - cr: database cursor\n"
" - uid: current user id\n"
" - context: current context"),
'state': fields.selection([ 'state': fields.selection([
('client_action','Client Action'), ('client_action','Client Action'),
('dummy','Dummy'), ('dummy','Dummy'),
@ -484,16 +501,20 @@ class actions_server(osv.osv):
('object_write','Write Object'), ('object_write','Write Object'),
('other','Multi Actions'), ('other','Multi Actions'),
], 'Action Type', required=True, size=32, help="Type of the Action that is to be executed"), ], 'Action Type', required=True, size=32, help="Type of the Action that is to be executed"),
'code':fields.text('Python Code', help="Python code to be executed"), 'code':fields.text('Python Code', help="Python code to be executed if condition is met.\n"
"It is a Python block that can use the same values as for the condition field"),
'sequence': fields.integer('Sequence', help="Important when you deal with multiple actions, the execution order will be decided based on this, low number is higher priority."), 'sequence': fields.integer('Sequence', help="Important when you deal with multiple actions, the execution order will be decided based on this, low number is higher priority."),
'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create)."), 'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create)."),
'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."), 'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."),
'trigger_name': fields.selection(_select_signals, string='Trigger Name', size=128, help="Select the Signal name that is to be used as the trigger."), 'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
'wkf_model_id': fields.many2one('ir.model', 'Workflow On', help="Workflow to be executed on this model."), 'wkf_model_id': fields.many2one('ir.model', 'Target Object', help="The object that should receive the workflow signal (must have an associated workflow)"),
'trigger_obj_id': fields.many2one('ir.model.fields','Trigger On', help="Select the object from the model on which the workflow will executed."), 'trigger_obj_id': fields.many2one('ir.model.fields','Relation Field', help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)"),
'email': fields.char('Email Address', size=512, help="Provides the fields that will be used to fetch the email address, e.g. when you select the invoice, then `object.invoice_address_id.email` is the field which gives the correct address"), 'email': fields.char('Email Address', size=512, help="Expression that returns the email address to send to. Can be based on the same values as for the condition field.\n"
'subject': fields.char('Subject', size=1024, translate=True, help="Specify the subject. You can use fields from the object, e.g. `Hello [[ object.partner_id.name ]]`"), "Example: object.invoice_address_id.email, or 'me@example.com'"),
'message': fields.text('Message', translate=True, help="Specify the message. You can use the fields from the object. e.g. `Dear [[ object.partner_id.name ]]`"), 'subject': fields.char('Subject', size=1024, translate=True, help="Email subject, may contain expressions enclosed in double brackets based on the same values as those "
"available in the condition field, e.g. `Hello [[ object.partner_id.name ]]`"),
'message': fields.text('Message', translate=True, help="Email contents, may contain expressions enclosed in double brackets based on the same values as those "
"available in the condition field, e.g. `Dear [[ object.partner_id.name ]]`"),
'mobile': fields.char('Mobile No', size=512, help="Provides fields that be used to fetch the mobile number, e.g. you select the invoice, then `object.invoice_address_id.mobile` is the field which gives the correct mobile number"), 'mobile': fields.char('Mobile No', size=512, help="Provides fields that be used to fetch the mobile number, e.g. you select the invoice, then `object.invoice_address_id.mobile` is the field which gives the correct mobile number"),
'sms': fields.char('SMS', size=160, translate=True), 'sms': fields.char('SMS', size=160, translate=True),
'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions', 'server_id', 'action_id', 'Other Actions'), 'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions', 'server_id', 'action_id', 'Other Actions'),
@ -512,12 +533,14 @@ class actions_server(osv.osv):
'condition': lambda *a: 'True', 'condition': lambda *a: 'True',
'type': lambda *a: 'ir.actions.server', 'type': lambda *a: 'ir.actions.server',
'sequence': lambda *a: 5, 'sequence': lambda *a: 5,
'code': lambda *a: """# You can use the following variables 'code': lambda *a: """# You can use the following variables:
# - object or obj # - self: ORM model of the record on which the action is triggered
# - time # - object: browse_record of the record on which the action is triggered if there is one, otherwise None
# - cr # - pool: ORM model pool (i.e. self.pool)
# - uid # - time: Python time module
# - ids # - cr: database cursor
# - uid: current user id
# - context: current context
# If you plan to return an action, assign: action = {...} # If you plan to return an action, assign: action = {...}
""", """,
} }
@ -567,6 +590,7 @@ class actions_server(osv.osv):
def merge_message(self, cr, uid, keystr, action, context=None): def merge_message(self, cr, uid, keystr, action, context=None):
if context is None: if context is None:
context = {} context = {}
def merge(match): def merge(match):
obj_pool = self.pool.get(action.model_id.model) obj_pool = self.pool.get(action.model_id.model)
id = context.get('active_id') id = context.get('active_id')
@ -602,15 +626,17 @@ class actions_server(osv.osv):
user = self.pool.get('res.users').browse(cr, uid, uid) user = self.pool.get('res.users').browse(cr, uid, uid)
for action in self.browse(cr, uid, ids, context): for action in self.browse(cr, uid, ids, context):
obj = None obj = None
obj_pool = self.pool.get(action.model_id.model)
if context.get('active_model') == action.model_id.model and context.get('active_id'): if context.get('active_model') == action.model_id.model and context.get('active_id'):
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context) obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
cxt = { cxt = {
'context': dict(context), # copy context to prevent side-effects of eval 'self': obj_pool,
'object': obj, 'object': obj,
'obj': obj,
'pool': self.pool,
'time': time, 'time': time,
'cr': cr, 'cr': cr,
'pool': self.pool, 'context': dict(context), # copy context to prevent side-effects of eval
'uid': uid, 'uid': uid,
'user': user 'user': user
} }
@ -625,21 +651,9 @@ class actions_server(osv.osv):
.read(cr, uid, action.action_id.id, context=context) .read(cr, uid, action.action_id.id, context=context)
if action.state=='code': if action.state=='code':
localdict = { eval(action.code, cxt, mode="exec", nocopy=True) # nocopy allows to return 'action'
'self': self.pool.get(action.model_id.model), if 'action' in cxt:
'pool': self.pool, return cxt['action']
'context': dict(context), # copy context to prevent side-effects of eval
'time': time,
'ids': ids,
'cr': cr,
'uid': uid,
'object':obj,
'obj': obj,
'user': user,
}
eval(action.code, localdict, mode="exec", nocopy=True) # nocopy allows to return 'action'
if 'action' in localdict:
return localdict['action']
if action.state == 'email': if action.state == 'email':
email_from = config['email_from'] email_from = config['email_from']
@ -652,7 +666,7 @@ class actions_server(osv.osv):
if not address: if not address:
logger.info('No partner email address specified, not sending any email.') logger.info('No partner email address specified, not sending any email.')
continue continue
if not email_from: if not email_from:
logger.debug('--email-from command line option is not specified, using a fallback value instead.') logger.debug('--email-from command line option is not specified, using a fallback value instead.')
if user.user_email: if user.user_email:
@ -663,7 +677,10 @@ class actions_server(osv.osv):
subject = self.merge_message(cr, uid, action.subject, action, context) subject = self.merge_message(cr, uid, action.subject, action, context)
body = self.merge_message(cr, uid, action.message, action, context) body = self.merge_message(cr, uid, action.message, action, context)
if tools.email_send(email_from, [address], subject, body, debug=False, subtype='html') == True: ir_mail_server = self.pool.get('ir.mail_server')
msg = ir_mail_server.build_email(email_from, [address], subject, body)
res_email = ir_mail_server.send_email(cr, uid, msg)
if res_email:
logger.info('Email successfully sent to: %s', address) logger.info('Email successfully sent to: %s', address)
else: else:
logger.warning('Failed to send email to: %s', address) logger.warning('Failed to send email to: %s', address)
@ -671,10 +688,10 @@ class actions_server(osv.osv):
if action.state == 'trigger': if action.state == 'trigger':
wf_service = netsvc.LocalService("workflow") wf_service = netsvc.LocalService("workflow")
model = action.wkf_model_id.model model = action.wkf_model_id.model
obj_pool = self.pool.get(action.model_id.model) m2o_field_name = action.trigger_obj_id.name
res_id = self.pool.get(action.model_id.model).read(cr, uid, [context.get('active_id')], [action.trigger_obj_id.name]) target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
id = res_id [0][action.trigger_obj_id.name] target_id = target_id[0] if isinstance(target_id,tuple) else target_id
wf_service.trg_validate(uid, model, int(id), action.trigger_name, cr) wf_service.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
if action.state == 'sms': if action.state == 'sms':
#TODO: set the user and password from the system #TODO: set the user and password from the system
@ -689,20 +706,9 @@ class actions_server(osv.osv):
result = self.run(cr, uid, [act.id], context) result = self.run(cr, uid, [act.id], context)
if result: if result:
res.append(result) res.append(result)
return res return res
if action.state == 'loop': if action.state == 'loop':
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
cxt = {
'context': dict(context), # copy context to prevent side-effects of eval
'object': obj,
'time': time,
'cr': cr,
'pool' : self.pool,
'uid' : uid
}
expr = eval(str(action.expression), cxt) expr = eval(str(action.expression), cxt)
context['object'] = obj context['object'] = obj
for i in expr: for i in expr:
@ -714,13 +720,6 @@ class actions_server(osv.osv):
for exp in action.fields_lines: for exp in action.fields_lines:
euq = exp.value euq = exp.value
if exp.type == 'equation': if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
cxt = {
'context': dict(context), # copy context to prevent side-effects of eval
'object': obj,
'time': time,
}
expr = eval(euq, cxt) expr = eval(euq, cxt)
else: else:
expr = exp.value expr = exp.value
@ -754,14 +753,7 @@ class actions_server(osv.osv):
for exp in action.fields_lines: for exp in action.fields_lines:
euq = exp.value euq = exp.value
if exp.type == 'equation': if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model) expr = eval(euq, cxt)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
expr = eval(euq,
{
'context': dict(context), # copy context to prevent side-effects of eval
'object': obj,
'time': time,
})
else: else:
expr = exp.value expr = exp.value
res[exp.col1.name] = expr res[exp.col1.name] = expr
@ -778,21 +770,11 @@ class actions_server(osv.osv):
for exp in action.fields_lines: for exp in action.fields_lines:
euq = exp.value euq = exp.value
if exp.type == 'equation': if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model) expr = eval(euq, cxt)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
expr = eval(euq,
{
'context': dict(context), # copy context to prevent side-effects of eval
'object': obj,
'time': time,
})
else: else:
expr = exp.value expr = exp.value
res[exp.col1.name] = expr res[exp.col1.name] = expr
obj_pool = None
res_id = False
model = action.copy_object.split(',')[0] model = action.copy_object.split(',')[0]
cid = action.copy_object.split(',')[1] cid = action.copy_object.split(',')[1]
obj_pool = self.pool.get(model) obj_pool = self.pool.get(model)

View File

@ -50,8 +50,7 @@ class ir_attachment(osv.osv):
for model, mids in res_ids.items(): for model, mids in res_ids.items():
# ignore attachments that are not attached to a resource anymore when checking access rights # ignore attachments that are not attached to a resource anymore when checking access rights
# (resource was deleted but attachment was not) # (resource was deleted but attachment was not)
cr.execute('select id from '+self.pool.get(model)._table+' where id in %s', (tuple(mids),)) mids = self.pool.get(model).exists(cr, uid, mids)
mids = [x[0] for x in cr.fetchall()]
ima.check(cr, uid, model, mode) ima.check(cr, uid, model, mode)
self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context) self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)

View File

@ -2,7 +2,7 @@
############################################################################## ##############################################################################
# #
# OpenERP, Open Source Management Solution # OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). # Copyright (C) 2011 OpenERP SA (<http://www.openerp.com>).
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -19,7 +19,7 @@
# #
############################################################################## ##############################################################################
""" """
A module to store some configuration parameters relative to a whole database. Store database-specific configuration parameters
""" """
from osv import osv,fields from osv import osv,fields
@ -36,23 +36,19 @@ _default_parameters = {
} }
class ir_config_parameter(osv.osv): class ir_config_parameter(osv.osv):
""" An osv to old configuration parameters for a given database. """Per-database storage of configuration key-value pairs."""
To be short, it's just a global dictionary of strings stored in a table. """
_name = 'ir.config_parameter' _name = 'ir.config_parameter'
_columns = { _columns = {
# The key of the configuration parameter.
'key': fields.char('Key', size=256, required=True, select=1), 'key': fields.char('Key', size=256, required=True, select=1),
# The value of the configuration parameter.
'value': fields.text('Value', required=True), 'value': fields.text('Value', required=True),
} }
_sql_constraints = [ _sql_constraints = [
('key_uniq', 'unique (key)', 'Key must be unique.') ('key_uniq', 'unique (key)', 'Key must be unique.')
] ]
def init(self, cr): def init(self, cr):
""" """
Initializes the parameters listed in _default_parameters. Initializes the parameters listed in _default_parameters.
@ -63,12 +59,12 @@ class ir_config_parameter(osv.osv):
self.set_param(cr, 1, key, func()) self.set_param(cr, 1, key, func())
def get_param(self, cr, uid, key, default=False, context=None): def get_param(self, cr, uid, key, default=False, context=None):
""" Get the value of a parameter. """Retrieve the value for a given key.
@param key: The key of the parameter. :param string key: The key of the parameter value to retrieve.
@type key: string :param string default: default value if parameter is missing.
@return: The value of the parameter, False if it does not exist. :return: The value of the parameter, or ``default`` if it does not exist.
@rtype: string :rtype: string
""" """
ids = self.search(cr, uid, [('key','=',key)], context=context) ids = self.search(cr, uid, [('key','=',key)], context=context)
if not ids: if not ids:
@ -78,15 +74,13 @@ class ir_config_parameter(osv.osv):
return value return value
def set_param(self, cr, uid, key, value, context=None): def set_param(self, cr, uid, key, value, context=None):
""" Set the value of a parameter. """Sets the value of a parameter.
@param key: The key of the parameter. :param string key: The key of the parameter value to set.
@type key: string :param string value: The value to set.
@param value: The value of the parameter. :return: the previous value of the parameter or False if it did
@type value: string not exist.
@return: Return the previous value of the parameter of False if it did :rtype: string
not existed.
@rtype: string
""" """
ids = self.search(cr, uid, [('key','=',key)], context=context) ids = self.search(cr, uid, [('key','=',key)], context=context)
if ids: if ids:
@ -97,5 +91,3 @@ class ir_config_parameter(osv.osv):
else: else:
self.create(cr, uid, {'key': key, 'value': value}, context=context) self.create(cr, uid, {'key': key, 'value': value}, context=context)
return False return False
ir_config_parameter()

View File

@ -0,0 +1,436 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2011 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 email.MIMEText import MIMEText
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.Header import Header
from email.Utils import formatdate, make_msgid, COMMASPACE
from email import Encoders
import logging
import re
import smtplib
from osv import osv
from osv import fields
from openerp.tools.translate import _
from openerp.tools import html2text
from openerp.tools.func import wraps
import openerp.tools as tools
# ustr was originally from tools.misc.
# it is moved to loglevels until we refactor tools.
from openerp.loglevels import ustr
_logger = logging.getLogger('ir.mail_server')
class MailDeliveryException(osv.except_osv):
"""Specific exception subclass for mail delivery errors"""
def __init__(self, name, value, exc_type='warning'):
super(MailDeliveryException, self).__init__(name, value, exc_type=exc_type)
class WriteToLogger(object):
"""debugging helper: behave as a fd and pipe to logger at the given level"""
def __init__(self, logger, level=logging.DEBUG):
self.logger = logger
self.level = level
def write(self, s):
self.logger.log(self.level, s)
def try_coerce_ascii(string_utf8):
"""Attempts to decode the given utf8-encoded string
as ASCII after coercing it to UTF-8, then return
the confirmed 7-bit ASCII string.
If the process fails (because the string
contains non-ASCII characters) returns ``None``.
"""
try:
string_utf8.decode('ascii')
except UnicodeDecodeError:
return
return string_utf8
def encode_header(header_text):
"""Returns an appropriate representation of the given header value,
suitable for direct assignment as a header value in an
email.message.Message. RFC2822 assumes that headers contain
only 7-bit characters, so we ensure it is the case, using
RFC2047 encoding when needed.
:param header_text: unicode or utf-8 encoded string with header value
:rtype: string | email.header.Header
:return: if ``header_text`` represents a plain ASCII string,
return the same 7-bit string, otherwise returns an email.header.Header
that will perform the appropriate RFC2047 encoding of
non-ASCII values.
"""
if not header_text: return ""
# convert anything to utf-8, suitable for testing ASCIIness, as 7-bit chars are
# encoded as ASCII in utf-8
header_text_utf8 = tools.ustr(header_text).encode('utf-8')
header_text_ascii = try_coerce_ascii(header_text_utf8)
# if this header contains non-ASCII characters,
# we'll need to wrap it up in a message.header.Header
# that will take care of RFC2047-encoding it as
# 7-bit string.
return header_text_ascii if header_text_ascii\
else Header(header_text_utf8, 'utf-8')
name_with_email_pattern = re.compile(r'("[^<@>]+")\s*<([^ ,<@]+@[^> ,]+)>')
address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)')
def extract_rfc2822_addresses(text):
"""Returns a list of valid RFC2822 addresses
that can be found in ``source``, ignoring
malformed ones and non-ASCII ones.
"""
if not text: return []
candidates = address_pattern.findall(tools.ustr(text).encode('utf-8'))
return filter(try_coerce_ascii, candidates)
def encode_rfc2822_address_header(header_text):
"""If ``header_text`` contains non-ASCII characters,
attempts to locate patterns of the form
``"Name" <address@domain>`` and replace the
``"Name"`` portion by the RFC2047-encoded
version, preserving the address part untouched.
"""
header_text_utf8 = tools.ustr(header_text).encode('utf-8')
header_text_ascii = try_coerce_ascii(header_text_utf8)
if header_text_ascii:
return header_text_ascii
# non-ASCII characters are present, attempt to
# replace all "Name" patterns with the RFC2047-
# encoded version
def replace(match_obj):
name, email = match_obj.group(1), match_obj.group(2)
name_encoded = str(Header(name, 'utf-8'))
return "%s <%s>" % (name_encoded, email)
header_text_utf8 = name_with_email_pattern.sub(replace,
header_text_utf8)
# try again after encoding
header_text_ascii = try_coerce_ascii(header_text_utf8)
if header_text_ascii:
return header_text_ascii
# fallback to extracting pure addresses only, which could
# still cause a failure downstream if the actual addresses
# contain non-ASCII characters
return COMMASPACE.join(extract_rfc2822_addresses(header_text_utf8))
class ir_mail_server(osv.osv):
"""Represents an SMTP server, able to send outgoing e-mails, with SSL and TLS capabilities."""
_name = "ir.mail_server"
_columns = {
'name': fields.char('Description', size=64, required=True, select=True),
'smtp_host': fields.char('Server Name', size=128, required=True, help="Hostname or IP of SMTP server"),
'smtp_port': fields.integer('SMTP Port', size=5, required=True, help="SMTP Port. Usually 465 for SSL, and 25 or 587 for other cases."),
'smtp_user': fields.char('Username', size=64, help="Optional username for SMTP authentication"),
'smtp_pass': fields.char('Password', size=64, help="Optional password for SMTP authentication"),
'smtp_encryption': fields.selection([('none','None'),
('starttls','TLS (STARTTLS)'),
('ssl','SSL/TLS')],
string='Connection Security',
help="Choose the connection encryption scheme:\n"
"- None: SMTP sessions are done in cleartext.\n"
"- TLS (STARTTLS): TLS encryption is requested at start of SMTP session (Recommended)\n"
"- SSL/TLS: SMTP sessions are encrypted with SSL/TLS through a dedicated port (default: 465)"),
'smtp_debug': fields.boolean('Debugging', help="If enabled, the full output of SMTP sessions will "
"be written to the server log at DEBUG level"
"(this is very verbose and may include confidential info!)"),
'sequence': fields.integer('Priority', help="When no specific mail server is requested for a mail, the highest priority one "
"is used. Default priority is 10 (smaller number = higher priority)"),
}
_defaults = {
'smtp_port': 25,
'sequence': 10,
'smtp_encryption': 'none',
}
def __init__(self, *args, **kwargs):
# Make sure we pipe the smtplib outputs to our own DEBUG logger
if not isinstance(smtplib.stderr, WriteToLogger):
logpiper = WriteToLogger(_logger)
smtplib.stderr = logpiper
smtplib.stdout = logpiper
return super(ir_mail_server, self).__init__(*args,**kwargs)
def name_get(self, cr, uid, ids, context=None):
return [(a["id"], "(%s)" % (a['name'])) for a in self.read(cr, uid, ids, ['name'], context=context)]
def test_smtp_connection(self, cr, uid, ids, context=None):
for smtp_server in self.browse(cr, uid, ids, context=context):
smtp = False
try:
smtp = self.connect(smtp_server.smtp_host, smtp_server.smtp_port, user=smtp_server.smtp_user,
password=smtp_server.smtp_pass, encryption=smtp_server.smtp_encryption,
smtp_debug=smtp_server.smtp_debug)
except Exception, e:
raise osv.except_osv(_("Connection test failed!"), _("Here is what we got instead:\n %s") % tools.ustr(e))
finally:
try:
if smtp: smtp.quit()
except Exception:
# ignored, just a consequence of the previous exception
pass
raise osv.except_osv(_("Connection test succeeded!"), _("Everything seems properly set up!"))
def connect(self, host, port, user=None, password=None, encryption=False, smtp_debug=False):
"""Returns a new SMTP connection to the give SMTP server, authenticated
with ``user`` and ``password`` if provided, and encrypted as requested
by the ``encryption`` parameter.
:param host: host or IP of SMTP server to connect to
:param int port: SMTP port to connect to
:param user: optional username to authenticate with
:param password: optional password to authenticate with
:param string encryption: optional: ``'ssl'`` | ``'starttls'``
:param bool smtp_debug: toggle debugging of SMTP sessions (all i/o
will be output in logs)
"""
if encryption == 'ssl':
if not 'SMTP_SSL' in smtplib.__all__:
raise osv.except_osv(
_("SMTP-over-SSL mode unavailable"),
_("Your OpenERP Server does not support SMTP-over-SSL. You could use STARTTLS instead."
"If SSL is needed, an upgrade to Python 2.6 on the server-side should do the trick."))
connection = smtplib.SMTP_SSL(host, port)
else:
connection = smtplib.SMTP(host, port)
connection.set_debuglevel(smtp_debug)
if encryption == 'starttls':
# starttls() will perform ehlo() if needed first
# and will discard the previous list of services
# after successfully performing STARTTLS command,
# (as per RFC 3207) so for example any AUTH
# capability that appears only on encrypted channels
# will be correctly detected for next step
connection.starttls()
if user:
# Attempt authentication - will raise if AUTH service not supported
connection.login(user, password)
return connection
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None):
"""Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it.
:param string email_from: sender email address
:param list email_to: list of recipient addresses (to be joined with commas)
:param string subject: email subject (no pre-encoding/quoting necessary)
:param string body: email body, according to the ``subtype`` (by default, plaintext).
If html subtype is used, the message will be automatically converted
to plaintext and wrapped in multipart/alternative.
:param string reply_to: optional value of Reply-To header
:param string object_id: optional tracking identifier, to be included in the message-id for
recognizing replies. Suggested format for object-id is "res_id-model",
e.g. "12345-crm.lead".
:param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'),
must match the format of the ``body`` parameter. Default is 'plain',
making the content part of the mail "text/plain".
:param list attachments: list of (filename, filecontents) pairs, where filecontents is a string
containing the bytes of the attachment
:param list email_cc: optional list of string values for CC header (to be joined with commas)
:param list email_bcc: optional list of string values for BCC header (to be joined with commas)
:param dict headers: optional map of headers to set on the outgoing mail (may override the
other headers, including Subject, Reply-To, Message-Id, etc.)
:rtype: email.message.Message (usually MIMEMultipart)
:return: the new RFC2822 email message
"""
email_from = email_from or tools.config.get('email_from')
assert email_from, "You must either provide a sender address explicitly or configure "\
"a global sender address in the server configuration or with the "\
"--email-from startup parameter."
# Note: we must force all strings to to 8-bit utf-8 when crafting message,
# or use encode_header() for headers, which does it automatically.
headers = headers or {} # need valid dict later
if not email_cc: email_cc = []
if not email_bcc: email_bcc = []
if not body: body = u''
email_body_utf8 = ustr(body).encode('utf-8')
email_text_part = MIMEText(email_body_utf8 or '', _subtype=subtype, _charset='utf-8')
msg = MIMEMultipart()
if not message_id:
if object_id:
message_id = tools.generate_tracking_message_id(object_id)
else:
message_id = make_msgid()
msg['Message-Id'] = encode_header(message_id)
if references:
msg['references'] = encode_header(references)
msg['Subject'] = encode_header(subject)
msg['From'] = encode_rfc2822_address_header(email_from)
del msg['Reply-To']
if reply_to:
msg['Reply-To'] = encode_rfc2822_address_header(reply_to)
else:
msg['Reply-To'] = msg['From']
msg['To'] = encode_rfc2822_address_header(COMMASPACE.join(email_to))
if email_cc:
msg['Cc'] = encode_rfc2822_address_header(COMMASPACE.join(email_cc))
if email_bcc:
msg['Bcc'] = encode_rfc2822_address_header(COMMASPACE.join(email_bcc))
msg['Date'] = formatdate(localtime=True)
# Custom headers may override normal headers or provide additional ones
for key, value in headers.iteritems():
msg[ustr(key).encode('utf-8')] = encode_header(value)
if html2text and subtype == 'html':
# Always provide alternative text body if possible.
text_utf8 = tools.html2text(email_body_utf8.decode('utf-8')).encode('utf-8')
alternative_part = MIMEMultipart(_subtype="alternative")
alternative_part.attach(MIMEText(text_utf8, _charset='utf-8', _subtype='plain'))
alternative_part.attach(email_text_part)
msg.attach(alternative_part)
else:
msg.attach(email_text_part)
if attachments:
for (fname, fcontent) in attachments:
filename_utf8 = ustr(fname).encode('utf-8')
part = MIMEBase('application', "octet-stream")
part.set_payload(fcontent)
Encoders.encode_base64(part)
# Force RFC2231 encoding for attachment filename
# See email.message.Message.add_header doc
part.add_header('Content-Disposition', 'attachment',
filename=('utf-8',None,filename_utf8))
msg.attach(part)
return msg
def send_email(self, cr, uid, message, mail_server_id=None, smtp_server=None, smtp_port=None,
smtp_user=None, smtp_password=None, smtp_encryption='none', smtp_debug=False,
context=None):
"""Sends an email directly (no queuing).
No retries are done, the caller should handle MailDeliveryException in order to ensure that
the mail is never lost.
If the mail_server_id is provided, sends using this mail server, ignoring other smtp_* arguments.
If mail_server_id is None and smtp_server is None, use the default mail server (highest priority).
If mail_server_id is None and smtp_server is not None, use the provided smtp_* arguments.
If both mail_server_id and smtp_server are None, look for an 'smtp_server' value in server config,
and fails if not found.
:param message: the email.message.Message to send. The envelope sender will be extracted from the
``Return-Path`` or ``From`` headers. The envelope recipients will be
extracted from the combined list of ``To``, ``CC`` and ``BCC`` headers.
:param mail_server_id: optional id of ir.mail_server to use for sending. overrides other smtp_* arguments.
:param smtp_server: optional hostname of SMTP server to use
:param smtp_encryption: one of 'none', 'starttls' or 'ssl' (see ir.mail_server fields for explanation)
:param smtp_port: optional SMTP port, if mail_server_id is not passed
:param smtp_user: optional SMTP user, if mail_server_id is not passed
:param smtp_password: optional SMTP password to use, if mail_server_id is not passed
:param smtp_debug: optional SMTP debug flag, if mail_server_id is not passed
:param debug: whether to turn on the SMTP level debugging, output to DEBUG log level
:return: the Message-ID of the message that was just sent, if successfully sent, otherwise raises
MailDeliveryException and logs root cause.
"""
smtp_from = message['Return-Path'] or message['From']
assert smtp_from, "The Return-Path or From header is required for any outbound e-mail"
# The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters.
from_rfc2822 = extract_rfc2822_addresses(smtp_from)
assert len(from_rfc2822) == 1, "Malformed 'Return-Path' or 'From' address - it may only contain plain ASCII characters"
smtp_from = from_rfc2822[0]
email_to = message['To']
email_cc = message['Cc']
email_bcc = message['Bcc']
smtp_to_list = filter(None, tools.flatten(map(extract_rfc2822_addresses,[email_to, email_cc, email_bcc])))
assert smtp_to_list, "At least one valid recipient address should be specified for outgoing emails (To/Cc/Bcc)"
# Get SMTP Server Details from Mail Server
mail_server = None
if mail_server_id:
mail_server = self.browse(cr, uid, mail_server_id)
elif not smtp_server:
mail_server_ids = self.search(cr, uid, [], order='sequence', limit=1)
if mail_server_ids:
mail_server = self.browse(cr, uid, mail_server_ids[0])
else:
# we were passed an explicit smtp_server or nothing at all
smtp_server = smtp_server or tools.config.get('smtp_server')
smtp_port = tools.config.get('smtp_port', 25) if smtp_port is None else smtp_port
smtp_user = smtp_user or tools.config.get('smtp_user')
smtp_password = smtp_password or tools.config.get('smtp_password')
if mail_server:
smtp_server = mail_server.smtp_host
smtp_user = mail_server.smtp_user
smtp_password = mail_server.smtp_pass
smtp_port = mail_server.smtp_port
smtp_encryption = mail_server.smtp_encryption
smtp_debug = smtp_debug or mail_server.smtp_debug
if not smtp_server:
raise osv.except_osv(
_("Missing SMTP Server"),
_("Please define at least one SMTP server, or provide the SMTP parameters explicitly."))
try:
message_id = message['Message-Id']
# Add email in Maildir if smtp_server contains maildir.
if smtp_server.startswith('maildir:/'):
from mailbox import Maildir
maildir_path = smtp_server[8:]
mdir = Maildir(maildir_path, factory=None, create = True)
mdir.add(message.as_string(True))
return message_id
try:
smtp = self.connect(smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption, smtp_debug)
smtp.sendmail(smtp_from, smtp_to_list, message.as_string())
finally:
try:
# Close Connection of SMTP Server
smtp.quit()
except Exception:
# ignored, just a consequence of the previous exception
pass
except Exception, e:
msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % (smtp_server, e.__class__.__name__, e)
_logger.exception(msg)
raise MailDeliveryException(_("Mail delivery failed"), msg)
return message_id
def on_change_encryption(self, cr, uid, ids, smtp_encryption):
if smtp_encryption == 'ssl':
result = {'value': {'smtp_port': 465}}
if not 'SMTP_SSL' in smtplib.__all__:
result['warning'] = {'title': _('Warning'),
'message': _('Your server does not seem to support SSL, you may want to try STARTTLS instead')}
else:
result = {'value': {'smtp_port': 25}}
return result
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -56,7 +56,7 @@ def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
class ir_model(osv.osv): class ir_model(osv.osv):
_name = 'ir.model' _name = 'ir.model'
_description = "Objects" _description = "Models"
_order = 'model' _order = 'model'
def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None): def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
@ -85,8 +85,8 @@ class ir_model(osv.osv):
return res return res
_columns = { _columns = {
'name': fields.char('Object Name', size=64, translate=True, required=True), 'name': fields.char('Model Description', size=64, translate=True, required=True),
'model': fields.char('Object', size=64, required=True, select=1), 'model': fields.char('Model', size=64, required=True, select=1),
'info': fields.text('Information'), 'info': fields.text('Information'),
'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True), 'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True), 'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
@ -97,12 +97,12 @@ class ir_model(osv.osv):
'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the object is defined or inherited'), 'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the object is defined or inherited'),
'view_ids': fields.function(_view_ids, method=True, type='one2many', obj='ir.ui.view', string='Views'), 'view_ids': fields.function(_view_ids, method=True, type='one2many', obj='ir.ui.view', string='Views'),
} }
_defaults = { _defaults = {
'model': lambda *a: 'x_', 'model': lambda *a: 'x_',
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base', 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
} }
def _check_model_name(self, cr, uid, ids, context=None): def _check_model_name(self, cr, uid, ids, context=None):
for model in self.browse(cr, uid, ids, context=context): for model in self.browse(cr, uid, ids, context=context):
if model.state=='manual': if model.state=='manual':
@ -114,9 +114,13 @@ class ir_model(osv.osv):
def _model_name_msg(self, cr, uid, ids, context=None): def _model_name_msg(self, cr, uid, ids, context=None):
return _('The Object name must start with x_ and not contain any special character !') return _('The Object name must start with x_ and not contain any special character !')
_constraints = [ _constraints = [
(_check_model_name, _model_name_msg, ['model']), (_check_model_name, _model_name_msg, ['model']),
] ]
_sql_constraints = [
('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
]
# overridden to allow searching both on model name (model field) # overridden to allow searching both on model name (model field)
# and model description (name field) # and model description (name field)
@ -616,7 +620,7 @@ class ir_model_data(osv.osv):
"""Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found""" """Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found"""
ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)]) ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
if not ids: if not ids:
raise ValueError('No references to %s.%s' % (module, xml_id)) raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
# the sql constraints ensure us we have only one result # the sql constraints ensure us we have only one result
return ids[0] return ids[0]
@ -626,7 +630,7 @@ class ir_model_data(osv.osv):
data_id = self._get_id(cr, uid, module, xml_id) data_id = self._get_id(cr, uid, module, xml_id)
res = self.read(cr, uid, data_id, ['model', 'res_id']) res = self.read(cr, uid, data_id, ['model', 'res_id'])
if not res['res_id']: if not res['res_id']:
raise ValueError('No references to %s.%s' % (module, xml_id)) raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
return (res['model'], res['res_id']) return (res['model'], res['res_id'])
def get_object(self, cr, uid, module, xml_id, context=None): def get_object(self, cr, uid, module, xml_id, context=None):

View File

@ -108,8 +108,8 @@ class ir_translation(osv.osv):
for res_id in tr: for res_id in tr:
if tr[res_id]: if tr[res_id]:
self._get_source.clear_cache(self, uid, name, tt, lang, tr[res_id]) self._get_source.clear_cache(self, uid, name, tt, lang, tr[res_id])
self._get_ids.clear_cache(self, uid, name, tt, lang, res_id)
self._get_source.clear_cache(self, uid, name, tt, lang) self._get_source.clear_cache(self, uid, name, tt, lang)
self._get_ids.clear_cache(self, uid, name, tt, lang, ids)
cr.execute('delete from ir_translation ' \ cr.execute('delete from ir_translation ' \
'where lang=%s ' \ 'where lang=%s ' \

View File

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

View File

@ -79,7 +79,10 @@ class ir_values(osv.osv):
method=True, type='text', string='Value'), method=True, type='text', string='Value'),
'object': fields.boolean('Is Object'), 'object': fields.boolean('Is Object'),
'key': fields.selection([('action','Action'),('default','Default')], 'Type', size=128, select=True), 'key': fields.selection([('action','Action'),('default','Default')], 'Type', size=128, select=True),
'key2' : fields.char('Event Type',help="The kind of action or button in the client side that will trigger the action.", size=128, select=True), 'key2' : fields.char('Event Type', size=128, select=True, help="The kind of action or button on the client side "
"that will trigger the action. One of: "
"client_action_multi, client_action_relate, tree_but_open, "
"client_print_multi"),
'meta': fields.text('Meta Datas'), 'meta': fields.text('Meta Datas'),
'meta_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle, 'meta_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle,
method=True, type='text', string='Metadata'), method=True, type='text', string='Metadata'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,10 +29,10 @@ import res_bank
import res_config import res_config
import res_currency import res_currency
import res_company import res_company
import res_user import res_users
import res_request import res_request
import res_lang import res_lang
import res_log import res_log
import res_widget import res_widget
import ir_property import ir_property

View File

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

View File

@ -143,6 +143,9 @@ class res_company(osv.osv):
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32), 'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
'company_registry': fields.char('Company Registry', size=64), 'company_registry': fields.char('Company Registry', size=64),
} }
_sql_constraints = [
('name_uniq', 'unique (name)', 'The company name must be unique !')
]
def _search(self, cr, uid, args, offset=0, limit=None, order=None, def _search(self, cr, uid, args, offset=0, limit=None, order=None,
context=None, count=False, access_rights_uid=None): context=None, count=False, access_rights_uid=None):

View File

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

View File

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

View File

@ -194,7 +194,7 @@ class lang(osv.osv):
trans_obj.unlink(cr, uid, trans_ids, context=context) trans_obj.unlink(cr, uid, trans_ids, context=context)
return super(lang, self).unlink(cr, uid, ids, context=context) return super(lang, self).unlink(cr, uid, ids, context=context)
def format(self, cr, uid, ids, percent, value, grouping=False, monetary=False): def format(self, cr, uid, ids, percent, value, grouping=False, monetary=False, context=None):
""" Format() will return the language-specific output for float values""" """ Format() will return the language-specific output for float values"""
if percent[0] != '%': if percent[0] != '%':

View File

@ -90,67 +90,88 @@
<record id="res_partner_asus" model="res.partner"> <record id="res_partner_asus" model="res.partner">
<field name="name">ASUStek</field> <field name="name">ASUStek</field>
<field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="supplier">1</field> <field name="supplier">1</field>
<field eval="0" name="customer"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="website">www.asustek.com</field>
</record> </record>
<record id="res_partner_agrolait" model="res.partner"> <record id="res_partner_agrolait" model="res.partner">
<field name="name">Agrolait</field> <field name="name">Agrolait</field>
<field eval="[(6, 0, [ref('res_partner_category_8')])]" name="category_id"/> <field eval="[(6, 0, [ref('base.res_partner_category_0')])]" name="category_id"/>
<field name="user_id" ref="base.user_root"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="website">www.agrolait.com</field>
</record> </record>
<record id="res_partner_c2c" model="res.partner"> <record id="res_partner_c2c" model="res.partner">
<field name="name">Camptocamp</field> <field name="name">Camptocamp</field>
<field eval="[(6, 0, [ref('res_partner_category_10'), ref('res_partner_category_5')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_10'), ref('res_partner_category_5')])]" name="category_id"/>
<field name="supplier">1</field> <field name="supplier">1</field>
<field name="user_id" ref="base.user_root"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="website">www.camptocamp.com</field>
</record> </record>
<record id="res_partner_sednacom" model="res.partner"> <record id="res_partner_sednacom" model="res.partner">
<field name="website">http://www.syleam.fr</field> <field name="website">www.syleam.fr</field>
<field name="name">Syleam</field> <field name="name">Syleam</field>
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
</record> </record>
<record id="res_partner_thymbra" model="res.partner"> <record id="res_partner_thymbra" model="res.partner">
<field name="name">Thymbra</field> <field name="name">Thymbra</field>
<field name="user_id" ref="base.user_root"/>
<field eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/>
<field name="website">www.thymbra.com/</field>
</record> </record>
<record id="res_partner_desertic_hispafuentes" model="res.partner"> <record id="res_partner_desertic_hispafuentes" model="res.partner">
<field name="name">Axelor</field> <field name="name">Axelor</field>
<field eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/>
<field name="supplier">1</field> <field name="supplier">1</field>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
<field name="website">www.axelor.com/</field>
</record> </record>
<record id="res_partner_tinyatwork" model="res.partner"> <record id="res_partner_tinyatwork" model="res.partner">
<field name="name">Tiny AT Work</field> <field name="name">Tiny AT Work</field>
<field name="user_id" ref="base.user_root"/>
<field eval="[(6, 0, [ref('res_partner_category_5'), ref('res_partner_category_10')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_5'), ref('res_partner_category_10')])]" name="category_id"/>
<field name="website">www.tinyatwork.com/</field>
</record> </record>
<record id="res_partner_2" model="res.partner"> <record id="res_partner_2" model="res.partner">
<field name="name">Bank Wealthy and sons</field> <field name="name">Bank Wealthy and sons</field>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">www.wealthyandsons.com/</field>
</record> </record>
<record id="res_partner_3" model="res.partner"> <record id="res_partner_3" model="res.partner">
<field name="name">China Export</field> <field name="name">China Export</field>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">www.chinaexport.com/</field>
</record> </record>
<record id="res_partner_4" model="res.partner"> <record id="res_partner_4" model="res.partner">
<field name="name">Distrib PC</field> <field name="name">Distrib PC</field>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="supplier">1</field> <field name="supplier">1</field>
<field eval="0" name="customer"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="website">www.distribpc.com/</field>
</record> </record>
<record id="res_partner_5" model="res.partner"> <record id="res_partner_5" model="res.partner">
<field name="name">Ecole de Commerce de Liege</field> <field name="name">Ecole de Commerce de Liege</field>
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
<field name="website">www.eci-liege.info//</field>
</record> </record>
<record id="res_partner_6" model="res.partner"> <record id="res_partner_6" model="res.partner">
<field name="name">Elec Import</field> <field name="name">Elec Import</field>
<field name="user_id" ref="user_demo"/> <field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="supplier">1</field> <field name="supplier">1</field>
<field eval="0" name="customer"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
</record> </record>
<record id="res_partner_maxtor" model="res.partner"> <record id="res_partner_maxtor" model="res.partner">
@ -159,6 +180,7 @@
<field name="user_id" ref="user_demo"/> <field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="supplier">1</field> <field name="supplier">1</field>
<field eval="0" name="customer"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
</record> </record>
<record id="res_partner_seagate" model="res.partner"> <record id="res_partner_seagate" model="res.partner">
@ -177,11 +199,11 @@
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
</record> </record>
<record id="res_partner_9" model="res.partner"> <record id="res_partner_9" model="res.partner">
<field name="website">http://balmerinc.com</field> <field name="website">www.balmerinc.com</field>
<field name="name">BalmerInc S.A.</field> <field name="name">BalmerInc S.A.</field>
<field eval="12000.00" name="credit_limit"/> <field eval="12000.00" name="credit_limit"/>
<field name="ref">or</field> <field name="ref">or</field>
<field name="user_id" ref="user_demo"/> <field name="user_id" ref="base.user_root"/>
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
</record> </record>
@ -190,6 +212,7 @@
<field name="ean13">3020170000003</field> <field name="ean13">3020170000003</field>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
</record> </record>
<record id="res_partner_11" model="res.partner"> <record id="res_partner_11" model="res.partner">
<field name="name">Leclerc</field> <field name="name">Leclerc</field>
@ -205,6 +228,7 @@
<field name="parent_id" ref="res_partner_10"/> <field name="parent_id" ref="res_partner_10"/>
<field eval="[(6, 0, [ref('res_partner_category_11')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_11')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
</record> </record>
<record id="res_partner_15" model="res.partner"> <record id="res_partner_15" model="res.partner">
<field name="name">Magazin BML 1</field> <field name="name">Magazin BML 1</field>
@ -219,6 +243,8 @@
<field name="name">Université de Liège</field> <field name="name">Université de Liège</field>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/> <field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
<field name="website">http://www.ulg.ac.be/</field>
</record> </record>
<!-- <!--
@ -230,16 +256,19 @@
<field model="res.users" name="user_id" search="[('name', '=', u'Thomas Lebrun')]"/> <field model="res.users" name="user_id" search="[('name', '=', u'Thomas Lebrun')]"/>
<field name="name">Dubois sprl</field> <field name="name">Dubois sprl</field>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="website">http://www.dubois.be/</field>
</record> </record>
<record id="res_partner_ericdubois0" model="res.partner"> <record id="res_partner_ericdubois0" model="res.partner">
<field name="name">Eric Dubois</field> <field name="name">Eric Dubois</field>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
</record> </record>
<record id="res_partner_fabiendupont0" model="res.partner"> <record id="res_partner_fabiendupont0" model="res.partner">
<field name="name">Fabien Dupont</field> <field name="name">Fabien Dupont</field>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
</record> </record>
<record id="res_partner_lucievonck0" model="res.partner"> <record id="res_partner_lucievonck0" model="res.partner">
@ -250,32 +279,41 @@
<record id="res_partner_notsotinysarl0" model="res.partner"> <record id="res_partner_notsotinysarl0" model="res.partner">
<field name="name">NotSoTiny SARL</field> <field name="name">NotSoTiny SARL</field>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">notsotiny.be</field>
</record> </record>
<record id="res_partner_theshelvehouse0" model="res.partner"> <record id="res_partner_theshelvehouse0" model="res.partner">
<field name="name">The Shelve House</field> <field name="name">The Shelve House</field>
<field eval="[(6,0,[ref('res_partner_category_retailers0')])]" name="category_id"/> <field eval="[(6,0,[ref('res_partner_category_retailers0')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
</record> </record>
<record id="res_partner_vickingdirect0" model="res.partner"> <record id="res_partner_vickingdirect0" model="res.partner">
<field name="name">Vicking Direct</field> <field name="name">Vicking Direct</field>
<field eval="[(6,0,[ref('res_partner_category_miscellaneoussuppliers0')])]" name="category_id"/> <field eval="[(6,0,[ref('res_partner_category_miscellaneoussuppliers0')])]" name="category_id"/>
<field name="supplier">1</field> <field name="supplier">1</field>
<field name="customer">0</field>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="website">vicking-direct.be</field>
</record> </record>
<record id="res_partner_woodywoodpecker0" model="res.partner"> <record id="res_partner_woodywoodpecker0" model="res.partner">
<field name="name">Wood y Wood Pecker</field> <field name="name">Wood y Wood Pecker</field>
<field eval="[(6,0,[ref('res_partner_category_woodsuppliers0')])]" name="category_id"/> <field eval="[(6,0,[ref('res_partner_category_woodsuppliers0')])]" name="category_id"/>
<field name="supplier">1</field> <field name="supplier">1</field>
<field eval="0" name="customer"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="website">woodywoodpecker.com</field>
</record> </record>
<record id="res_partner_zerooneinc0" model="res.partner"> <record id="res_partner_zerooneinc0" model="res.partner">
<field name="name">ZeroOne Inc</field> <field name="name">ZeroOne Inc</field>
<field eval="[(6,0,[ref('res_partner_category_consumers0')])]" name="category_id"/> <field eval="[(6,0,[ref('res_partner_category_consumers0')])]" name="category_id"/>
<field name="address" eval="[]"/> <field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">http://www.zerooneinc.com/</field>
</record> </record>
<!-- <!--
@ -312,6 +350,7 @@
<field name="email">info@axelor.com</field> <field name="email">info@axelor.com</field>
<field name="phone">+33 1 64 61 04 01</field> <field name="phone">+33 1 64 61 04 01</field>
<field name="street">12 rue Albert Einstein</field> <field name="street">12 rue Albert Einstein</field>
<field name="type">default</field>
<field name="partner_id" ref="res_partner_desertic_hispafuentes"/> <field name="partner_id" ref="res_partner_desertic_hispafuentes"/>
</record> </record>
<record id="res_partner_address_3" model="res.partner.address"> <record id="res_partner_address_3" model="res.partner.address">
@ -329,6 +368,8 @@
<field name="zip">23410</field> <field name="zip">23410</field>
<field model="res.country" name="country_id" search="[('name','=','Taiwan')]"/> <field model="res.country" name="country_id" search="[('name','=','Taiwan')]"/>
<field name="street">31 Hong Kong street</field> <field name="street">31 Hong Kong street</field>
<field name="email">info@asustek.com</field>
<field name="phone">+ 1 64 61 04 01</field>
<field name="type">default</field> <field name="type">default</field>
<field name="partner_id" ref="res_partner_asus"/> <field name="partner_id" ref="res_partner_asus"/>
</record> </record>
@ -338,6 +379,8 @@
<field name="zip">23540</field> <field name="zip">23540</field>
<field model="res.country" name="country_id" search="[('name','=','China')]"/> <field model="res.country" name="country_id" search="[('name','=','China')]"/>
<field name="street">56 Beijing street</field> <field name="street">56 Beijing street</field>
<field name="email">info@maxtor.com</field>
<field name="phone">+ 11 8528 456 789</field>
<field name="type">default</field> <field name="type">default</field>
<field name="partner_id" ref="res_partner_maxtor"/> <field name="partner_id" ref="res_partner_maxtor"/>
</record> </record>
@ -348,6 +391,8 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/> <field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">23 rue du Vieux Bruges</field> <field name="street">23 rue du Vieux Bruges</field>
<field name="type">default</field> <field name="type">default</field>
<field name="email">info@elecimport.com</field>
<field name="phone">+ 32 025 897 456</field>
<field name="partner_id" ref="res_partner_6"/> <field name="partner_id" ref="res_partner_6"/>
</record> </record>
<record id="res_partner_address_7" model="res.partner.address"> <record id="res_partner_address_7" model="res.partner.address">
@ -357,6 +402,8 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/> <field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">42 rue de la Lesse</field> <field name="street">42 rue de la Lesse</field>
<field name="type">default</field> <field name="type">default</field>
<field name="email">info@distribpc.com</field>
<field name="phone">+ 32 081256987</field>
<field name="partner_id" ref="res_partner_4"/> <field name="partner_id" ref="res_partner_4"/>
</record> </record>
<record id="res_partner_address_8" model="res.partner.address"> <record id="res_partner_address_8" model="res.partner.address">
@ -366,7 +413,10 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/> <field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">69 rue de Chimay</field> <field name="street">69 rue de Chimay</field>
<field name="type">default</field> <field name="type">default</field>
<field name="email">s.l@agrolait.be</field>
<field name="phone">003281588558</field>
<field name="partner_id" ref="res_partner_agrolait"/> <field name="partner_id" ref="res_partner_agrolait"/>
<field name="title" ref="base.res_partner_title_madam"/>
</record> </record>
<record id="res_partner_address_8delivery" model="res.partner.address"> <record id="res_partner_address_8delivery" model="res.partner.address">
<field name="city">Wavre</field> <field name="city">Wavre</field>
@ -375,7 +425,10 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/> <field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">71 rue de Chimay</field> <field name="street">71 rue de Chimay</field>
<field name="type">delivery</field> <field name="type">delivery</field>
<field name="email">p.l@agrolait.be</field>
<field name="phone">003281588557</field>
<field name="partner_id" ref="res_partner_agrolait"/> <field name="partner_id" ref="res_partner_agrolait"/>
<field name="title" ref="base.res_partner_title_sir"/>
</record> </record>
<record id="res_partner_address_8invoice" model="res.partner.address"> <record id="res_partner_address_8invoice" model="res.partner.address">
<field name="city">Wavre</field> <field name="city">Wavre</field>
@ -384,7 +437,10 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/> <field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">69 rue de Chimay</field> <field name="street">69 rue de Chimay</field>
<field name="type">invoice</field> <field name="type">invoice</field>
<field name="email">serge.l@agrolait.be</field>
<field name="phone">003281588556</field>
<field name="partner_id" ref="res_partner_agrolait"/> <field name="partner_id" ref="res_partner_agrolait"/>
<field name="title" ref="base.res_partner_title_sir"/>
</record> </record>
<record id="res_partner_address_9" model="res.partner.address"> <record id="res_partner_address_9" model="res.partner.address">
<field name="city">Paris</field> <field name="city">Paris</field>
@ -393,7 +449,10 @@
<field model="res.country" name="country_id" search="[('name','=','France')]"/> <field model="res.country" name="country_id" search="[('name','=','France')]"/>
<field name="street">1 rue Rockfeller</field> <field name="street">1 rue Rockfeller</field>
<field name="type">default</field> <field name="type">default</field>
<field name="email">a.g@wealthyandsons.com</field>
<field name="phone">003368978776</field>
<field name="partner_id" ref="res_partner_2"/> <field name="partner_id" ref="res_partner_2"/>
<field name="title" ref="base.res_partner_title_sir"/>
</record> </record>
<record id="res_partner_address_11" model="res.partner.address"> <record id="res_partner_address_11" model="res.partner.address">
<field name="city">Alencon</field> <field name="city">Alencon</field>
@ -412,48 +471,84 @@
<field name="zip">6985</field> <field name="zip">6985</field>
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/> <field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">2 Impasse de la Soif</field> <field name="street">2 Impasse de la Soif</field>
<field name="email">k.lesbrouffe@eci-liege.info</field>
<field name="phone">+32 421 52571</field>
<field name="type">default</field> <field name="type">default</field>
<field name="partner_id" ref="res_partner_5"/> <field name="partner_id" ref="res_partner_5"/>
</record> </record>
<record id="res_partner_address_zen" model="res.partner.address"> <record id="res_partner_address_zen" model="res.partner.address">
<field name="city">Shanghai</field> <field name="city">Shanghai</field>
<field name="name">Zen</field> <field name="name">Zen</field>
<field name="zip">4785552</field> <field name="zip">478552</field>
<field model="res.country" name="country_id" search="[('name','=','China')]"/> <field model="res.country" name="country_id" search="[('name','=','China')]"/>
<field name="street">52 Chop Suey street</field> <field name="street">52 Chop Suey street</field>
<field name="type">default</field> <field name="type">default</field>
<field name="email">zen@chinaexport.com</field>
<field name="phone">+86-751-64845671</field>
<field name="partner_id" ref="res_partner_3"/> <field name="partner_id" ref="res_partner_3"/>
</record> </record>
<record id="res_partner_address_12" model="res.partner.address"> <record id="res_partner_address_12" model="res.partner.address">
<field name="type">default</field> <field name="type">default</field>
<field name="name">Centrale</field> <field name="city">Grenoble</field>
<field name="name">Loïc Dupont</field>
<field name="zip">38100</field>
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
<field name="street">Rue Lavoisier 145</field>
<field name="type">default</field>
<field name="email">l.dupont@tecsas.fr</field>
<field name="phone">+33-658-256545</field>
<field name="partner_id" ref="res_partner_10"/> <field name="partner_id" ref="res_partner_10"/>
</record> </record>
<record id="res_partner_address_13" model="res.partner.address"> <record id="res_partner_address_13" model="res.partner.address">
<field name="type">default</field> <field name="type">default</field>
<field name="name">Centrale d'achats 1</field> <field name="name">Carl François</field>
<field name="city">Bruxelles</field>
<field name="zip">1000</field>
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">89 Chaussée de Waterloo</field>
<field name="email">carl.françois@bml.be</field>
<field name="phone">+32-258-256545</field>
<field name="partner_id" ref="res_partner_14"/> <field name="partner_id" ref="res_partner_14"/>
</record> </record>
<record id="res_partner_address_14" model="res.partner.address"> <record id="res_partner_address_14" model="res.partner.address">
<field name="type">default</field> <field name="type">default</field>
<field name="name">Shop 1</field> <field name="name">Lucien Ferguson</field>
<field name="street">89 Chaussée de Liège</field>
<field name="city">Namur</field>
<field name="zip">5000</field>
<field name="email">lucien.ferguson@bml.be</field>
<field name="phone">+32-621-568978</field>
<field name="partner_id" ref="res_partner_15"/> <field name="partner_id" ref="res_partner_15"/>
</record> </record>
<record id="res_partner_address_15" model="res.partner.address"> <record id="res_partner_address_15" model="res.partner.address">
<field name="type">default</field> <field name="type">default</field>
<field name="name">Shop 2</field> <field name="name">Marine Leclerc</field>
<field name="street">rue Grande</field>
<field name="city">Brest</field>
<field name="zip">29200</field>
<field name="email">marine@leclerc.fr</field>
<field name="phone">+33-298.334558</field>
<field name="partner_id" ref="res_partner_11"/> <field name="partner_id" ref="res_partner_11"/>
</record> </record>
<record id="res_partner_address_16" model="res.partner.address"> <record id="res_partner_address_16" model="res.partner.address">
<field name="type">default</field> <field name="type">invoice</field>
<field name="name">Shop 3</field> <field name="name">Claude Leclerc</field>
<field name="street">rue Grande</field>
<field name="city">Brest</field>
<field name="zip">29200</field>
<field name="email">claude@leclerc.fr</field>
<field name="phone">+33-298.334598</field>
<field name="partner_id" ref="res_partner_11"/> <field name="partner_id" ref="res_partner_11"/>
</record> </record>
<record id="res_partner_address_accent" model="res.partner.address"> <record id="res_partner_address_accent" model="res.partner.address">
<field name="type">default</field> <field name="type">default</field>
<field name="city">Liège</field> <field name="name">Martine Ohio</field>
<field name="street">Université de Liège</field> <field name="street">Place du 20Août</field>
<field name="city">Liège</field>
<field name="zip">4000</field>
<field name="email">martine.ohio@ulg.ac.be</field>
<field name="phone">+32-45895245</field>
<field name="partner_id" ref="res_partner_accent"/> <field name="partner_id" ref="res_partner_accent"/>
</record> </record>
<record id="res_partner_address_Camptocamp" model="res.partner.address"> <record id="res_partner_address_Camptocamp" model="res.partner.address">
@ -472,6 +567,8 @@
<field name="zip">95014</field> <field name="zip">95014</field>
<field model="res.country" name="country_id" search="[('name','=','United States')]"/> <field model="res.country" name="country_id" search="[('name','=','United States')]"/>
<field name="street">10200 S. De Anza Blvd</field> <field name="street">10200 S. De Anza Blvd</field>
<field name="email">info@seagate.com</field>
<field name="phone">+1 408 256987</field>
<field name="type">default</field> <field name="type">default</field>
<field name="partner_id" ref="res_partner_seagate"/> <field name="partner_id" ref="res_partner_seagate"/>
</record> </record>
@ -552,7 +649,12 @@
<record id="res_partner_address_brussels0" model="res.partner.address"> <record id="res_partner_address_brussels0" model="res.partner.address">
<field eval="'Brussels'" name="city"/> <field eval="'Brussels'" name="city"/>
<field eval="'Brussels'" name="name"/> <field eval="'Leen Vandenloep'" name="name"/>
<field eval="'Puurs'" name="city"/>
<field eval="'2870'" name="zip"/>
<field name="country_id" ref="base.be"/>
<field eval="'(+32).70.12.85.00'" name="phone"/>
<field eval="'Schoonmansveld 28'" name="street"/>
<field name="partner_id" ref="res_partner_vickingdirect0"/> <field name="partner_id" ref="res_partner_vickingdirect0"/>
<field name="country_id" ref="base.be"/> <field name="country_id" ref="base.be"/>
</record> </record>
@ -562,6 +664,7 @@
<field eval="'Kainuu'" name="city"/> <field eval="'Kainuu'" name="city"/>
<field eval="'Roger Pecker'" name="name"/> <field eval="'Roger Pecker'" name="name"/>
<field name="partner_id" ref="res_partner_woodywoodpecker0"/> <field name="partner_id" ref="res_partner_woodywoodpecker0"/>
<field eval="'(+358).9.589 689'" name="phone"/>
<field name="country_id" ref="base.fi"/> <field name="country_id" ref="base.fi"/>
</record> </record>
@ -597,10 +700,13 @@
<record id="res_partner_address_ericdubois0" model="res.partner.address"> <record id="res_partner_address_ericdubois0" model="res.partner.address">
<field eval="'Mons'" name="city"/> <field eval="'Mons'" name="city"/>
<field eval="'Eric Dubois'" name="name"/>
<field eval="'7000'" name="zip"/> <field eval="'7000'" name="zip"/>
<field name="partner_id" ref="res_partner_ericdubois0"/> <field name="partner_id" ref="res_partner_ericdubois0"/>
<field name="country_id" ref="base.be"/> <field name="country_id" ref="base.be"/>
<field eval="'Chaussée de Binche, 27'" name="street"/> <field eval="'Chaussée de Binche, 27'" name="street"/>
<field eval="'e.dubois@gmail.com'" name="email"/>
<field eval="'(+32).758 958 789'" name="phone"/>
</record> </record>

View File

@ -147,25 +147,16 @@ class users(osv.osv):
return cr.fetchall() return cr.fetchall()
def send_welcome_email(self, cr, uid, id, context=None): def send_welcome_email(self, cr, uid, id, context=None):
logger= netsvc.Logger() if isinstance(id,list): id = id[0]
user = self.pool.get('res.users').read(cr, uid, id, context=context) user = self.read(cr, uid, id, ['email','login','name', 'user_email'], context=context)
if not user.get('email'): email = user['email'] or user['user_email']
return False
if not tools.config.get('smtp_server'):
logger.notifyChannel('mails', netsvc.LOG_WARNING,
_('"smtp_server" needs to be set to send mails to users'))
return False
if not tools.config.get('email_from'):
logger.notifyChannel("mails", netsvc.LOG_WARNING,
_('"email_from" needs to be set to send welcome mails '
'to users'))
return False
return tools.email_send(email_from=None, email_to=[user['email']], ir_mail_server = self.pool.get('ir.mail_server')
subject=self.get_welcome_mail_subject( msg = ir_mail_server.build_email(email_from=None, # take config default
cr, uid, context=context), email_to=[email],
body=self.get_welcome_mail_body( subject=self.get_welcome_mail_subject(cr, uid, context=context),
cr, uid, context=context) % user) body=(self.get_welcome_mail_body(cr, uid, context=context) % user))
return ir_mail_server.send_email(cr, uid, msg, context=context)
def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None): def _set_interface_type(self, cr, uid, ids, name, value, arg, context=None):
"""Implementation of 'view' function field setter, sets the type of interface of the users. """Implementation of 'view' function field setter, sets the type of interface of the users.
@ -347,7 +338,7 @@ class users(osv.osv):
} }
# User can write to a few of her own fields (but not her groups for example) # User can write to a few of her own fields (but not her groups for example)
SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email'] SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
def write(self, cr, uid, ids, values, context=None): def write(self, cr, uid, ids, values, context=None):
if not hasattr(ids, '__iter__'): if not hasattr(ids, '__iter__'):
@ -562,7 +553,7 @@ class users_implied(osv.osv):
_inherit = 'res.users' _inherit = 'res.users'
def create(self, cr, uid, values, context=None): def create(self, cr, uid, values, context=None):
groups = values.pop('groups_id') groups = values.pop('groups_id', None)
user_id = super(users_implied, self).create(cr, uid, values, context) user_id = super(users_implied, self).create(cr, uid, values, context)
if groups: if groups:
# delegate addition of groups to add implied groups # delegate addition of groups to add implied groups

View File

@ -20,7 +20,7 @@
############################################################################## ##############################################################################
import partner_sms_send import partner_sms_send
import partner_wizard_spam import partner_wizard_massmail
import partner_clear_ids import partner_clear_ids
import partner_wizard_ean_check import partner_wizard_ean_check

View File

@ -19,15 +19,16 @@
# #
############################################################################## ##############################################################################
import netsvc
import tools
from osv import fields, osv from osv import fields, osv
import re import re
import logging
class partner_wizard_spam(osv.osv_memory): _logger = logging.getLogger('mass.mailing')
class partner_massmail_wizard(osv.osv_memory):
""" Mass Mailing """ """ Mass Mailing """
_name = "partner.wizard.spam" _name = "partner.massmail.wizard"
_description = "Mass Mailing" _description = "Mass Mailing"
_columns = { _columns = {
@ -37,45 +38,45 @@ class partner_wizard_spam(osv.osv_memory):
} }
def mass_mail_send(self, cr, uid, ids, context): def mass_mail_send(self, cr, uid, ids, context):
""" """Send the given mail to all partners whose ids
Send Email are present in ``context['active_ids']``, to
all addresses with an email set.
@param cr: the current row, from the database cursor. :param dict context: ``context['active_ids']``
@param uid: the current users ID for security checks. should contain the list of
@param ids: the ID or list of IDs ids of the partners who should
@param context: A standard dictionary receive the mail.
""" """
nbr = 0 nbr = 0
partner_pool = self.pool.get('res.partner') partner_pool = self.pool.get('res.partner')
data = self.browse(cr, uid, ids[0], context=context) data = self.browse(cr, uid, ids[0], context=context)
event_pool = self.pool.get('res.partner.event') event_pool = self.pool.get('res.partner.event')
active_ids = context and context.get('active_ids', []) assert context['active_model'] == 'res.partner', 'This wizard must be started on a list of Partners'
active_ids = context.get('active_ids', [])
partners = partner_pool.browse(cr, uid, active_ids, context) partners = partner_pool.browse(cr, uid, active_ids, context)
type_ = 'plain' subtype = 'plain'
if re.search('(<(pre)|[pubi].*>)', data.text): if re.search('(<(pre)|[pubi].*>)', data.text):
type_ = 'html' subtype = 'html'
ir_mail_server = self.pool.get('ir.mail_server')
emails_seen = set()
for partner in partners: for partner in partners:
for adr in partner.address: for adr in partner.address:
if adr.email: if adr.email and not adr.email in emails_seen:
name = adr.name or partner.name try:
to = '"%s" <%s>' % (name, adr.email) emails_seen.add(adr.email)
#TODO: add some tests to check for invalid email addresses name = adr.name or partner.name
#CHECKME: maybe we should use res.partner/email_send to = '"%s" <%s>' % (name, adr.email)
tools.email_send(data.email_from, msg = ir_mail_server.build_email(data.email_from, [to], data.subject, data.text, subtype=subtype)
[to], if ir_mail_server.send_email(cr, uid, msg):
data.subject, nbr += 1
data.text, except Exception:
subtype=type_, #ignore failed deliveries, will be logged anyway
openobject_id="res.partner-%s"%partner.id) pass
nbr += 1
event_pool.create(cr, uid, event_pool.create(cr, uid,
{'name': 'Email(s) sent through mass mailing', {'name': 'Email(s) sent through mass mailing',
'partner_id': partner.id, 'partner_id': partner.id,
'description': data.text }) 'description': data.text })
#TODO: log number of message sent _logger.info('Mass-mailing wizard sent %s emails', nbr)
return {'email_sent': nbr} return {'email_sent': nbr}
partner_wizard_spam()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -4,7 +4,7 @@
<record id="view_partner_mass_mail" model="ir.ui.view"> <record id="view_partner_mass_mail" model="ir.ui.view">
<field name="name">Mass Mailing</field> <field name="name">Mass Mailing</field>
<field name="model">partner.wizard.spam</field> <field name="model">partner.massmail.wizard</field>
<field name="type">form</field> <field name="type">form</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Mass Mailing" col="4"> <form string="Mass Mailing" col="4">
@ -24,7 +24,7 @@
</record> </record>
<act_window name="Mass Mailing" <act_window name="Mass Mailing"
res_model="partner.wizard.spam" res_model="partner.massmail.wizard"
src_model="res.partner" src_model="res.partner"
view_mode="form" view_mode="form"
target="new" target="new"

View File

@ -212,6 +212,7 @@
<rng:optional><rng:attribute name="help"/></rng:optional> <rng:optional><rng:attribute name="help"/></rng:optional>
<rng:optional><rng:attribute name="width"/></rng:optional> <rng:optional><rng:attribute name="width"/></rng:optional>
<rng:optional><rng:attribute name="wrap"/></rng:optional> <rng:optional><rng:attribute name="wrap"/></rng:optional>
<rng:optional><rng:attribute name="name"/></rng:optional>
<rng:zeroOrMore> <rng:zeroOrMore>
<rng:choice> <rng:choice>
<rng:ref name="notebook"/> <rng:ref name="notebook"/>

View File

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

View File

@ -48,6 +48,7 @@
"access_res_country_state_group_user","res_country_state group_user","model_res_country_state","group_partner_manager",1,1,1,1 "access_res_country_state_group_user","res_country_state group_user","model_res_country_state","group_partner_manager",1,1,1,1
"access_res_currency_group_all","res_currency group_all","model_res_currency",,1,0,0,0 "access_res_currency_group_all","res_currency group_all","model_res_currency",,1,0,0,0
"access_res_currency_rate_group_all","res_currency_rate group_all","model_res_currency_rate",,1,0,0,0 "access_res_currency_rate_group_all","res_currency_rate group_all","model_res_currency_rate",,1,0,0,0
"access_res_currency_rate_type_group_all","res_currency_rate_type group_all","model_res_currency_rate_type",,1,0,0,0
"access_res_currency_group_system","res_currency group_system","model_res_currency","group_system",1,1,1,1 "access_res_currency_group_system","res_currency group_system","model_res_currency","group_system",1,1,1,1
"access_res_currency_rate_group_system","res_currency_rate group_system","model_res_currency_rate","group_system",1,1,1,1 "access_res_currency_rate_group_system","res_currency_rate group_system","model_res_currency_rate","group_system",1,1,1,1
"access_res_groups_group_erp_manager","res_groups group_erp_manager","model_res_groups","group_erp_manager",1,1,1,1 "access_res_groups_group_erp_manager","res_groups group_erp_manager","model_res_groups","group_erp_manager",1,1,1,1
@ -123,4 +124,6 @@
"access_res_widget_user","res.widget.user","model_res_widget",,1,0,0,0 "access_res_widget_user","res.widget.user","model_res_widget",,1,0,0,0
"access_res_log_all","res.log","model_res_log",,1,1,1,1 "access_res_log_all","res.log","model_res_log",,1,1,1,1
"access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0 "access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0
"access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0
"access_ir_actions_todo_category","ir_actions_todo_category","model_ir_actions_todo_category","group_system",1,1,1,1 "access_ir_actions_todo_category","ir_actions_todo_category","model_ir_actions_todo_category","group_system",1,1,1,1
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0

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

View File

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

View File

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

View File

@ -347,7 +347,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""") cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
for (model, name) in cr.fetchall(): for (model, name) in cr.fetchall():
model_obj = pool.get(model) model_obj = pool.get(model)
if isinstance(model_obj, osv.osv.osv_memory): if isinstance(model_obj, osv.osv.osv_memory) and not isinstance(model_obj, osv.osv.osv):
logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name)) logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
cr.execute("SELECT model from ir_model") cr.execute("SELECT model from ir_model")

View File

@ -22,10 +22,14 @@
""" Models registries. """ Models registries.
""" """
import threading
import logging
import openerp.sql_db import openerp.sql_db
import openerp.osv.orm import openerp.osv.orm
import openerp.modules.db
import openerp.tools.config
class Registry(object): class Registry(object):
""" Model registry for a particular database. """ Model registry for a particular database.
@ -44,6 +48,14 @@ class Registry(object):
self.db_name = db_name self.db_name = db_name
self.db = openerp.sql_db.db_connect(db_name) self.db = openerp.sql_db.db_connect(db_name)
cr = self.db.cursor()
has_unaccent = openerp.modules.db.has_unaccent(cr)
if openerp.tools.config['unaccent'] and not has_unaccent:
logger = logging.getLogger('unaccent')
logger.warning("The option --unaccent was given but no unaccent() function was found in database.")
self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent
cr.close()
def do_parent_store(self, cr): def do_parent_store(self, cr):
for o in self._init_parent: for o in self._init_parent:
self.get(o)._parent_store_compute(cr) self.get(o)._parent_store_compute(cr)
@ -93,7 +105,6 @@ class Registry(object):
for model in self.models.itervalues(): for model in self.models.itervalues():
model.clear_caches() model.clear_caches()
class RegistryManager(object): class RegistryManager(object):
""" Model registries manager. """ Model registries manager.
@ -105,19 +116,20 @@ class RegistryManager(object):
# Mapping between db name and model registry. # Mapping between db name and model registry.
# Accessed through the methods below. # Accessed through the methods below.
registries = {} registries = {}
registries_lock = threading.RLock()
@classmethod @classmethod
def get(cls, db_name, force_demo=False, status=None, update_module=False, def get(cls, db_name, force_demo=False, status=None, update_module=False,
pooljobs=True): pooljobs=True):
""" Return a registry for a given database name.""" """ Return a registry for a given database name."""
with cls.registries_lock:
if db_name in cls.registries: if db_name in cls.registries:
registry = cls.registries[db_name] registry = cls.registries[db_name]
else: else:
registry = cls.new(db_name, force_demo, status, registry = cls.new(db_name, force_demo, status,
update_module, pooljobs) update_module, pooljobs)
return registry return registry
@classmethod @classmethod
@ -128,42 +140,43 @@ class RegistryManager(object):
The (possibly) previous registry for that database name is discarded. The (possibly) previous registry for that database name is discarded.
""" """
import openerp.modules import openerp.modules
registry = Registry(db_name) with cls.registries_lock:
registry = Registry(db_name)
# Initializing a registry will call general code which will in turn # Initializing a registry will call general code which will in turn
# call registries.get (this object) to obtain the registry being # call registries.get (this object) to obtain the registry being
# initialized. Make it available in the registries dictionary then # initialized. Make it available in the registries dictionary then
# remove it if an exception is raised. # remove it if an exception is raised.
cls.delete(db_name) cls.delete(db_name)
cls.registries[db_name] = registry cls.registries[db_name] = registry
try: try:
# This should be a method on Registry # This should be a method on Registry
openerp.modules.load_modules(registry.db, force_demo, status, update_module) openerp.modules.load_modules(registry.db, force_demo, status, update_module)
except Exception: except Exception:
del cls.registries[db_name] del cls.registries[db_name]
raise raise
cr = registry.db.cursor() cr = registry.db.cursor()
try: try:
registry.do_parent_store(cr) registry.do_parent_store(cr)
registry.get('ir.actions.report.xml').register_all(cr) registry.get('ir.actions.report.xml').register_all(cr)
cr.commit() cr.commit()
finally: finally:
cr.close() cr.close()
if pooljobs: if pooljobs:
registry.get('ir.cron').restart(registry.db.dbname) registry.get('ir.cron').restart(registry.db.dbname)
return registry return registry
@classmethod @classmethod
def delete(cls, db_name): def delete(cls, db_name):
""" Delete the registry linked to a given database. """ """ Delete the registry linked to a given database. """
if db_name in cls.registries: with cls.registries_lock:
del cls.registries[db_name] if db_name in cls.registries:
del cls.registries[db_name]
@classmethod @classmethod
@ -177,8 +190,9 @@ class RegistryManager(object):
This method is given to spare you a ``RegistryManager.get(db_name)`` This method is given to spare you a ``RegistryManager.get(db_name)``
that would loads the given database if it was not already loaded. that would loads the given database if it was not already loaded.
""" """
if db_name in cls.registries: with cls.registries_lock:
cls.registries[db_name].clear_caches() if db_name in cls.registries:
cls.registries[db_name].clear_caches()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

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

View File

@ -55,7 +55,7 @@ def _symbol_set(symb):
class _column(object): class _column(object):
""" Base of all fields, a database column """ Base of all fields, a database column
An instance of this object is a *description* of a database column. It will An instance of this object is a *description* of a database column. It will
not hold any data, but only provide the methods to manipulate data of an not hold any data, but only provide the methods to manipulate data of an
ORM record or even prepare/update the database to hold such a field of data. ORM record or even prepare/update the database to hold such a field of data.
@ -675,7 +675,7 @@ class many2many(_column):
if not cr.fetchone(): if not cr.fetchone():
cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, act[1])) cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, act[1]))
elif act[0] == 5: elif act[0] == 5:
cr.execute('update '+self._rel+' set '+self._id2+'=null where '+self._id2+'=%s', (id,)) cr.execute('delete from '+self._rel+' where ' + self._id1 + ' = %s', (id,))
elif act[0] == 6: elif act[0] == 6:
d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context) d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
@ -1295,7 +1295,7 @@ class property(function):
def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None): def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
prop = obj.pool.get('ir.property') prop = obj.pool.get('ir.property')
# get the default values (for res_id = False) for the property fields # get the default values (for res_id = False) for the property fields
default_val = self._get_defaults(obj, cr, uid, prop_names, context) default_val = self._get_defaults(obj, cr, uid, prop_names, context)
# build the dictionary that will be returned # build the dictionary that will be returned
@ -1417,12 +1417,16 @@ class column_info(object):
:attr parent_column: the name of the column containing the m2o :attr parent_column: the name of the column containing the m2o
relationship to the parent model that contains relationship to the parent model that contains
this column, None for local columns. this column, None for local columns.
:attr original_parent: if the column is inherited, name of the original
parent model that contains it i.e in case of multilevel
inheritence, None for local columns.
""" """
def __init__(self, name, column, parent_model=None, parent_column=None): def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
self.name = name self.name = name
self.column = column self.column = column
self.parent_model = parent_model self.parent_model = parent_model
self.parent_column = parent_column self.parent_column = parent_column
self.original_parent = original_parent
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -38,7 +38,7 @@
- classicals (varchar, integer, boolean, ...) - classicals (varchar, integer, boolean, ...)
- relations (one2many, many2one, many2many) - relations (one2many, many2one, many2many)
- functions - functions
""" """
import calendar import calendar
@ -165,7 +165,7 @@ def modifiers_tests():
test_modifiers({}, '{}') test_modifiers({}, '{}')
test_modifiers({"invisible": True}, '{"invisible": true}') test_modifiers({"invisible": True}, '{"invisible": true}')
test_modifiers({"invisible": False}, '{}') test_modifiers({"invisible": False}, '{}')
def check_object_name(name): def check_object_name(name):
""" Check if the given name is a valid openerp object name. """ Check if the given name is a valid openerp object name.
@ -212,6 +212,19 @@ def last_day_of_current_month():
def intersect(la, lb): def intersect(la, lb):
return filter(lambda x: x in lb, la) return filter(lambda x: x in lb, la)
def fix_import_export_id_paths(fieldname):
"""
Fixes the id fields in import and exports, and splits field paths
on '/'.
:param str fieldname: name of the field to import/export
:return: split field name
:rtype: list of str
"""
fixed_db_id = re.sub(r'([^/])\.id', r'\1/.id', fieldname)
fixed_external_id = re.sub(r'([^/]):id', r'\1/id', fixed_db_id)
return fixed_external_id.split('/')
class except_orm(Exception): class except_orm(Exception):
def __init__(self, name, value): def __init__(self, name, value):
self.name = name self.name = name
@ -252,7 +265,7 @@ class browse_null(object):
# #
class browse_record_list(list): class browse_record_list(list):
""" Collection of browse objects """ Collection of browse objects
Such an instance will be returned when doing a ``browse([ids..])`` Such an instance will be returned when doing a ``browse([ids..])``
and will be iterable, yielding browse() objects and will be iterable, yielding browse() objects
""" """
@ -267,9 +280,9 @@ class browse_record_list(list):
class browse_record(object): class browse_record(object):
""" An object that behaves like a row of an object's table. """ An object that behaves like a row of an object's table.
It has attributes after the columns of the corresponding object. It has attributes after the columns of the corresponding object.
Examples:: Examples::
uobj = pool.get('res.users') uobj = pool.get('res.users')
user_rec = uobj.browse(cr, uid, 104) user_rec = uobj.browse(cr, uid, 104)
name = user_rec.name name = user_rec.name
@ -326,9 +339,12 @@ class browse_record(object):
col = self._table._inherit_fields[name][2] col = self._table._inherit_fields[name][2]
elif hasattr(self._table, str(name)): elif hasattr(self._table, str(name)):
attr = getattr(self._table, name) attr = getattr(self._table, name)
if isinstance(attr, (types.MethodType, types.LambdaType, types.FunctionType)): if isinstance(attr, (types.MethodType, types.LambdaType, types.FunctionType)):
return lambda *args, **argv: attr(self._cr, self._uid, [self._id], *args, **argv) def function_proxy(*args, **kwargs):
if 'context' not in kwargs and self._context:
kwargs.update(context=self._context)
return attr(self._cr, self._uid, [self._id], *args, **kwargs)
return function_proxy
else: else:
return attr return attr
else: else:
@ -475,6 +491,16 @@ class browse_record(object):
__repr__ = __str__ __repr__ = __str__
def refresh(self):
"""Force refreshing this browse_record's data and all the data of the
records that belong to the same cache, by emptying the cache completely,
preserving only the record identifiers (for prefetching optimizations).
"""
for model, model_cache in self._cache.iteritems():
# only preserve the ids of the records that were in the cache
cached_ids = dict([(i, {'id': i}) for i in model_cache.keys()])
self._cache[model].clear()
self._cache[model].update(cached_ids)
def get_pg_type(f): def get_pg_type(f):
""" """
@ -587,22 +613,32 @@ class orm_template(object):
_order = 'id' _order = 'id'
_sequence = None _sequence = None
_description = None _description = None
# structure:
# { 'parent_model': 'm2o_field', ... }
_inherits = {} _inherits = {}
# Mapping from inherits'd field name to triple (m, r, f)
# where m is the model from which it is inherits'd, # Mapping from inherits'd field name to triple (m, r, f, n) where m is the
# r is the (local) field towards m, # model from which it is inherits'd, r is the (local) field towards m, f
# and f is the _column object itself. # is the _column object itself, and n is the original (i.e. top-most)
# parent model.
# Example:
# { 'field_name': ('parent_model', 'm2o_field_to_reach_parent',
# field_column_obj, origina_parent_model), ... }
_inherit_fields = {} _inherit_fields = {}
# Mapping field name/column_info object # Mapping field name/column_info object
# This is similar to _inherit_fields but: # This is similar to _inherit_fields but:
# 1. includes self fields, # 1. includes self fields,
# 2. uses column_info instead of a triple. # 2. uses column_info instead of a triple.
_all_columns = {} _all_columns = {}
_table = None _table = None
_invalids = set() _invalids = set()
_log_create = False _log_create = False
CONCURRENCY_CHECK_FIELD = '__last_update' CONCURRENCY_CHECK_FIELD = '__last_update'
def log(self, cr, uid, id, message, secondary=False, context=None): def log(self, cr, uid, id, message, secondary=False, context=None):
if context and context.get('disable_log'): if context and context.get('disable_log'):
return True return True
@ -770,7 +806,7 @@ class orm_template(object):
'You may need to add a dependency on the parent class\' module.' % (name, parent_name)) 'You may need to add a dependency on the parent class\' module.' % (name, parent_name))
nattr = {} nattr = {}
for s in attributes: for s in attributes:
new = copy.copy(getattr(pool.get(parent_name), s)) new = copy.copy(getattr(pool.get(parent_name), s, {}))
if s == '_columns': if s == '_columns':
# Don't _inherit custom fields. # Don't _inherit custom fields.
for c in new.keys(): for c in new.keys():
@ -872,7 +908,7 @@ class orm_template(object):
elif field_type == 'integer': elif field_type == 'integer':
return 0 return 0
elif field_type == 'boolean': elif field_type == 'boolean':
return False return 'False'
return '' return ''
def selection_field(in_field): def selection_field(in_field):
@ -984,11 +1020,7 @@ class orm_template(object):
cols = self._columns.copy() cols = self._columns.copy()
for f in self._inherit_fields: for f in self._inherit_fields:
cols.update({f: self._inherit_fields[f][2]}) cols.update({f: self._inherit_fields[f][2]})
def fsplit(fieldname): fields_to_export = map(fix_import_export_id_paths, fields_to_export)
fixed_db_id = re.sub(r'([^/])\.id', r'\1/.id', fieldname)
fixed_external_id = re.sub(r'([^/]):id', r'\1/id', fixed_db_id)
return fixed_external_id.split('/')
fields_to_export = map(fsplit, fields_to_export)
datas = [] datas = []
for row in self.browse(cr, uid, ids, context): for row in self.browse(cr, uid, ids, context):
datas += self.__export_row(cr, uid, row, fields_to_export, context) datas += self.__export_row(cr, uid, row, fields_to_export, context)
@ -1024,10 +1056,7 @@ class orm_template(object):
""" """
if not context: if not context:
context = {} context = {}
def _replace_field(x): fields = map(fix_import_export_id_paths, fields)
x = re.sub('([a-z0-9A-Z_])\\.id$', '\\1/.id', x)
return x.replace(':id','/id').split('/')
fields = map(_replace_field, fields)
logger = netsvc.Logger() logger = netsvc.Logger()
ir_model_data_obj = self.pool.get('ir.model.data') ir_model_data_obj = self.pool.get('ir.model.data')
@ -1089,7 +1118,7 @@ class orm_template(object):
if line[i] and skip: if line[i] and skip:
return False return False
continue continue
#set the mode for m2o, o2m, m2m : xml_id/id/name #set the mode for m2o, o2m, m2m : xml_id/id/name
if len(field) == len(prefix)+1: if len(field) == len(prefix)+1:
mode = False mode = False
@ -1102,7 +1131,7 @@ class orm_template(object):
for db_id in line.split(config.get('csv_internal_sep')): for db_id in line.split(config.get('csv_internal_sep')):
res.append(_get_id(relation, db_id, current_module, mode)) res.append(_get_id(relation, db_id, current_module, mode))
return [(6,0,res)] return [(6,0,res)]
# ID of the record using a XML ID # ID of the record using a XML ID
if field[len(prefix)]=='id': if field[len(prefix)]=='id':
try: try:
@ -1126,9 +1155,9 @@ class orm_template(object):
relation_obj = self.pool.get(relation) relation_obj = self.pool.get(relation)
newfd = relation_obj.fields_get( cr, uid, context=context ) newfd = relation_obj.fields_get( cr, uid, context=context )
pos = position pos = position
res = many_ids(line[i], relation, current_module, mode) res = many_ids(line[i], relation, current_module, mode)
first = 0 first = 0
while pos < len(datas): while pos < len(datas):
res2 = process_liness(self, datas, prefix + [field[len(prefix)]], current_module, relation_obj._name, newfd, pos, first) res2 = process_liness(self, datas, prefix + [field[len(prefix)]], current_module, relation_obj._name, newfd, pos, first)
@ -1138,15 +1167,15 @@ class orm_template(object):
nbrmax = max(nbrmax, pos) nbrmax = max(nbrmax, pos)
warning += w2 warning += w2
first += 1 first += 1
if data_res_id2: if data_res_id2:
res.append((4, data_res_id2)) res.append((4, data_res_id2))
if (not newrow) or not reduce(lambda x, y: x or y, newrow.values(), 0): if (not newrow) or not reduce(lambda x, y: x or y, newrow.values(), 0):
break break
res.append( (data_res_id2 and 1 or 0, data_res_id2 or 0, newrow) ) res.append( (data_res_id2 and 1 or 0, data_res_id2 or 0, newrow) )
elif fields_def[field[len(prefix)]]['type']=='many2one': elif fields_def[field[len(prefix)]]['type']=='many2one':
relation = fields_def[field[len(prefix)]]['relation'] relation = fields_def[field[len(prefix)]]['relation']
@ -1175,7 +1204,7 @@ class orm_template(object):
else: else:
res = line[i] res = line[i]
row[field[len(prefix)]] = res or False row[field[len(prefix)]] = res or False
result = (row, nbrmax, warning, data_res_id, xml_id) result = (row, nbrmax, warning, data_res_id, xml_id)
@ -1189,7 +1218,7 @@ class orm_template(object):
position = 0 position = 0
while position<len(datas): while position<len(datas):
res = {} res = {}
(res, position, warning, res_id, xml_id) = \ (res, position, warning, res_id, xml_id) = \
process_liness(self, datas, [], current_module, self._name, fields_def, position=position) process_liness(self, datas, [], current_module, self._name, fields_def, position=position)
if len(warning): if len(warning):
@ -1201,7 +1230,7 @@ class orm_template(object):
current_module, res, mode=mode, xml_id=xml_id, current_module, res, mode=mode, xml_id=xml_id,
noupdate=noupdate, res_id=res_id, context=context) noupdate=noupdate, res_id=res_id, context=context)
except Exception, e: except Exception, e:
return (-1, res, 'Line ' + str(position) +' : ' + str(e), '') return (-1, res, 'Line ' + str(position) + ' : ' + tools.ustr(e), '')
if config.get('import_partial', False) and filename and (not (position%100)): if config.get('import_partial', False) and filename and (not (position%100)):
data = pickle.load(file(config.get('import_partial'))) data = pickle.load(file(config.get('import_partial')))
@ -1556,7 +1585,7 @@ class orm_template(object):
field = model_fields.get(node.get('name')) field = model_fields.get(node.get('name'))
if field: if field:
transfer_field_to_modifiers(field, modifiers) transfer_field_to_modifiers(field, modifiers)
elif node.tag in ('form', 'tree'): elif node.tag in ('form', 'tree'):
result = self.view_header_get(cr, user, False, node.tag, context) result = self.view_header_get(cr, user, False, node.tag, context)
@ -1887,22 +1916,20 @@ class orm_template(object):
raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, inherit_id) raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, inherit_id)
return source return source
def apply_view_inheritance(source, inherit_id): def apply_view_inheritance(cr, user, source, inherit_id):
""" Apply all the (directly and indirectly) inheriting views. """ Apply all the (directly and indirectly) inheriting views.
:param source: a parent architecture to modify (with parent :param source: a parent architecture to modify (with parent
modifications already applied) modifications already applied)
:param inherit_id: the database id of the parent view :param inherit_id: the database view_id of the parent view
:return: a modified source where all the modifying architecture :return: a modified source where all the modifying architecture
are applied are applied
""" """
# get all views which inherit from (ie modify) this view sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, user, inherit_id, self._name)
cr.execute('select arch,id from ir_ui_view where inherit_id=%s and model=%s order by priority', (inherit_id, self._name)) for (view_arch, view_id) in sql_inherit:
sql_inherit = cr.fetchall() source = apply_inheritance_specs(source, view_arch, view_id)
for (inherit, id) in sql_inherit: source = apply_view_inheritance(cr, user, source, view_id)
source = apply_inheritance_specs(source, inherit, id)
source = apply_view_inheritance(source, id)
return source return source
result = {'type': view_type, 'model': self._name} result = {'type': view_type, 'model': self._name}
@ -1945,7 +1972,7 @@ class orm_template(object):
result['view_id'] = sql_res['id'] result['view_id'] = sql_res['id']
source = etree.fromstring(encode(sql_res['arch'])) source = etree.fromstring(encode(sql_res['arch']))
result['arch'] = apply_view_inheritance(source, result['view_id']) result['arch'] = apply_view_inheritance(cr, user, source, result['view_id'])
result['name'] = sql_res['name'] result['name'] = sql_res['name']
result['field_parent'] = sql_res['field_parent'] or False result['field_parent'] = sql_res['field_parent'] or False
@ -2097,19 +2124,15 @@ class orm_template(object):
raise NotImplementedError(_('The search method is not implemented on this object !')) raise NotImplementedError(_('The search method is not implemented on this object !'))
def name_get(self, cr, user, ids, context=None): def name_get(self, cr, user, ids, context=None):
""" """Returns the preferred display value (text representation) for the records with the
given ``ids``. By default this will be the value of the ``name`` column, unless
:param cr: database cursor the model implements a custom behavior.
:param user: current user id Can sometimes be seen as the inverse function of :meth:`~.name_search`, but it is not
:type user: integer guaranteed to be.
:param ids: list of ids
:param context: context arguments, like lang, time zone
:type context: dictionary
:return: tuples with the text representation of requested objects for to-many relationships
:rtype: list(tuple)
:return: list of pairs ``(id,text_repr)`` for all records with the given ``ids``.
""" """
if not context:
context = {}
if not ids: if not ids:
return [] return []
if isinstance(ids, (int, long)): if isinstance(ids, (int, long)):
@ -2118,38 +2141,39 @@ class orm_template(object):
[self._rec_name], context, load='_classic_write')] [self._rec_name], context, load='_classic_write')]
def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
""" """Search for records that have a display name matching the given ``name`` pattern if compared
Search for records and their display names according to a search domain. with the given ``operator``, while also matching the optional search domain (``args``).
This is used for example to provide suggestions based on a partial value for a relational
field.
Sometimes be seen as the inverse function of :meth:`~.name_get`, but it is not
guaranteed to be.
:param cr: database cursor This method is equivalent to calling :meth:`~.search` with a search domain based on ``name``
:param user: current user id and then :meth:`~.name_get` on the result of the search.
:param name: object name to search
:param args: list of tuples specifying search criteria [('field_name', 'operator', 'value'), ...]
:param operator: operator for search criterion
:param context: context arguments, like lang, time zone
:type context: dictionary
:param limit: optional max number of records to return
:return: list of object names matching the search criteria, used to provide completion for to-many relationships
This method is equivalent of :py:meth:`~osv.osv.osv.search` on **name** + :py:meth:`~osv.osv.osv.name_get` on the result.
See :py:meth:`~osv.osv.osv.search` for an explanation of the possible values for the search domain specified in **args**.
:param list args: optional search domain (see :meth:`~.search` for syntax),
specifying further restrictions
:param str operator: domain operator for matching the ``name`` pattern, such as ``'like'``
or ``'='``.
:param int limit: optional max number of records to return
:rtype: list
:return: list of pairs ``(id,text_repr)`` for all matching records.
""" """
return self._name_search(cr, user, name, args, operator, context, limit) return self._name_search(cr, user, name, args, operator, context, limit)
def name_create(self, cr, uid, name, context=None): def name_create(self, cr, uid, name, context=None):
""" """Creates a new record by calling :meth:`~.create` with only one
Creates a new record by calling :py:meth:`~osv.osv.osv.create` with only one value provided: the name of the new record (``_rec_name`` field).
value provided: the name of the new record (``_rec_name`` field). The new record will also be initialized with any default values applicable
The new record will also be initialized with any default values applicable to this model, or provided through the context. The usual behavior of
to this model, or provided through the context. The usual behavior of :meth:`~.create` applies.
:py:meth:`~osv.osv.osv.create` applies. Similarly, this method may raise an exception if the model has multiple
Similarly, this method may raise an exception if the model has multiple required fields and some do not have default values.
required fields and some do not have default values.
:param name: name of the record to create :param name: name of the record to create
:return: the :py:meth:`~osv.osv.osv.name_get` value for the newly-created record. :rtype: tuple
:return: the :meth:`~.name_get` pair value for the newly-created record.
""" """
rec_id = self.create(cr, uid, {self._rec_name: name}, context); rec_id = self.create(cr, uid, {self._rec_name: name}, context);
return self.name_get(cr, uid, [rec_id], context)[0] return self.name_get(cr, uid, [rec_id], context)[0]
@ -2172,7 +2196,19 @@ class orm_template(object):
def copy(self, cr, uid, id, default=None, context=None): def copy(self, cr, uid, id, default=None, context=None):
raise NotImplementedError(_('The copy method is not implemented on this object !')) raise NotImplementedError(_('The copy method is not implemented on this object !'))
def exists(self, cr, uid, id, context=None): def exists(self, cr, uid, ids, context=None):
"""Checks whether the given id or ids exist in this model,
and return the list of ids that do. This is simple to use for
a truth test on a browse_record::
if record.exists():
pass
:param ids: id or list of ids to check for existence
:type ids: int or [int]
:return: the list of ids that currently exist, out of
the given `ids`
"""
raise NotImplementedError(_('The exists method is not implemented on this object !')) raise NotImplementedError(_('The exists method is not implemented on this object !'))
def read_string(self, cr, uid, id, langs, fields=None, context=None): def read_string(self, cr, uid, id, langs, fields=None, context=None):
@ -2259,6 +2295,16 @@ class orm_template(object):
except AttributeError: except AttributeError:
pass pass
def check_access_rule(self, cr, uid, ids, operation, context=None):
"""Verifies that the operation given by ``operation`` is allowed for the user
according to ir.rules.
:param operation: one of ``write``, ``unlink``
:raise except_orm: * if current ir.rules do not permit this operation.
:return: None if the operation is allowed
"""
raise NotImplementedError(_('The check_access_rule method is not implemented on this object !'))
class orm_memory(orm_template): class orm_memory(orm_template):
_protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists'] _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
@ -2414,8 +2460,7 @@ class orm_memory(orm_template):
args = [('active', '=', 1)] args = [('active', '=', 1)]
if args: if args:
import expression import expression
e = expression.expression(args) e = expression.expression(cr, user, args, self, context)
e.parse(cr, user, self, context)
res = e.exp res = e.exp
return res or [] return res or []
@ -2445,6 +2490,9 @@ class orm_memory(orm_template):
break break
f = True f = True
for arg in result: for arg in result:
if len(arg) != 3:
# Amazing hack: orm_memory handles only simple domains.
continue
if arg[1] == '=': if arg[1] == '=':
val = eval('data[arg[0]]'+'==' +' arg[2]', locals()) val = eval('data[arg[0]]'+'==' +' arg[2]', locals())
elif arg[1] in ['<', '>', 'in', 'not in', '<=', '>=', '<>']: elif arg[1] in ['<', '>', 'in', 'not in', '<=', '>=', '<>']:
@ -2488,8 +2536,28 @@ class orm_memory(orm_template):
# nothing to check in memory... # nothing to check in memory...
pass pass
def exists(self, cr, uid, id, context=None): def exists(self, cr, uid, ids, context=None):
return id in self.datas if isinstance(ids, (long,int)):
ids = [ids]
return [id for id in ids if id in self.datas]
def check_access_rule(self, cr, uid, ids, operation, context=None):
# ir.rules do not currently apply for orm.memory instances,
# only the implicit visibility=owner one.
for id in ids:
self._check_access(uid, id, operation)
# Definition of log access columns, automatically added to models if
# self._log_access is True
LOG_ACCESS_COLUMNS = {
'create_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL',
'create_date': 'TIMESTAMP',
'write_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL',
'write_date': 'TIMESTAMP'
}
# special columns automatically created by the ORM
MAGIC_COLUMNS = ['id'] + LOG_ACCESS_COLUMNS.keys() + \
['internal.create_uid', 'internal.date_access'] # for osv_memory only
class orm(orm_template): class orm(orm_template):
_sql_constraints = [] _sql_constraints = []
@ -2497,6 +2565,7 @@ class orm(orm_template):
_protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists'] _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
__logger = logging.getLogger('orm') __logger = logging.getLogger('orm')
__schema = logging.getLogger('orm.schema') __schema = logging.getLogger('orm.schema')
def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
""" """
Get the list of records in list view grouped by the given ``groupby`` fields Get the list of records in list view grouped by the given ``groupby`` fields
@ -2617,20 +2686,22 @@ class orm(orm_template):
del d['id'] del d['id']
return data return data
def _inherits_join_add(self, parent_model_name, query): def _inherits_join_add(self, current_table, parent_model_name, query):
""" """
Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates) Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates)
:param current_table: current model object
:param parent_model_name: name of the parent model for which the clauses should be added :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 :param query: query object on which the JOIN should be added
""" """
inherits_field = self._inherits[parent_model_name] inherits_field = current_table._inherits[parent_model_name]
parent_model = self.pool.get(parent_model_name) parent_model = self.pool.get(parent_model_name)
parent_table_name = parent_model._table parent_table_name = parent_model._table
quoted_parent_table_name = '"%s"' % parent_table_name quoted_parent_table_name = '"%s"' % parent_table_name
if quoted_parent_table_name not in query.tables: if quoted_parent_table_name not in query.tables:
query.tables.append(quoted_parent_table_name) query.tables.append(quoted_parent_table_name)
query.where_clause.append('("%s".%s = %s.id)' % (self._table, inherits_field, parent_table_name)) query.where_clause.append('(%s.%s = %s.id)' % (current_table._table, inherits_field, parent_table_name))
def _inherits_join_calc(self, field, query): def _inherits_join_calc(self, field, query):
""" """
@ -2645,7 +2716,7 @@ class orm(orm_template):
while field in current_table._inherit_fields and not field in current_table._columns: while field in current_table._inherit_fields and not field in current_table._columns:
parent_model_name = current_table._inherit_fields[field][0] parent_model_name = current_table._inherit_fields[field][0]
parent_table = self.pool.get(parent_model_name) parent_table = self.pool.get(parent_model_name)
self._inherits_join_add(parent_model_name, query) self._inherits_join_add(current_table, parent_model_name, query)
current_table = parent_table current_table = parent_table
return '"%s".%s' % (current_table._table, field) return '"%s".%s' % (current_table._table, field)
@ -2707,7 +2778,7 @@ class orm(orm_template):
pass pass
if not val_id: if not val_id:
raise except_orm(_('ValidateError'), raise except_orm(_('ValidateError'),
_('Invalid value for reference field "%s" (last part must be a non-zero integer): "%s"') % (field, value)) _('Invalid value for reference field "%s.%s" (last part must be a non-zero integer): "%s"') % (self._table, field, value))
val = val_model val = val_model
else: else:
val = value val = value
@ -2717,13 +2788,13 @@ class orm(orm_template):
elif val in dict(self._columns[field].selection(self, cr, uid, context=context)): elif val in dict(self._columns[field].selection(self, cr, uid, context=context)):
return return
raise except_orm(_('ValidateError'), raise except_orm(_('ValidateError'),
_('The value "%s" for the field "%s" is not in the selection') % (value, field)) _('The value "%s" for the field "%s.%s" is not in the selection') % (value, self._table, field))
def _check_removed_columns(self, cr, log=False): def _check_removed_columns(self, cr, log=False):
# iterate on the database columns to drop the NOT NULL constraints # iterate on the database columns to drop the NOT NULL constraints
# of fields which were required but have been removed (or will be added by another module) # of fields which were required but have been removed (or will be added by another module)
columns = [c for c in self._columns if not (isinstance(self._columns[c], fields.function) and not self._columns[c].store)] columns = [c for c in self._columns if not (isinstance(self._columns[c], fields.function) and not self._columns[c].store)]
columns += ('id', 'write_uid', 'write_date', 'create_uid', 'create_date') # openerp access columns columns += MAGIC_COLUMNS
cr.execute("SELECT a.attname, a.attnotnull" cr.execute("SELECT a.attname, a.attnotnull"
" FROM pg_class c, pg_attribute a" " FROM pg_class c, pg_attribute a"
" WHERE c.relname=%s" " WHERE c.relname=%s"
@ -2790,7 +2861,7 @@ class orm(orm_template):
column_data = self._select_column_data(cr) column_data = self._select_column_data(cr)
for k, f in self._columns.iteritems(): for k, f in self._columns.iteritems():
if k in ('id', 'write_uid', 'write_date', 'create_uid', 'create_date'): if k in MAGIC_COLUMNS:
continue continue
# Don't update custom (also called manual) fields # Don't update custom (also called manual) fields
if f.manual and not update_custom_fields: if f.manual and not update_custom_fields:
@ -2883,7 +2954,7 @@ class orm(orm_template):
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, k)) cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, k))
cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (self._table, k, newname)) cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (self._table, k, newname))
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, get_pg_type(f)[1])) cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, get_pg_type(f)[1]))
cr.execute("COMMENT ON COLUMN %s.%s IS '%s'" % (self._table, k, f.string.replace("'", "''"))) cr.execute("COMMENT ON COLUMN %s.\"%s\" IS %%s" % (self._table, k), (f.string,))
self.__schema.debug("Table '%s': column '%s' has changed type (DB=%s, def=%s), data moved to column %s !", self.__schema.debug("Table '%s': column '%s' has changed type (DB=%s, def=%s), data moved to column %s !",
self._table, k, f_pg_type, f._type, newname) self._table, k, f_pg_type, f._type, newname)
@ -2970,7 +3041,7 @@ class orm(orm_template):
if not isinstance(f, fields.function) or f.store: if not isinstance(f, fields.function) or f.store:
# add the missing field # add the missing field
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, get_pg_type(f)[1])) cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, get_pg_type(f)[1]))
cr.execute("COMMENT ON COLUMN %s.%s IS '%s'" % (self._table, k, f.string.replace("'", "''"))) cr.execute("COMMENT ON COLUMN %s.\"%s\" IS %%s" % (self._table, k), (f.string,))
self.__schema.debug("Table '%s': added column '%s' with definition=%s", self.__schema.debug("Table '%s': added column '%s' with definition=%s",
self._table, k, get_pg_type(f)[1]) self._table, k, get_pg_type(f)[1])
@ -3053,7 +3124,7 @@ class orm(orm_template):
def _create_table(self, cr): 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)) WITHOUT OIDS' % (self._table,))
cr.execute("COMMENT ON TABLE \"%s\" IS '%s'" % (self._table, self._description.replace("'", "''"))) cr.execute(("COMMENT ON TABLE \"%s\" IS %%s" % self._table), (self._description,))
self.__schema.debug("Table '%s': created", self._table) self.__schema.debug("Table '%s': created", self._table)
@ -3092,23 +3163,17 @@ class orm(orm_template):
def _add_log_columns(self, cr): def _add_log_columns(self, cr):
logs = { for field, field_def in LOG_ACCESS_COLUMNS.iteritems():
'create_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL',
'create_date': 'TIMESTAMP',
'write_uid': 'INTEGER REFERENCES res_users ON DELETE SET NULL',
'write_date': 'TIMESTAMP'
}
for k in logs:
cr.execute(""" cr.execute("""
SELECT c.relname SELECT c.relname
FROM pg_class c, pg_attribute a FROM pg_class c, pg_attribute a
WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid
""", (self._table, k)) """, (self._table, field))
if not cr.rowcount: if not cr.rowcount:
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, logs[k])) cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, field, field_def))
cr.commit() cr.commit()
self.__schema.debug("Table '%s': added column '%s' with definition=%s", self.__schema.debug("Table '%s': added column '%s' with definition=%s",
self._table, k, logs[k]) self._table, field, field_def)
def _select_column_data(self, cr): def _select_column_data(self, cr):
@ -3263,7 +3328,7 @@ class orm(orm_template):
if f == order: if f == order:
ok = False ok = False
if ok: if ok:
self.pool._store_function[object].append( (self._name, store_field, fnct, fields2, order, length)) self.pool._store_function[object].append((self._name, store_field, fnct, tuple(fields2) if fields2 else None, order, length))
self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4])) self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4]))
for (key, _, msg) in self._sql_constraints: for (key, _, msg) in self._sql_constraints:
@ -3336,9 +3401,9 @@ class orm(orm_template):
for table in self._inherits: for table in self._inherits:
other = self.pool.get(table) other = self.pool.get(table)
for col in other._columns.keys(): for col in other._columns.keys():
res[col] = (table, self._inherits[table], other._columns[col]) res[col] = (table, self._inherits[table], other._columns[col], table)
for col in other._inherit_fields.keys(): for col in other._inherit_fields.keys():
res[col] = (table, self._inherits[table], other._inherit_fields[col][2]) res[col] = (table, self._inherits[table], other._inherit_fields[col][2], other._inherit_fields[col][3])
self._inherit_fields = res self._inherit_fields = res
self._all_columns = self._get_column_infos() self._all_columns = self._get_column_infos()
self._inherits_reload_src() self._inherits_reload_src()
@ -3349,8 +3414,8 @@ class orm(orm_template):
inherited field via _inherits) to a ``column_info`` struct inherited field via _inherits) to a ``column_info`` struct
giving detailed columns """ giving detailed columns """
result = {} result = {}
for k, (parent, m2o, col) in self._inherit_fields.iteritems(): for k, (parent, m2o, col, original_parent) in self._inherit_fields.iteritems():
result[k] = fields.column_info(k, col, parent, m2o) result[k] = fields.column_info(k, col, parent, m2o, original_parent)
for k, col in self._columns.iteritems(): for k, col in self._columns.iteritems():
result[k] = fields.column_info(k, col) result[k] = fields.column_info(k, col)
return result return result
@ -3454,7 +3519,7 @@ class orm(orm_template):
res = [] res = []
if len(fields_pre): if len(fields_pre):
def convert_field(f): def convert_field(f):
f_qual = "%s.%s" % (self._table, f) # need fully-qualified references in case len(tables) > 1 f_qual = '%s."%s"' % (self._table, f) # need fully-qualified references in case len(tables) > 1
if f in ('create_date', 'write_date'): if f in ('create_date', 'write_date'):
return "date_trunc('second', %s) as %s" % (f_qual, f) return "date_trunc('second', %s) as %s" % (f_qual, f)
if f == self.CONCURRENCY_CHECK_FIELD: if f == self.CONCURRENCY_CHECK_FIELD:
@ -4059,7 +4124,7 @@ class orm(orm_template):
upd_todo = [] upd_todo = []
for v in vals.keys(): for v in vals.keys():
if v in self._inherit_fields: if v in self._inherit_fields:
(table, col, col_detail) = self._inherit_fields[v] (table, col, col_detail, original_parent) = self._inherit_fields[v]
tocreate[table][v] = vals[v] tocreate[table][v] = vals[v]
del vals[v] del vals[v]
else: else:
@ -4206,44 +4271,52 @@ class orm(orm_template):
:return: [(priority, model_name, [record_ids,], [function_fields,])] :return: [(priority, model_name, [record_ids,], [function_fields,])]
""" """
# FIXME: rewrite, cleanup, use real variable names if fields is None: fields = []
# e.g.: http://pastie.org/1222060 stored_functions = self.pool._store_function.get(self._name, [])
result = {}
fncts = self.pool._store_function.get(self._name, [])
for fnct in range(len(fncts)):
if fncts[fnct][3]:
ok = False
if not fields:
ok = True
for f in (fields or []):
if f in fncts[fnct][3]:
ok = True
break
if not ok:
continue
result.setdefault(fncts[fnct][0], {}) # use indexed names for the details of the stored_functions:
model_name_, func_field_to_compute_, id_mapping_fnct_, trigger_fields_, priority_ = range(5)
# only keep functions that should be triggered for the ``fields``
# being written to.
to_compute = [f for f in stored_functions \
if ((not f[trigger_fields_]) or set(fields).intersection(f[trigger_fields_]))]
mapping = {}
for function in to_compute:
# use admin user for accessing objects having rules defined on store fields # use admin user for accessing objects having rules defined on store fields
ids2 = fncts[fnct][2](self, cr, ROOT_USER_ID, ids, context) target_ids = [id for id in function[id_mapping_fnct_](self, cr, ROOT_USER_ID, ids, context) if id]
for id in filter(None, ids2):
result[fncts[fnct][0]].setdefault(id, []) # the compound key must consider the priority and model name
result[fncts[fnct][0]][id].append(fnct) key = (function[priority_], function[model_name_])
dict = {} for target_id in target_ids:
for object in result: mapping.setdefault(key, {}).setdefault(target_id,set()).add(tuple(function))
k2 = {}
for id, fnct in result[object].items(): # Here mapping looks like:
k2.setdefault(tuple(fnct), []) # { (10, 'model_a') : { target_id1: [ (function_1_tuple, function_2_tuple) ], ... }
k2[tuple(fnct)].append(id) # (20, 'model_a') : { target_id2: [ (function_3_tuple, function_4_tuple) ], ... }
for fnct, id in k2.items(): # (99, 'model_a') : { target_id1: [ (function_5_tuple, function_6_tuple) ], ... }
dict.setdefault(fncts[fnct[0]][4], []) # }
dict[fncts[fnct[0]][4]].append((fncts[fnct[0]][4], object, id, map(lambda x: fncts[x][1], fnct)))
result2 = [] # Now we need to generate the batch function calls list
tmp = dict.keys() # call_map =
tmp.sort() # { (10, 'model_a') : [(10, 'model_a', [record_ids,], [function_fields,])] }
for k in tmp: call_map = {}
result2 += dict[k] for ((priority,model), id_map) in mapping.iteritems():
return result2 functions_ids_maps = {}
# function_ids_maps =
# { (function_1_tuple, function_2_tuple) : [target_id1, target_id2, ..] }
for id, functions in id_map.iteritems():
functions_ids_maps.setdefault(tuple(functions), []).append(id)
for functions, ids in functions_ids_maps.iteritems():
call_map.setdefault((priority,model),[]).append((priority, model, ids,
[f[func_field_to_compute_] for f in functions]))
ordered_keys = call_map.keys()
ordered_keys.sort()
result = []
if ordered_keys:
result = reduce(operator.add, (call_map[k] for k in ordered_keys))
return result
def _store_set_values(self, cr, uid, ids, fields, context): def _store_set_values(self, cr, uid, ids, fields, context):
"""Calls the fields.function's "implementation function" for all ``fields``, on records with ``ids`` (taking care of """Calls the fields.function's "implementation function" for all ``fields``, on records with ``ids`` (taking care of
@ -4355,8 +4428,7 @@ class orm(orm_template):
if domain: if domain:
import expression import expression
e = expression.expression(domain) e = expression.expression(cr, user, domain, self, context)
e.parse(cr, user, self, context)
tables = e.get_tables() tables = e.get_tables()
where_clause, where_params = e.to_sql() where_clause, where_params = e.to_sql()
where_clause = where_clause and [where_clause] or [] where_clause = where_clause and [where_clause] or []
@ -4381,7 +4453,7 @@ class orm(orm_template):
if parent_model and child_object: if parent_model and child_object:
# as inherited rules are being applied, we need to add the missing JOIN # 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) # to reach the parent table (if it was not JOINed yet in the query)
child_object._inherits_join_add(parent_model, query) child_object._inherits_join_add(child_object, parent_model, query)
query.where_clause += added_clause query.where_clause += added_clause
query.where_clause_params += added_params query.where_clause_params += added_params
for table in added_tables: for table in added_tables:
@ -4470,7 +4542,7 @@ class orm(orm_template):
else: else:
continue # ignore non-readable or "non-joinable" fields continue # ignore non-readable or "non-joinable" fields
elif order_field in self._inherit_fields: elif order_field in self._inherit_fields:
parent_obj = self.pool.get(self._inherit_fields[order_field][0]) parent_obj = self.pool.get(self._inherit_fields[order_field][3])
order_column = parent_obj._columns[order_field] order_column = parent_obj._columns[order_field]
if order_column._classic_read: if order_column._classic_read:
inner_clause = self._inherits_join_calc(order_field, query) inner_clause = self._inherits_join_calc(order_field, query)
@ -4577,7 +4649,7 @@ class orm(orm_template):
for f in fields: for f in fields:
ftype = fields[f]['type'] ftype = fields[f]['type']
if self._log_access and f in ('create_date', 'create_uid', 'write_date', 'write_uid'): if self._log_access and f in LOG_ACCESS_COLUMNS:
del data[f] del data[f]
if f in default: if f in default:
@ -4614,9 +4686,13 @@ class orm(orm_template):
# force a clean recompute! # force a clean recompute!
for parent_column in ['parent_left', 'parent_right']: for parent_column in ['parent_left', 'parent_right']:
data.pop(parent_column, None) data.pop(parent_column, None)
# Remove _inherits field's from data recursively, missing parents will
for v in self._inherits: # be created by create() (so that copy() copy everything).
del data[self._inherits[v]] def remove_ids(inherits_dict):
for parent_table in inherits_dict:
del data[inherits_dict[parent_table]]
remove_ids(self.pool.get(parent_table)._inherits)
remove_ids(self._inherits)
return data return data
def copy_translations(self, cr, uid, old_id, new_id, context=None): def copy_translations(self, cr, uid, old_id, new_id, context=None):
@ -4690,9 +4766,9 @@ class orm(orm_template):
def exists(self, cr, uid, ids, context=None): def exists(self, cr, uid, ids, context=None):
if type(ids) in (int, long): if type(ids) in (int, long):
ids = [ids] ids = [ids]
query = 'SELECT count(1) FROM "%s"' % (self._table) query = 'SELECT id FROM "%s"' % (self._table)
cr.execute(query + "WHERE ID IN %s", (tuple(ids),)) cr.execute(query + "WHERE ID IN %s", (tuple(ids),))
return cr.fetchone()[0] == len(ids) return [x[0] for x in cr.fetchall()]
def check_recursion(self, cr, uid, ids, context=None, parent=None): def check_recursion(self, cr, uid, ids, context=None, parent=None):
warnings.warn("You are using deprecated %s.check_recursion(). Please use the '_check_recursion()' instead!" % \ warnings.warn("You are using deprecated %s.check_recursion(). Please use the '_check_recursion()' instead!" % \

View File

@ -231,6 +231,7 @@ class report_rml(report_int):
def _get_path(self): def _get_path(self):
ret = [] ret = []
ret.append(self.tmpl.replace(os.path.sep, '/').rsplit('/',1)[0]) # Same dir as the report rml ret.append(self.tmpl.replace(os.path.sep, '/').rsplit('/',1)[0]) # Same dir as the report rml
ret.append('addons')
ret.append(tools.config['root_path']) ret.append(tools.config['root_path'])
return ret return ret

View File

@ -65,7 +65,10 @@ class report(object):
if type =='html2html': if type =='html2html':
match = html_parents match = html_parents
if txt.group(3): if txt.group(3):
match = [txt.group(3)] group_3 = txt.group(3)
if group_3.startswith("'") or group_3.startswith('"'):
group_3 = group_3[1:-1]
match = [group_3]
n = node n = node
while n.tag not in match: while n.tag not in match:
n = n.getparent() n = n.getparent()

View File

@ -447,14 +447,14 @@ class _rml_canvas(object):
self._logger.debug("Image %s used", node.get('name')) self._logger.debug("Image %s used", node.get('name'))
s = StringIO(image_data) s = StringIO(image_data)
else: else:
newtext = node.text
if self.localcontext: if self.localcontext:
res = utils._regex.findall(node.text) res = utils._regex.findall(newtext)
for key in res: for key in res:
newtext = eval(key, {}, self.localcontext) newtext = eval(key, {}, self.localcontext) or ''
node.text = newtext or ''
image_data = None image_data = None
if node.text: if newtext:
image_data = base64.decodestring(node.text) image_data = base64.decodestring(newtext)
if image_data: if image_data:
s = StringIO(image_data) s = StringIO(image_data)
else: else:

View File

@ -68,6 +68,9 @@ rml2sxw = {
'para': 'p', 'para': 'p',
} }
def get_date_length(date_format=DT_FORMAT):
return len((datetime.now()).strftime(date_format))
class _format(object): class _format(object):
def set_value(self, cr, uid, name, object, field, lang_obj): def set_value(self, cr, uid, name, object, field, lang_obj):
self.object = object self.object = object
@ -78,7 +81,7 @@ class _format(object):
class _float_format(float, _format): class _float_format(float, _format):
def __init__(self,value): def __init__(self,value):
super(_float_format, self).__init__() super(_float_format, self).__init__()
self.val = value self.val = value or 0.0
def __str__(self): def __str__(self):
digits = 2 digits = 2
@ -86,17 +89,17 @@ class _float_format(float, _format):
digits = self._field.digits[1] digits = self._field.digits[1]
if hasattr(self, 'lang_obj'): if hasattr(self, 'lang_obj'):
return self.lang_obj.format('%.' + str(digits) + 'f', self.name, True) return self.lang_obj.format('%.' + str(digits) + 'f', self.name, True)
return self.val return str(self.val)
class _int_format(int, _format): class _int_format(int, _format):
def __init__(self,value): def __init__(self,value):
super(_int_format, self).__init__() super(_int_format, self).__init__()
self.val = value and str(value) or str(0) self.val = value or 0
def __str__(self): def __str__(self):
if hasattr(self,'lang_obj'): if hasattr(self,'lang_obj'):
return self.lang_obj.format('%.d', self.name, True) return self.lang_obj.format('%.d', self.name, True)
return self.val return str(self.val)
class _date_format(str, _format): class _date_format(str, _format):
def __init__(self,value): def __init__(self,value):
@ -106,7 +109,7 @@ class _date_format(str, _format):
def __str__(self): def __str__(self):
if self.val: if self.val:
if getattr(self,'name', None): if getattr(self,'name', None):
date = datetime.strptime(self.name, DT_FORMAT) date = datetime.strptime(self.name[:get_date_length()], DT_FORMAT)
return date.strftime(str(self.lang_obj.date_format)) return date.strftime(str(self.lang_obj.date_format))
return self.val return self.val
@ -264,7 +267,7 @@ class rml_parse(object):
d = obj._field.digits[1] or DEFAULT_DIGITS d = obj._field.digits[1] or DEFAULT_DIGITS
return d return d
def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False): def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False):
""" """
Assuming 'Account' decimal.precision=3: Assuming 'Account' decimal.precision=3:
formatLang(value) -> digits=2 (default) formatLang(value) -> digits=2 (default)
@ -296,13 +299,19 @@ class rml_parse(object):
date_format = date_format + " " + self.lang_dict['time_format'] date_format = date_format + " " + self.lang_dict['time_format']
parse_format = DHM_FORMAT parse_format = DHM_FORMAT
if not isinstance(value, time.struct_time): if not isinstance(value, time.struct_time):
return time.strftime(date_format, time.strptime(value, parse_format)) return time.strftime(date_format, time.strptime(value[:get_date_length(parse_format)], parse_format))
else: else:
date = datetime(*value.timetuple()[:6]) date = datetime(*value.timetuple()[:6])
return date.strftime(date_format) return date.strftime(date_format)
return self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
if currency_obj:
if currency_obj.position == 'after':
res='%s %s'%(res,currency_obj.symbol)
elif currency_obj and currency_obj.position == 'before':
res='%s %s'%(currency_obj.symbol, res)
return res
def repeatIn(self, lst, name,nodes_parent=False): def repeatIn(self, lst, name,nodes_parent=False):
ret_lst = [] ret_lst = []

View File

@ -183,6 +183,8 @@ class Cursor(object):
self.__caller = False self.__caller = False
self.__closer = False self.__closer = False
self._default_log_exceptions = True
def __del__(self): def __del__(self):
if not self.__closed: if not self.__closed:
# Oops. 'self' has not been closed explicitly. # Oops. 'self' has not been closed explicitly.
@ -199,7 +201,7 @@ class Cursor(object):
self._close(True) self._close(True)
@check @check
def execute(self, query, params=None, log_exceptions=True): def execute(self, query, params=None, log_exceptions=None):
if '%d' in query or '%f' in query: if '%d' in query or '%f' in query:
self.__logger.warn(query) self.__logger.warn(query)
self.__logger.warn("SQL queries cannot contain %d or %f anymore. " self.__logger.warn("SQL queries cannot contain %d or %f anymore. "
@ -212,11 +214,11 @@ class Cursor(object):
params = params or None params = params or None
res = self._obj.execute(query, params) res = self._obj.execute(query, params)
except psycopg2.ProgrammingError, pe: except psycopg2.ProgrammingError, pe:
if log_exceptions: if self._default_log_exceptions or log_exceptions:
self.__logger.error("Programming error: %s, in query %s", pe, query) self.__logger.error("Programming error: %s, in query %s", pe, query)
raise raise
except Exception: except Exception:
if log_exceptions: if self._default_log_exceptions or log_exceptions:
self.__logger.exception("bad query: %s", self._obj.query or query) self.__logger.exception("bad query: %s", self._obj.query or query)
raise raise

View File

@ -254,6 +254,8 @@ class configmanager(object):
"osv_memory tables. This is a decimal value expressed in hours, " "osv_memory tables. This is a decimal value expressed in hours, "
"and the default is 1 hour.", "and the default is 1 hour.",
type="float") type="float")
group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true",
help="Use the unaccent function provided by the database when available.")
parser.add_option_group(group) parser.add_option_group(group)
# Copy all optparse options (i.e. MyOption) into self.options. # Copy all optparse options (i.e. MyOption) into self.options.
@ -356,7 +358,7 @@ class configmanager(object):
'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog', 'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog',
'list_db', 'xmlrpcs', 'list_db', 'xmlrpcs',
'test_file', 'test_disable', 'test_commit', 'test_report_directory', 'test_file', 'test_disable', 'test_commit', 'test_report_directory',
'osv_memory_count_limit', 'osv_memory_age_limit', 'osv_memory_count_limit', 'osv_memory_age_limit', 'unaccent',
] ]
for arg in keys: for arg in keys:

View File

@ -45,6 +45,7 @@ from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart from email.MIMEMultipart import MIMEMultipart
from email.Header import Header from email.Header import Header
from email.Utils import formatdate, COMMASPACE from email.Utils import formatdate, COMMASPACE
from email import Utils
from email import Encoders from email import Encoders
from itertools import islice, izip from itertools import islice, izip
from lxml import etree from lxml import etree
@ -59,6 +60,7 @@ except ImportError:
html2text = None html2text = None
import openerp.loglevels as loglevels import openerp.loglevels as loglevels
import openerp.pooler as pooler
from config import config from config import config
from cache import * from cache import *
@ -280,15 +282,7 @@ email_re = re.compile(r"""
""", re.VERBOSE) """, re.VERBOSE)
res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE) res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE) command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE) reference_re = re.compile("<.*-open(?:object|erp)-(\\d+).*@(.*)>", re.UNICODE)
priorities = {
'1': '1 (Highest)',
'2': '2 (High)',
'3': '3 (Normal)',
'4': '4 (Low)',
'5': '5 (Lowest)',
}
def html2plaintext(html, body_id=None, encoding='utf-8'): def html2plaintext(html, body_id=None, encoding='utf-8'):
""" From an HTML text, convert the HTML to plain text. """ From an HTML text, convert the HTML to plain text.
@ -354,150 +348,51 @@ def html2plaintext(html, body_id=None, encoding='utf-8'):
return html return html
def generate_tracking_message_id(openobject_id): def generate_tracking_message_id(res_id):
"""Returns a string that can be used in the Message-ID RFC822 header field """Returns a string that can be used in the Message-ID RFC822 header field
Used to track the replies related to a given object thanks to the "In-Reply-To" Used to track the replies related to a given object thanks to the "In-Reply-To"
or "References" fields that Mail User Agents will set. or "References" fields that Mail User Agents will set.
""" """
return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname()) return "<%s-openerp-%s@%s>" % (time.time(), res_id, socket.gethostname())
def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False, debug=False):
""" Low-level method to send directly a Message through the configured smtp server.
:param smtp_from: RFC-822 envelope FROM (not displayed to recipient)
:param smtp_to_list: RFC-822 envelope RCPT_TOs (not displayed to recipient)
:param message: an email.message.Message to send
:param debug: True if messages should be output to stderr before being sent,
and smtplib.SMTP put into debug mode.
:return: True if the mail was delivered successfully to the smtp,
else False (+ exception logged)
"""
class WriteToLogger(object):
def __init__(self):
self.logger = loglevels.Logger()
def write(self, s):
self.logger.notifyChannel('email_send', loglevels.LOG_DEBUG, s)
if openobject_id:
message['Message-Id'] = generate_tracking_message_id(openobject_id)
try:
smtp_server = config['smtp_server']
if smtp_server.startswith('maildir:/'):
from mailbox import Maildir
maildir_path = smtp_server[8:]
mdir = Maildir(maildir_path,factory=None, create = True)
mdir.add(message.as_string(True))
return True
oldstderr = smtplib.stderr
if not ssl: ssl = config.get('smtp_ssl', False)
s = smtplib.SMTP()
try:
# in case of debug, the messages are printed to stderr.
if debug:
smtplib.stderr = WriteToLogger()
s.set_debuglevel(int(bool(debug))) # 0 or 1
s.connect(smtp_server, config['smtp_port'])
if ssl:
s.ehlo()
s.starttls()
s.ehlo()
if config['smtp_user'] or config['smtp_password']:
s.login(config['smtp_user'], config['smtp_password'])
s.sendmail(smtp_from, smtp_to_list, message.as_string())
finally:
try:
s.quit()
if debug:
smtplib.stderr = oldstderr
except Exception:
# ignored, just a consequence of the previous exception
pass
except Exception:
_logger.error('could not deliver email', exc_info=True)
return False
return True
def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'): attachments=None, message_id=None, references=None, openobject_id=False, debug=False, subtype='plain', headers=None,
smtp_server=None, smtp_port=None, ssl=False, smtp_user=None, smtp_password=None, cr=None, uid=None):
"""Low-level function for sending an email (deprecated).
"""Send an email. :deprecate: since OpenERP 6.1, please use ir.mail_server.send_email() instead.
:param email_from: A string used to fill the `From` header, if falsy,
@param email_from A string used to fill the `From` header, if falsy, config['email_from'] is used instead. Also used for
config['email_from'] is used instead. Also used for the `Reply-To` header if `reply_to` is not provided
the `Reply-To` header if `reply_to` is not provided :param email_to: a sequence of addresses to send the mail to.
@param email_to a sequence of addresses to send the mail to.
""" """
if x_headers is None:
x_headers = {}
# If not cr, get cr from current thread database
if not cr:
db_name = getattr(threading.currentThread(), 'dbname', None)
if db_name:
cr = pooler.get_db_only(db_name).cursor()
else:
raise Exception("No database cursor found, please pass one explicitly")
if not (email_from or config['email_from']): # Send Email
raise ValueError("Sending an email requires either providing a sender " try:
"address or having configured one") mail_server_pool = pooler.get_pool(cr.dbname).get('ir.mail_server')
res = False
# Pack Message into MIME Object
email_msg = mail_server_pool.build_email(email_from, email_to, subject, body, email_cc, email_bcc, reply_to,
attachments, message_id, references, openobject_id, subtype, headers=headers)
if not email_from: email_from = config.get('email_from', False) res = mail_server_pool.send_email(cr, uid or 1, email_msg, mail_server_id=None,
email_from = ustr(email_from).encode('utf-8') smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user, smtp_password=smtp_password,
smtp_encryption=('ssl' if ssl else None), debug=debug)
if not email_cc: email_cc = [] except Exception:
if not email_bcc: email_bcc = [] _log.exception("tools.email_send failed to deliver email")
if not body: body = u'' return False
finally:
email_body = ustr(body).encode('utf-8') cr.close()
email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8') return res
msg = MIMEMultipart()
msg['Subject'] = Header(ustr(subject), 'utf-8')
msg['From'] = email_from
del msg['Reply-To']
if reply_to:
msg['Reply-To'] = reply_to
else:
msg['Reply-To'] = msg['From']
msg['To'] = COMMASPACE.join(email_to)
if email_cc:
msg['Cc'] = COMMASPACE.join(email_cc)
if email_bcc:
msg['Bcc'] = COMMASPACE.join(email_bcc)
msg['Date'] = formatdate(localtime=True)
msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
# Add dynamic X Header
for key, value in x_headers.iteritems():
msg['%s' % key] = str(value)
if html2text and subtype == 'html':
text = html2text(email_body.decode('utf-8')).encode('utf-8')
alternative_part = MIMEMultipart(_subtype="alternative")
alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
alternative_part.attach(email_text)
msg.attach(alternative_part)
else:
msg.attach(email_text)
if attach:
for (fname,fcontent) in attach:
part = MIMEBase('application', "octet-stream")
part.set_payload( fcontent )
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
msg.attach(part)
return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug)
#---------------------------------------------------------- #----------------------------------------------------------
# SMS # SMS
@ -1089,10 +984,7 @@ def detect_server_timezone():
return 'UTC' return 'UTC'
def get_server_timezone(): def get_server_timezone():
# timezone detection is safe in multithread, so lazy init is ok here return "UTC"
if (not config['timezone']):
config['timezone'] = detect_server_timezone()
return config['timezone']
DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d" DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"

View File

@ -27,6 +27,16 @@ import openerp.netsvc as netsvc
import openerp.pooler as pooler import openerp.pooler as pooler
class workflow_service(netsvc.Service): class workflow_service(netsvc.Service):
"""
Sometimes you might want to fire a signal or re-evaluate the current state
of a workflow using the service's API. You can access the workflow services
using:
>>> import netsvc
>>> wf_service = netsvc.LocalService("workflow")
"""
def __init__(self, name='workflow'): def __init__(self, name='workflow'):
netsvc.Service.__init__(self, name) netsvc.Service.__init__(self, name)
self.wkf_on_create_cache={} self.wkf_on_create_cache={}
@ -35,12 +45,31 @@ class workflow_service(netsvc.Service):
self.wkf_on_create_cache[cr.dbname]={} self.wkf_on_create_cache[cr.dbname]={}
def trg_write(self, uid, res_type, res_id, cr): def trg_write(self, uid, res_type, res_id, cr):
"""
Reevaluates the specified workflow instance. Thus if any condition for
a transition have been changed in the backend, then running ``trg_write``
will move the workflow over that transition.
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:param cr: a database cursor
"""
ident = (uid,res_type,res_id) ident = (uid,res_type,res_id)
cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s', (res_id or None,res_type or None, 'active')) cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s', (res_id or None,res_type or None, 'active'))
for (id,) in cr.fetchall(): for (id,) in cr.fetchall():
instance.update(cr, id, ident) instance.update(cr, id, ident)
def trg_trigger(self, uid, res_type, res_id, cr): def trg_trigger(self, uid, res_type, res_id, cr):
"""
Activate a trigger.
If a workflow instance is waiting for a trigger from another model, then this
trigger can be activated if its conditions are met.
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:param cr: a database cursor
"""
cr.execute('select instance_id from wkf_triggers where res_id=%s and model=%s', (res_id,res_type)) cr.execute('select instance_id from wkf_triggers where res_id=%s and model=%s', (res_id,res_type))
res = cr.fetchall() res = cr.fetchall()
for (instance_id,) in res: for (instance_id,) in res:
@ -49,10 +78,24 @@ class workflow_service(netsvc.Service):
instance.update(cr, instance_id, ident) instance.update(cr, instance_id, ident)
def trg_delete(self, uid, res_type, res_id, cr): def trg_delete(self, uid, res_type, res_id, cr):
"""
Delete a workflow instance
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:param cr: a database cursor
"""
ident = (uid,res_type,res_id) ident = (uid,res_type,res_id)
instance.delete(cr, ident) instance.delete(cr, ident)
def trg_create(self, uid, res_type, res_id, cr): def trg_create(self, uid, res_type, res_id, cr):
"""
Create a new workflow instance
:param res_type: the model name
:param res_id: the model instance id to own the created worfklow instance
:param cr: a database cursor
"""
ident = (uid,res_type,res_id) ident = (uid,res_type,res_id)
self.wkf_on_create_cache.setdefault(cr.dbname, {}) self.wkf_on_create_cache.setdefault(cr.dbname, {})
if res_type in self.wkf_on_create_cache[cr.dbname]: if res_type in self.wkf_on_create_cache[cr.dbname]:
@ -65,6 +108,14 @@ class workflow_service(netsvc.Service):
instance.create(cr, ident, wkf_id) instance.create(cr, ident, wkf_id)
def trg_validate(self, uid, res_type, res_id, signal, cr): def trg_validate(self, uid, res_type, res_id, signal, cr):
"""
Fire a signal on a given workflow instance
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:signal: the signal name to be fired
:param cr: a database cursor
"""
result = False result = False
ident = (uid,res_type,res_id) ident = (uid,res_type,res_id)
# ids of all active workflow instances for a corresponding resource (id, model_nam) # ids of all active workflow instances for a corresponding resource (id, model_nam)
@ -74,10 +125,19 @@ class workflow_service(netsvc.Service):
result = result or res2 result = result or res2
return result return result
# make all workitems which are waiting for a (subflow) workflow instance
# for the old resource point to the (first active) workflow instance for
# the new resource
def trg_redirect(self, uid, res_type, res_id, new_rid, cr): def trg_redirect(self, uid, res_type, res_id, new_rid, cr):
"""
Re-bind a workflow instance to another instance of the same model.
Make all workitems which are waiting for a (subflow) workflow instance
for the old resource point to the (first active) workflow instance for
the new resource.
:param res_type: the model name
:param res_id: the model instance id the workflow belongs to
:param new_rid: the model instance id to own the worfklow instance
:param cr: a database cursor
"""
# get ids of wkf instances for the old resource (res_id) # get ids of wkf instances for the old resource (res_id)
#CHECKME: shouldn't we get only active instances? #CHECKME: shouldn't we get only active instances?
cr.execute('select id, wkf_id from wkf_instance where res_id=%s and res_type=%s', (res_id, res_type)) cr.execute('select id, wkf_id from wkf_instance where res_id=%s and res_type=%s', (res_id, res_type))

View File

@ -210,6 +210,7 @@ Section OpenERP_Server SectionOpenERP_Server
WriteIniStr "$INSTDIR\openerp-server.conf" "options" "db_user" $TextPostgreSQLUsername WriteIniStr "$INSTDIR\openerp-server.conf" "options" "db_user" $TextPostgreSQLUsername
WriteIniStr "$INSTDIR\openerp-server.conf" "options" "db_password" $TextPostgreSQLPassword WriteIniStr "$INSTDIR\openerp-server.conf" "options" "db_password" $TextPostgreSQLPassword
WriteIniStr "$INSTDIR\openerp-server.conf" "options" "db_port" $TextPostgreSQLPort WriteIniStr "$INSTDIR\openerp-server.conf" "options" "db_port" $TextPostgreSQLPort
WriteIniStr "$INSTDIR\openerp-server.conf" "options" "pg_path" "$INSTDIR\PostgreSQL\bin"
nsExec::Exec '"$INSTDIR\openerp-server.exe" --stop-after-init --logfile "$INSTDIR\openerp-server.log" -s' nsExec::Exec '"$INSTDIR\openerp-server.exe" --stop-after-init --logfile "$INSTDIR\openerp-server.log" -s'
nsExec::Exec '"$INSTDIR\service\OpenERPServerService.exe" -auto -install' nsExec::Exec '"$INSTDIR\service\OpenERPServerService.exe" -auto -install'

View File

@ -89,7 +89,7 @@ if os.name == 'nt':
"pydot", "asyncore","asynchat", "reportlab", "vobject", "pydot", "asyncore","asynchat", "reportlab", "vobject",
"HTMLParser", "select", "mako", "poplib", "HTMLParser", "select", "mako", "poplib",
"imaplib", "smtplib", "email", "yaml", "DAV", "imaplib", "smtplib", "email", "yaml", "DAV",
"uuid", "commands", "openerp", "uuid", "commands", "openerp", "simplejson", "vatnumber"
], ],
"excludes" : ["Tkconstants","Tkinter","tcl"], "excludes" : ["Tkconstants","Tkinter","tcl"],
} }
@ -165,6 +165,7 @@ setup(name = name,
'pywebdav', 'pywebdav',
'feedparser', 'feedparser',
'simplejson >= 2.0', 'simplejson >= 2.0',
'vatnumber', # required by base_vat module
], ],
extras_require = { extras_require = {
'SSL' : ['pyopenssl'], 'SSL' : ['pyopenssl'],