[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
GNU Public Licence.
(c) 2003-TODAY, Fabien Pinckaers - OpenERP s.a.
(c) 2003-TODAY, Fabien Pinckaers - OpenERP SA
"""
import logging
@ -88,8 +88,11 @@ def setup_pid_file():
def preload_registry(dbname):
""" Preload a registry, and start the cron."""
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
pool.get('ir.cron').restart(db.dbname)
try:
db, 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):
""" Preload a registry, possibly run a test file, and start the cron."""
@ -233,6 +236,8 @@ def quit_on_signals():
if __name__ == "__main__":
os.environ["TZ"] = "UTC"
check_root_user()
openerp.tools.config.parse_config(sys.argv[1:])
check_postgres_user()

View File

@ -67,7 +67,7 @@
'res/res_currency_view.xml',
'res/res_partner_event_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_wizard_ean_check_view.xml',
'res/res_partner_data.xml',

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -418,7 +418,10 @@ class server_object_lines(osv.osv):
_columns = {
'server_id': fields.many2one('ir.actions.server', 'Object Mapping'),
'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([
('value','Value'),
('equation','Formula')
@ -435,14 +438,15 @@ server_object_lines()
class actions_server(osv.osv):
def _select_signals(self, cr, uid, context=None):
cr.execute("SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t \
WHERE w.id = a.wkf_id AND t.act_from = a.id OR t.act_to = a.id AND t.signal!='' \
AND t.signal NOT IN (null, NULL)")
cr.execute("""SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t
WHERE w.id = a.wkf_id AND
(t.act_from = a.id OR t.act_to = a.id) AND
t.signal IS NOT NULL""")
result = cr.fetchall() or []
res = []
for rs in result:
if rs[0] is not None and rs[1] is not None:
line = rs[0], "%s - (%s)" % (rs[1], rs[0])
line = rs[1], "%s - (%s)" % (rs[1], rs[0])
res.append(line)
return res
@ -453,13 +457,15 @@ class actions_server(osv.osv):
return [(r['model'], r['name']) for r in res] + [('','')]
def change_object(self, cr, uid, ids, copy_object, state, context=None):
if state == 'object_copy':
if state == 'object_copy' and copy_object:
if context is None:
context = {}
model_pool = self.pool.get('ir.model')
model = copy_object.split(',')[0]
mid = model_pool.search(cr, uid, [('model','=',model)])
return {
'value':{'srcmodel_id':mid[0]},
'context':context
'value': {'srcmodel_id': mid[0]},
'context': context
}
else:
return {}
@ -469,8 +475,19 @@ class actions_server(osv.osv):
_sequence = 'ir_actions_id_seq'
_order = 'sequence,name'
_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),
'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"),
'name': fields.char('Action Name', required=True, size=64, translate=True),
'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([
('client_action','Client Action'),
('dummy','Dummy'),
@ -484,16 +501,20 @@ class actions_server(osv.osv):
('object_write','Write Object'),
('other','Multi Actions'),
], '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."),
'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create)."),
'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."),
'trigger_name': fields.selection(_select_signals, string='Trigger Name', size=128, help="Select the Signal name that is to be used as the trigger."),
'wkf_model_id': fields.many2one('ir.model', 'Workflow On', help="Workflow to be executed on this model."),
'trigger_obj_id': fields.many2one('ir.model.fields','Trigger On', help="Select the object from the model on which the workflow will executed."),
'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"),
'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 ]]`"),
'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 ]]`"),
'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
'wkf_model_id': fields.many2one('ir.model', 'Target Object', help="The object that should receive the workflow signal (must have an associated workflow)"),
'trigger_obj_id': fields.many2one('ir.model.fields','Relation Field', help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)"),
'email': fields.char('Email Address', size=512, help="Expression that returns the email address to send to. Can be based on the same values as for the condition field.\n"
"Example: object.invoice_address_id.email, or 'me@example.com'"),
'subject': fields.char('Subject', size=1024, translate=True, help="Email subject, may contain expressions enclosed in double brackets based on the same values as those "
"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"),
'sms': fields.char('SMS', size=160, translate=True),
'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',
'type': lambda *a: 'ir.actions.server',
'sequence': lambda *a: 5,
'code': lambda *a: """# You can use the following variables
# - object or obj
# - time
# - cr
# - uid
# - ids
'code': lambda *a: """# You can use the following variables:
# - self: ORM model of the record on which the action is triggered
# - object: browse_record of the record on which the action is triggered if there is one, otherwise None
# - pool: ORM model pool (i.e. self.pool)
# - time: Python time module
# - cr: database cursor
# - uid: current user id
# - context: current context
# 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):
if context is None:
context = {}
def merge(match):
obj_pool = self.pool.get(action.model_id.model)
id = context.get('active_id')
@ -602,15 +626,17 @@ class actions_server(osv.osv):
user = self.pool.get('res.users').browse(cr, uid, uid)
for action in self.browse(cr, uid, ids, context):
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'):
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
'self': obj_pool,
'object': obj,
'obj': obj,
'pool': self.pool,
'time': time,
'cr': cr,
'pool': self.pool,
'context': dict(context), # copy context to prevent side-effects of eval
'uid': uid,
'user': user
}
@ -625,21 +651,9 @@ class actions_server(osv.osv):
.read(cr, uid, action.action_id.id, context=context)
if action.state=='code':
localdict = {
'self': self.pool.get(action.model_id.model),
'pool': self.pool,
'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']
eval(action.code, cxt, mode="exec", nocopy=True) # nocopy allows to return 'action'
if 'action' in cxt:
return cxt['action']
if action.state == 'email':
email_from = config['email_from']
@ -652,7 +666,7 @@ class actions_server(osv.osv):
if not address:
logger.info('No partner email address specified, not sending any email.')
continue
if not email_from:
logger.debug('--email-from command line option is not specified, using a fallback value instead.')
if user.user_email:
@ -663,7 +677,10 @@ class actions_server(osv.osv):
subject = self.merge_message(cr, uid, action.subject, 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)
else:
logger.warning('Failed to send email to: %s', address)
@ -671,10 +688,10 @@ class actions_server(osv.osv):
if action.state == 'trigger':
wf_service = netsvc.LocalService("workflow")
model = action.wkf_model_id.model
obj_pool = self.pool.get(action.model_id.model)
res_id = self.pool.get(action.model_id.model).read(cr, uid, [context.get('active_id')], [action.trigger_obj_id.name])
id = res_id [0][action.trigger_obj_id.name]
wf_service.trg_validate(uid, model, int(id), action.trigger_name, cr)
m2o_field_name = action.trigger_obj_id.name
target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
target_id = target_id[0] if isinstance(target_id,tuple) else target_id
wf_service.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
if action.state == 'sms':
#TODO: set the user and password from the system
@ -689,20 +706,9 @@ class actions_server(osv.osv):
result = self.run(cr, uid, [act.id], context)
if result:
res.append(result)
return res
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)
context['object'] = obj
for i in expr:
@ -714,13 +720,6 @@ class actions_server(osv.osv):
for exp in action.fields_lines:
euq = exp.value
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)
else:
expr = exp.value
@ -754,14 +753,7 @@ class actions_server(osv.osv):
for exp in action.fields_lines:
euq = exp.value
if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
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,
})
expr = eval(euq, cxt)
else:
expr = exp.value
res[exp.col1.name] = expr
@ -778,21 +770,11 @@ class actions_server(osv.osv):
for exp in action.fields_lines:
euq = exp.value
if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
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,
})
expr = eval(euq, cxt)
else:
expr = exp.value
res[exp.col1.name] = expr
obj_pool = None
res_id = False
model = action.copy_object.split(',')[0]
cid = action.copy_object.split(',')[1]
obj_pool = self.pool.get(model)

View File

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

View File

@ -2,7 +2,7 @@
##############################################################################
#
# 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
# 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
@ -36,23 +36,19 @@ _default_parameters = {
}
class ir_config_parameter(osv.osv):
""" An osv to old configuration parameters for a given database.
To be short, it's just a global dictionary of strings stored in a table. """
"""Per-database storage of configuration key-value pairs."""
_name = 'ir.config_parameter'
_columns = {
# The key of the configuration parameter.
'key': fields.char('Key', size=256, required=True, select=1),
# The value of the configuration parameter.
'value': fields.text('Value', required=True),
}
_sql_constraints = [
('key_uniq', 'unique (key)', 'Key must be unique.')
]
def init(self, cr):
"""
Initializes the parameters listed in _default_parameters.
@ -63,12 +59,12 @@ class ir_config_parameter(osv.osv):
self.set_param(cr, 1, key, func())
def get_param(self, cr, uid, key, default=False, context=None):
""" Get the value of a parameter.
@param key: The key of the parameter.
@type key: string
@return: The value of the parameter, False if it does not exist.
@rtype: string
"""Retrieve the value for a given key.
:param string key: The key of the parameter value to retrieve.
:param string default: default value if parameter is missing.
:return: The value of the parameter, or ``default`` if it does not exist.
:rtype: string
"""
ids = self.search(cr, uid, [('key','=',key)], context=context)
if not ids:
@ -78,15 +74,13 @@ class ir_config_parameter(osv.osv):
return value
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.
@type key: string
@param value: The value of the parameter.
@type value: string
@return: Return the previous value of the parameter of False if it did
not existed.
@rtype: string
:param string key: The key of the parameter value to set.
:param string value: The value to set.
:return: the previous value of the parameter or False if it did
not exist.
:rtype: string
"""
ids = self.search(cr, uid, [('key','=',key)], context=context)
if ids:
@ -97,5 +91,3 @@ class ir_config_parameter(osv.osv):
else:
self.create(cr, uid, {'key': key, 'value': value}, context=context)
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):
_name = 'ir.model'
_description = "Objects"
_description = "Models"
_order = 'model'
def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
@ -85,8 +85,8 @@ class ir_model(osv.osv):
return res
_columns = {
'name': fields.char('Object Name', size=64, translate=True, required=True),
'model': fields.char('Object', size=64, required=True, select=1),
'name': fields.char('Model Description', size=64, translate=True, required=True),
'model': fields.char('Model', size=64, required=True, select=1),
'info': fields.text('Information'),
'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
@ -97,12 +97,12 @@ class ir_model(osv.osv):
'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the object is defined or inherited'),
'view_ids': fields.function(_view_ids, method=True, type='one2many', obj='ir.ui.view', string='Views'),
}
_defaults = {
'model': lambda *a: 'x_',
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
}
def _check_model_name(self, cr, uid, ids, context=None):
for model in self.browse(cr, uid, ids, context=context):
if model.state=='manual':
@ -114,9 +114,13 @@ class ir_model(osv.osv):
def _model_name_msg(self, cr, uid, ids, context=None):
return _('The Object name must start with x_ and not contain any special character !')
_constraints = [
(_check_model_name, _model_name_msg, ['model']),
]
_sql_constraints = [
('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
]
# overridden to allow searching both on model name (model field)
# and model description (name field)
@ -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"""
ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
if not ids:
raise ValueError('No references to %s.%s' % (module, xml_id))
raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
# the sql constraints ensure us we have only one result
return ids[0]
@ -626,7 +630,7 @@ class ir_model_data(osv.osv):
data_id = self._get_id(cr, uid, module, xml_id)
res = self.read(cr, uid, data_id, ['model', 'res_id'])
if not res['res_id']:
raise ValueError('No references to %s.%s' % (module, xml_id))
raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
return (res['model'], res['res_id'])
def get_object(self, cr, uid, module, xml_id, context=None):

View File

@ -108,8 +108,8 @@ class ir_translation(osv.osv):
for res_id in tr:
if 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_ids.clear_cache(self, uid, name, tt, lang, ids)
cr.execute('delete from ir_translation ' \
'where lang=%s ' \

View File

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

View File

@ -79,7 +79,10 @@ class ir_values(osv.osv):
method=True, type='text', string='Value'),
'object': fields.boolean('Is Object'),
'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_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle,
method=True, type='text', string='Metadata'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -194,7 +194,7 @@ class lang(osv.osv):
trans_obj.unlink(cr, uid, trans_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"""
if percent[0] != '%':

View File

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

View File

@ -147,25 +147,16 @@ class users(osv.osv):
return cr.fetchall()
def send_welcome_email(self, cr, uid, id, context=None):
logger= netsvc.Logger()
user = self.pool.get('res.users').read(cr, uid, id, context=context)
if not user.get('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
if isinstance(id,list): id = id[0]
user = self.read(cr, uid, id, ['email','login','name', 'user_email'], context=context)
email = user['email'] or user['user_email']
return tools.email_send(email_from=None, email_to=[user['email']],
subject=self.get_welcome_mail_subject(
cr, uid, context=context),
body=self.get_welcome_mail_body(
cr, uid, context=context) % user)
ir_mail_server = self.pool.get('ir.mail_server')
msg = ir_mail_server.build_email(email_from=None, # take config default
email_to=[email],
subject=self.get_welcome_mail_subject(cr, uid, context=context),
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):
"""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)
SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
def write(self, cr, uid, ids, values, context=None):
if not hasattr(ids, '__iter__'):
@ -562,7 +553,7 @@ class users_implied(osv.osv):
_inherit = 'res.users'
def create(self, cr, uid, values, context=None):
groups = values.pop('groups_id')
groups = values.pop('groups_id', None)
user_id = super(users_implied, self).create(cr, uid, values, context)
if groups:
# delegate addition of groups to add implied groups

View File

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

View File

@ -19,15 +19,16 @@
#
##############################################################################
import netsvc
import tools
from osv import fields, osv
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 """
_name = "partner.wizard.spam"
_name = "partner.massmail.wizard"
_description = "Mass Mailing"
_columns = {
@ -37,45 +38,45 @@ class partner_wizard_spam(osv.osv_memory):
}
def mass_mail_send(self, cr, uid, ids, context):
"""
Send Email
"""Send the given mail to all partners whose ids
are present in ``context['active_ids']``, to
all addresses with an email set.
@param cr: the current row, from the database cursor.
@param uid: the current users ID for security checks.
@param ids: the ID or list of IDs
@param context: A standard dictionary
:param dict context: ``context['active_ids']``
should contain the list of
ids of the partners who should
receive the mail.
"""
nbr = 0
partner_pool = self.pool.get('res.partner')
data = self.browse(cr, uid, ids[0], context=context)
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)
type_ = 'plain'
subtype = 'plain'
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 adr in partner.address:
if adr.email:
name = adr.name or partner.name
to = '"%s" <%s>' % (name, adr.email)
#TODO: add some tests to check for invalid email addresses
#CHECKME: maybe we should use res.partner/email_send
tools.email_send(data.email_from,
[to],
data.subject,
data.text,
subtype=type_,
openobject_id="res.partner-%s"%partner.id)
nbr += 1
if adr.email and not adr.email in emails_seen:
try:
emails_seen.add(adr.email)
name = adr.name or partner.name
to = '"%s" <%s>' % (name, adr.email)
msg = ir_mail_server.build_email(data.email_from, [to], data.subject, data.text, subtype=subtype)
if ir_mail_server.send_email(cr, uid, msg):
nbr += 1
except Exception:
#ignore failed deliveries, will be logged anyway
pass
event_pool.create(cr, uid,
{'name': 'Email(s) sent through mass mailing',
'partner_id': partner.id,
'description': data.text })
#TODO: log number of message sent
_logger.info('Mass-mailing wizard sent %s emails', nbr)
return {'email_sent': nbr}
partner_wizard_spam()
# 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">
<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="arch" type="xml">
<form string="Mass Mailing" col="4">
@ -24,7 +24,7 @@
</record>
<act_window name="Mass Mailing"
res_model="partner.wizard.spam"
res_model="partner.massmail.wizard"
src_model="res.partner"
view_mode="form"
target="new"

View File

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

View File

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

View File

@ -48,6 +48,7 @@
"access_res_country_state_group_user","res_country_state group_user","model_res_country_state","group_partner_manager",1,1,1,1
"access_res_currency_group_all","res_currency group_all","model_res_currency",,1,0,0,0
"access_res_currency_rate_group_all","res_currency_rate group_all","model_res_currency_rate",,1,0,0,0
"access_res_currency_rate_type_group_all","res_currency_rate_type group_all","model_res_currency_rate_type",,1,0,0,0
"access_res_currency_group_system","res_currency group_system","model_res_currency","group_system",1,1,1,1
"access_res_currency_rate_group_system","res_currency_rate group_system","model_res_currency_rate","group_system",1,1,1,1
"access_res_groups_group_erp_manager","res_groups group_erp_manager","model_res_groups","group_erp_manager",1,1,1,1
@ -123,4 +124,6 @@
"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_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0
"access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0
"access_ir_actions_todo_category","ir_actions_todo_category","model_ir_actions_todo_category","group_system",1,1,1,1
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0

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

View File

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

View File

@ -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""")
for (model, name) in cr.fetchall():
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))
cr.execute("SELECT model from ir_model")

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@
- classicals (varchar, integer, boolean, ...)
- relations (one2many, many2one, many2many)
- functions
"""
import calendar
@ -165,7 +165,7 @@ def modifiers_tests():
test_modifiers({}, '{}')
test_modifiers({"invisible": True}, '{"invisible": true}')
test_modifiers({"invisible": False}, '{}')
def check_object_name(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):
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):
def __init__(self, name, value):
self.name = name
@ -252,7 +265,7 @@ class browse_null(object):
#
class browse_record_list(list):
""" Collection of browse objects
Such an instance will be returned when doing a ``browse([ids..])``
and will be iterable, yielding browse() objects
"""
@ -267,9 +280,9 @@ class browse_record_list(list):
class browse_record(object):
""" An object that behaves like a row of an object's table.
It has attributes after the columns of the corresponding object.
Examples::
uobj = pool.get('res.users')
user_rec = uobj.browse(cr, uid, 104)
name = user_rec.name
@ -326,9 +339,12 @@ class browse_record(object):
col = self._table._inherit_fields[name][2]
elif hasattr(self._table, str(name)):
attr = getattr(self._table, name)
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:
return attr
else:
@ -475,6 +491,16 @@ class browse_record(object):
__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):
"""
@ -587,22 +613,32 @@ class orm_template(object):
_order = 'id'
_sequence = None
_description = None
# structure:
# { 'parent_model': 'm2o_field', ... }
_inherits = {}
# Mapping from inherits'd field name to triple (m, r, f)
# where m is the model from which it is inherits'd,
# r is the (local) field towards m,
# and f is the _column object itself.
# Mapping from inherits'd field name to triple (m, r, f, n) where m is the
# model from which it is inherits'd, r is the (local) field towards m, f
# 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 = {}
# Mapping field name/column_info object
# This is similar to _inherit_fields but:
# 1. includes self fields,
# 2. uses column_info instead of a triple.
_all_columns = {}
_table = None
_invalids = set()
_log_create = False
CONCURRENCY_CHECK_FIELD = '__last_update'
def log(self, cr, uid, id, message, secondary=False, context=None):
if context and context.get('disable_log'):
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))
nattr = {}
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':
# Don't _inherit custom fields.
for c in new.keys():
@ -872,7 +908,7 @@ class orm_template(object):
elif field_type == 'integer':
return 0
elif field_type == 'boolean':
return False
return 'False'
return ''
def selection_field(in_field):
@ -984,11 +1020,7 @@ class orm_template(object):
cols = self._columns.copy()
for f in self._inherit_fields:
cols.update({f: self._inherit_fields[f][2]})
def fsplit(fieldname):
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)
fields_to_export = map(fix_import_export_id_paths, fields_to_export)
datas = []
for row in self.browse(cr, uid, ids, context):
datas += self.__export_row(cr, uid, row, fields_to_export, context)
@ -1024,10 +1056,7 @@ class orm_template(object):
"""
if not context:
context = {}
def _replace_field(x):
x = re.sub('([a-z0-9A-Z_])\\.id$', '\\1/.id', x)
return x.replace(':id','/id').split('/')
fields = map(_replace_field, fields)
fields = map(fix_import_export_id_paths, fields)
logger = netsvc.Logger()
ir_model_data_obj = self.pool.get('ir.model.data')
@ -1089,7 +1118,7 @@ class orm_template(object):
if line[i] and skip:
return False
continue
#set the mode for m2o, o2m, m2m : xml_id/id/name
if len(field) == len(prefix)+1:
mode = False
@ -1102,7 +1131,7 @@ class orm_template(object):
for db_id in line.split(config.get('csv_internal_sep')):
res.append(_get_id(relation, db_id, current_module, mode))
return [(6,0,res)]
# ID of the record using a XML ID
if field[len(prefix)]=='id':
try:
@ -1126,9 +1155,9 @@ class orm_template(object):
relation_obj = self.pool.get(relation)
newfd = relation_obj.fields_get( cr, uid, context=context )
pos = position
res = many_ids(line[i], relation, current_module, mode)
first = 0
while pos < len(datas):
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)
warning += w2
first += 1
if data_res_id2:
res.append((4, data_res_id2))
if (not newrow) or not reduce(lambda x, y: x or y, newrow.values(), 0):
break
res.append( (data_res_id2 and 1 or 0, data_res_id2 or 0, newrow) )
elif fields_def[field[len(prefix)]]['type']=='many2one':
relation = fields_def[field[len(prefix)]]['relation']
@ -1175,7 +1204,7 @@ class orm_template(object):
else:
res = line[i]
row[field[len(prefix)]] = res or False
result = (row, nbrmax, warning, data_res_id, xml_id)
@ -1189,7 +1218,7 @@ class orm_template(object):
position = 0
while position<len(datas):
res = {}
(res, position, warning, res_id, xml_id) = \
process_liness(self, datas, [], current_module, self._name, fields_def, position=position)
if len(warning):
@ -1201,7 +1230,7 @@ class orm_template(object):
current_module, res, mode=mode, xml_id=xml_id,
noupdate=noupdate, res_id=res_id, context=context)
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)):
data = pickle.load(file(config.get('import_partial')))
@ -1556,7 +1585,7 @@ class orm_template(object):
field = model_fields.get(node.get('name'))
if field:
transfer_field_to_modifiers(field, modifiers)
elif node.tag in ('form', 'tree'):
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)
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.
:param source: a parent architecture to modify (with parent
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
are applied
"""
# get all views which inherit from (ie modify) this view
cr.execute('select arch,id from ir_ui_view where inherit_id=%s and model=%s order by priority', (inherit_id, self._name))
sql_inherit = cr.fetchall()
for (inherit, id) in sql_inherit:
source = apply_inheritance_specs(source, inherit, id)
source = apply_view_inheritance(source, id)
sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, user, inherit_id, self._name)
for (view_arch, view_id) in sql_inherit:
source = apply_inheritance_specs(source, view_arch, view_id)
source = apply_view_inheritance(cr, user, source, view_id)
return source
result = {'type': view_type, 'model': self._name}
@ -1945,7 +1972,7 @@ class orm_template(object):
result['view_id'] = sql_res['id']
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['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 !'))
def name_get(self, cr, user, ids, context=None):
"""
:param cr: database cursor
:param user: current user id
:type user: integer
: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
"""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
the model implements a custom behavior.
Can sometimes be seen as the inverse function of :meth:`~.name_search`, but it is not
guaranteed to be.
:rtype: list(tuple)
:return: list of pairs ``(id,text_repr)`` for all records with the given ``ids``.
"""
if not context:
context = {}
if not ids:
return []
if isinstance(ids, (int, long)):
@ -2118,38 +2141,39 @@ class orm_template(object):
[self._rec_name], context, load='_classic_write')]
def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
"""
Search for records and their display names according to a search domain.
"""Search for records that have a display name matching the given ``name`` pattern if compared
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
:param user: current user id
: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**.
This method is equivalent to calling :meth:`~.search` with a search domain based on ``name``
and then :meth:`~.name_get` on the result of the search.
: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)
def name_create(self, cr, uid, name, context=None):
"""
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).
The new record will also be initialized with any default values applicable
to this model, or provided through the context. The usual behavior of
:py:meth:`~osv.osv.osv.create` applies.
Similarly, this method may raise an exception if the model has multiple
required fields and some do not have default values.
"""Creates a new record by calling :meth:`~.create` with only one
value provided: the name of the new record (``_rec_name`` field).
The new record will also be initialized with any default values applicable
to this model, or provided through the context. The usual behavior of
:meth:`~.create` applies.
Similarly, this method may raise an exception if the model has multiple
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);
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):
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 !'))
def read_string(self, cr, uid, id, langs, fields=None, context=None):
@ -2259,6 +2295,16 @@ class orm_template(object):
except AttributeError:
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):
_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)]
if args:
import expression
e = expression.expression(args)
e.parse(cr, user, self, context)
e = expression.expression(cr, user, args, self, context)
res = e.exp
return res or []
@ -2445,6 +2490,9 @@ class orm_memory(orm_template):
break
f = True
for arg in result:
if len(arg) != 3:
# Amazing hack: orm_memory handles only simple domains.
continue
if arg[1] == '=':
val = eval('data[arg[0]]'+'==' +' arg[2]', locals())
elif arg[1] in ['<', '>', 'in', 'not in', '<=', '>=', '<>']:
@ -2488,8 +2536,28 @@ class orm_memory(orm_template):
# nothing to check in memory...
pass
def exists(self, cr, uid, id, context=None):
return id in self.datas
def exists(self, cr, uid, ids, context=None):
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):
_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']
__logger = logging.getLogger('orm')
__schema = logging.getLogger('orm.schema')
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
@ -2617,20 +2686,22 @@ class orm(orm_template):
del d['id']
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)
:param current_table: current model object
:param parent_model_name: name of the parent model for which the clauses should be added
:param query: query object on which the JOIN should be added
"""
inherits_field = self._inherits[parent_model_name]
inherits_field = current_table._inherits[parent_model_name]
parent_model = self.pool.get(parent_model_name)
parent_table_name = parent_model._table
quoted_parent_table_name = '"%s"' % parent_table_name
if quoted_parent_table_name not in query.tables:
query.tables.append(quoted_parent_table_name)
query.where_clause.append('("%s".%s = %s.id)' % (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):
"""
@ -2645,7 +2716,7 @@ class orm(orm_template):
while field in current_table._inherit_fields and not field in current_table._columns:
parent_model_name = current_table._inherit_fields[field][0]
parent_table = self.pool.get(parent_model_name)
self._inherits_join_add(parent_model_name, query)
self._inherits_join_add(current_table, parent_model_name, query)
current_table = parent_table
return '"%s".%s' % (current_table._table, field)
@ -2707,7 +2778,7 @@ class orm(orm_template):
pass
if not val_id:
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
else:
val = value
@ -2717,13 +2788,13 @@ class orm(orm_template):
elif val in dict(self._columns[field].selection(self, cr, uid, context=context)):
return
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):
# 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)
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"
" FROM pg_class c, pg_attribute a"
" WHERE c.relname=%s"
@ -2790,7 +2861,7 @@ class orm(orm_template):
column_data = self._select_column_data(cr)
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
# Don't update custom (also called manual) 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" 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("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._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:
# add the missing field
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._table, k, get_pg_type(f)[1])
@ -3053,7 +3124,7 @@ class orm(orm_template):
def _create_table(self, cr):
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)
@ -3092,23 +3163,17 @@ class orm(orm_template):
def _add_log_columns(self, cr):
logs = {
'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:
for field, field_def in LOG_ACCESS_COLUMNS.iteritems():
cr.execute("""
SELECT c.relname
FROM pg_class c, pg_attribute a
WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid
""", (self._table, k))
""", (self._table, field))
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()
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):
@ -3263,7 +3328,7 @@ class orm(orm_template):
if f == order:
ok = False
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]))
for (key, _, msg) in self._sql_constraints:
@ -3336,9 +3401,9 @@ class orm(orm_template):
for table in self._inherits:
other = self.pool.get(table)
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():
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._all_columns = self._get_column_infos()
self._inherits_reload_src()
@ -3349,8 +3414,8 @@ class orm(orm_template):
inherited field via _inherits) to a ``column_info`` struct
giving detailed columns """
result = {}
for k, (parent, m2o, col) in self._inherit_fields.iteritems():
result[k] = fields.column_info(k, col, parent, m2o)
for k, (parent, m2o, col, original_parent) in self._inherit_fields.iteritems():
result[k] = fields.column_info(k, col, parent, m2o, original_parent)
for k, col in self._columns.iteritems():
result[k] = fields.column_info(k, col)
return result
@ -3454,7 +3519,7 @@ class orm(orm_template):
res = []
if len(fields_pre):
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'):
return "date_trunc('second', %s) as %s" % (f_qual, f)
if f == self.CONCURRENCY_CHECK_FIELD:
@ -4059,7 +4124,7 @@ class orm(orm_template):
upd_todo = []
for v in vals.keys():
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]
del vals[v]
else:
@ -4206,44 +4271,52 @@ class orm(orm_template):
:return: [(priority, model_name, [record_ids,], [function_fields,])]
"""
# FIXME: rewrite, cleanup, use real variable names
# e.g.: http://pastie.org/1222060
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
if fields is None: fields = []
stored_functions = self.pool._store_function.get(self._name, [])
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
ids2 = fncts[fnct][2](self, cr, ROOT_USER_ID, ids, context)
for id in filter(None, ids2):
result[fncts[fnct][0]].setdefault(id, [])
result[fncts[fnct][0]][id].append(fnct)
dict = {}
for object in result:
k2 = {}
for id, fnct in result[object].items():
k2.setdefault(tuple(fnct), [])
k2[tuple(fnct)].append(id)
for fnct, id in k2.items():
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 = []
tmp = dict.keys()
tmp.sort()
for k in tmp:
result2 += dict[k]
return result2
target_ids = [id for id in function[id_mapping_fnct_](self, cr, ROOT_USER_ID, ids, context) if id]
# the compound key must consider the priority and model name
key = (function[priority_], function[model_name_])
for target_id in target_ids:
mapping.setdefault(key, {}).setdefault(target_id,set()).add(tuple(function))
# Here mapping looks like:
# { (10, 'model_a') : { target_id1: [ (function_1_tuple, function_2_tuple) ], ... }
# (20, 'model_a') : { target_id2: [ (function_3_tuple, function_4_tuple) ], ... }
# (99, 'model_a') : { target_id1: [ (function_5_tuple, function_6_tuple) ], ... }
# }
# Now we need to generate the batch function calls list
# call_map =
# { (10, 'model_a') : [(10, 'model_a', [record_ids,], [function_fields,])] }
call_map = {}
for ((priority,model), id_map) in mapping.iteritems():
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):
"""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:
import expression
e = expression.expression(domain)
e.parse(cr, user, self, context)
e = expression.expression(cr, user, domain, self, context)
tables = e.get_tables()
where_clause, where_params = e.to_sql()
where_clause = where_clause and [where_clause] or []
@ -4381,7 +4453,7 @@ class orm(orm_template):
if parent_model and child_object:
# as inherited rules are being applied, we need to add the missing JOIN
# to reach the parent table (if it was not JOINed yet in the query)
child_object._inherits_join_add(parent_model, query)
child_object._inherits_join_add(child_object, parent_model, query)
query.where_clause += added_clause
query.where_clause_params += added_params
for table in added_tables:
@ -4470,7 +4542,7 @@ class orm(orm_template):
else:
continue # ignore non-readable or "non-joinable" fields
elif order_field in self._inherit_fields:
parent_obj = self.pool.get(self._inherit_fields[order_field][0])
parent_obj = self.pool.get(self._inherit_fields[order_field][3])
order_column = parent_obj._columns[order_field]
if order_column._classic_read:
inner_clause = self._inherits_join_calc(order_field, query)
@ -4577,7 +4649,7 @@ class orm(orm_template):
for f in fields:
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]
if f in default:
@ -4614,9 +4686,13 @@ class orm(orm_template):
# force a clean recompute!
for parent_column in ['parent_left', 'parent_right']:
data.pop(parent_column, None)
for v in self._inherits:
del data[self._inherits[v]]
# Remove _inherits field's from data recursively, missing parents will
# be created by create() (so that copy() copy everything).
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
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):
if type(ids) in (int, long):
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),))
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):
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):
ret = []
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'])
return ret

View File

@ -65,7 +65,10 @@ class report(object):
if type =='html2html':
match = html_parents
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
while n.tag not in match:
n = n.getparent()

View File

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

View File

@ -68,6 +68,9 @@ rml2sxw = {
'para': 'p',
}
def get_date_length(date_format=DT_FORMAT):
return len((datetime.now()).strftime(date_format))
class _format(object):
def set_value(self, cr, uid, name, object, field, lang_obj):
self.object = object
@ -78,7 +81,7 @@ class _format(object):
class _float_format(float, _format):
def __init__(self,value):
super(_float_format, self).__init__()
self.val = value
self.val = value or 0.0
def __str__(self):
digits = 2
@ -86,17 +89,17 @@ class _float_format(float, _format):
digits = self._field.digits[1]
if hasattr(self, 'lang_obj'):
return self.lang_obj.format('%.' + str(digits) + 'f', self.name, True)
return self.val
return str(self.val)
class _int_format(int, _format):
def __init__(self,value):
super(_int_format, self).__init__()
self.val = value and str(value) or str(0)
self.val = value or 0
def __str__(self):
if hasattr(self,'lang_obj'):
return self.lang_obj.format('%.d', self.name, True)
return self.val
return str(self.val)
class _date_format(str, _format):
def __init__(self,value):
@ -106,7 +109,7 @@ class _date_format(str, _format):
def __str__(self):
if self.val:
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 self.val
@ -264,7 +267,7 @@ class rml_parse(object):
d = obj._field.digits[1] or DEFAULT_DIGITS
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:
formatLang(value) -> digits=2 (default)
@ -296,13 +299,19 @@ class rml_parse(object):
date_format = date_format + " " + self.lang_dict['time_format']
parse_format = DHM_FORMAT
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:
date = datetime(*value.timetuple()[:6])
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):
ret_lst = []

View File

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

View File

@ -254,6 +254,8 @@ class configmanager(object):
"osv_memory tables. This is a decimal value expressed in hours, "
"and the default is 1 hour.",
type="float")
group.add_option("--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)
# 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',
'list_db', 'xmlrpcs',
'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:

View File

@ -45,6 +45,7 @@ from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.Header import Header
from email.Utils import formatdate, COMMASPACE
from email import Utils
from email import Encoders
from itertools import islice, izip
from lxml import etree
@ -59,6 +60,7 @@ except ImportError:
html2text = None
import openerp.loglevels as loglevels
import openerp.pooler as pooler
from config import config
from cache import *
@ -280,15 +282,7 @@ email_re = re.compile(r"""
""", re.VERBOSE)
res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE)
priorities = {
'1': '1 (Highest)',
'2': '2 (High)',
'3': '3 (Normal)',
'4': '4 (Low)',
'5': '5 (Lowest)',
}
reference_re = re.compile("<.*-open(?:object|erp)-(\\d+).*@(.*)>", re.UNICODE)
def html2plaintext(html, body_id=None, encoding='utf-8'):
""" 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
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
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.
"""
return "<%s-openobject-%s@%s>" % (time.time(), openobject_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
return "<%s-openerp-%s@%s>" % (time.time(), res_id, socket.gethostname())
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.
@param email_from A string used to fill the `From` header, if falsy,
config['email_from'] is used instead. Also used for
the `Reply-To` header if `reply_to` is not provided
@param email_to a sequence of addresses to send the mail to.
: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,
config['email_from'] is used instead. Also used for
the `Reply-To` header if `reply_to` is not provided
: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']):
raise ValueError("Sending an email requires either providing a sender "
"address or having configured one")
# Send Email
try:
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)
email_from = ustr(email_from).encode('utf-8')
if not email_cc: email_cc = []
if not email_bcc: email_bcc = []
if not body: body = u''
email_body = ustr(body).encode('utf-8')
email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
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)
res = mail_server_pool.send_email(cr, uid or 1, email_msg, mail_server_id=None,
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)
except Exception:
_log.exception("tools.email_send failed to deliver email")
return False
finally:
cr.close()
return res
#----------------------------------------------------------
# SMS
@ -1089,10 +984,7 @@ def detect_server_timezone():
return 'UTC'
def get_server_timezone():
# timezone detection is safe in multithread, so lazy init is ok here
if (not config['timezone']):
config['timezone'] = detect_server_timezone()
return config['timezone']
return "UTC"
DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"

View File

@ -27,6 +27,16 @@ import openerp.netsvc as netsvc
import openerp.pooler as pooler
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'):
netsvc.Service.__init__(self, name)
self.wkf_on_create_cache={}
@ -35,12 +45,31 @@ class workflow_service(netsvc.Service):
self.wkf_on_create_cache[cr.dbname]={}
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)
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():
instance.update(cr, id, ident)
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))
res = cr.fetchall()
for (instance_id,) in res:
@ -49,10 +78,24 @@ class workflow_service(netsvc.Service):
instance.update(cr, instance_id, ident)
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)
instance.delete(cr, ident)
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)
self.wkf_on_create_cache.setdefault(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)
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
ident = (uid,res_type,res_id)
# 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
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):
"""
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)
#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))

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_password" $TextPostgreSQLPassword
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\service\OpenERPServerService.exe" -auto -install'

View File

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