[MERGE] trunk
bzr revid: al@openerp.com-20110924145258-16r9qi7hr3dip1jz
This commit is contained in:
commit
7a88d9058d
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -75,29 +75,29 @@
|
|||
-->
|
||||
|
||||
<record id="view_users_form_simple_modif" model="ir.ui.view">
|
||||
<field name="name">res.users.form.modif</field>
|
||||
<field name="name">res.users.preferences.form</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="type">form</field>
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Users">
|
||||
<field name="name"/>
|
||||
<field name="name" readonly="1"/>
|
||||
<newline/>
|
||||
<group colspan="2" col="2">
|
||||
<separator string="Preferences" colspan="2"/>
|
||||
<field name="view"/>
|
||||
<field name="context_lang"/>
|
||||
<field name="context_tz"/>
|
||||
<field name="menu_tips"/>
|
||||
<field name="view" readonly="0"/>
|
||||
<field name="context_lang" readonly="0"/>
|
||||
<field name="context_tz" readonly="0"/>
|
||||
<field name="menu_tips" readonly="0"/>
|
||||
</group>
|
||||
<group name="default_filters" colspan="2" col="2">
|
||||
<separator string="Default Filters" colspan="2"/>
|
||||
<field name="company_id" widget="selection"
|
||||
<field name="company_id" widget="selection" readonly="0"
|
||||
groups="base.group_multi_company" on_change="on_change_company_id(company_id)"/>
|
||||
</group>
|
||||
<separator string="Email Preferences" colspan="4"/>
|
||||
<field colspan="4" name="user_email" widget="email"/>
|
||||
<field colspan="4" name="signature"/>
|
||||
<field colspan="4" name="user_email" widget="email" readonly="0"/>
|
||||
<field colspan="4" name="signature" readonly="0"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -147,7 +147,7 @@
|
|||
<page string="Access Rights">
|
||||
<field nolabel="1" name="groups_id"/>
|
||||
</page>
|
||||
<page string="Companies" groups="base.group_multi_company">
|
||||
<page string="Allowed Companies" groups="base.group_multi_company">
|
||||
<field colspan="4" nolabel="1" name="company_ids" select="1"/>
|
||||
</page>
|
||||
</notebook>
|
||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: openobject-server\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
|
||||
"PO-Revision-Date: 2011-09-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
|
||||
|
|
|
@ -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
|
@ -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:
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
|
@ -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):
|
||||
|
|
|
@ -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 ' \
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -20,6 +20,5 @@
|
|||
##############################################################################
|
||||
import wizard_menu
|
||||
import wizard_screen
|
||||
import create_action
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import wizard
|
||||
import pooler
|
||||
import time
|
||||
|
||||
action_type = '''<?xml version="1.0"?>
|
||||
<form string="Select Action Type">
|
||||
<field name="type"/>
|
||||
</form>'''
|
||||
|
||||
action_type_fields = {
|
||||
'type': {'string':"Select Action Type",'type':'selection','required':True ,'selection':[('ir.actions.report.xml','Open Report')]},
|
||||
}
|
||||
|
||||
report_action = '''<?xml version="1.0"?>
|
||||
<form string="Select Report">
|
||||
<field name="report" colspan="4"/>
|
||||
</form>'''
|
||||
|
||||
report_action_fields = {
|
||||
'report': {'string':"Select Report",'type':'many2one','relation':'ir.actions.report.xml', 'required':True},
|
||||
}
|
||||
|
||||
class create_action(wizard.interface):
|
||||
|
||||
def _create_report_action(self, cr, uid, data, context={}):
|
||||
pool = pooler.get_pool(cr.dbname)
|
||||
|
||||
reports = pool.get('ir.actions.report.xml')
|
||||
form = data['form']
|
||||
|
||||
rpt = reports.browse(cr, uid, form['report'])
|
||||
|
||||
action = """action = {"type": "ir.actions.report.xml","model":"%s","report_name": "%s","ids": context["active_ids"]}""" % (rpt.model, rpt.report_name)
|
||||
|
||||
obj = pool.get('ir.actions.server')
|
||||
obj.write(cr, uid, data['ids'], {'code':action})
|
||||
|
||||
return {}
|
||||
|
||||
states = {
|
||||
'init': {
|
||||
'actions': [],
|
||||
'result': {'type':'form', 'arch':action_type,'fields':action_type_fields, 'state':[('step_1','Next'),('end','Close')]}
|
||||
},
|
||||
'step_1': {
|
||||
'actions': [],
|
||||
'result': {'type':'form', 'arch':report_action,'fields':report_action_fields, 'state':[('create','Create'),('end','Close')]}
|
||||
},
|
||||
'create': {
|
||||
'actions': [_create_report_action],
|
||||
'result': {'type':'state', 'state':'end'}
|
||||
},
|
||||
}
|
||||
create_action('server.action.create')
|
||||
|
||||
|
|
@ -21,12 +21,5 @@
|
|||
</field>
|
||||
</record>
|
||||
<act_window context="{'model_id': active_id}" id="act_menu_create" name="Create Menu" res_model="wizard.ir.model.menu.create" target="new" view_mode="form"/>
|
||||
<wizard
|
||||
id="wizard_server_action_create"
|
||||
model="ir.actions.server"
|
||||
name="server.action.create"
|
||||
string="Create Action"
|
||||
menu="False"
|
||||
/>
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -107,48 +107,59 @@ class module(osv.osv):
|
|||
view_obj = self.pool.get('ir.ui.view')
|
||||
report_obj = self.pool.get('ir.actions.report.xml')
|
||||
menu_obj = self.pool.get('ir.ui.menu')
|
||||
mlist = self.browse(cr, uid, ids, context=context)
|
||||
mnames = {}
|
||||
for m in mlist:
|
||||
# skip uninstalled modules below,
|
||||
# no data to find anyway
|
||||
if m.state in ('installed', 'to upgrade', 'to remove'):
|
||||
mnames[m.name] = m.id
|
||||
res[m.id] = {
|
||||
'menus_by_module':[],
|
||||
'reports_by_module':[],
|
||||
|
||||
dmodels = []
|
||||
if field_name is None or 'views_by_module' in field_name:
|
||||
dmodels.append('ir.ui.view')
|
||||
if field_name is None or 'reports_by_module' in field_name:
|
||||
dmodels.append('ir.actions.report.xml')
|
||||
if field_name is None or 'menus_by_module' in field_name:
|
||||
dmodels.append('ir.ui.menu')
|
||||
assert dmodels, "no models for %s" % field_name
|
||||
|
||||
for module_rec in self.browse(cr, uid, ids, context=context):
|
||||
res[module_rec.id] = {
|
||||
'menus_by_module': [],
|
||||
'reports_by_module': [],
|
||||
'views_by_module': []
|
||||
}
|
||||
|
||||
if not mnames:
|
||||
return res
|
||||
# Skip uninstalled modules below, no data to find anyway.
|
||||
if module_rec.state not in ('installed', 'to upgrade', 'to remove'):
|
||||
continue
|
||||
|
||||
view_id = model_data_obj.search(cr,uid,[('module','in', mnames.keys()),
|
||||
('model','in',('ir.ui.view','ir.actions.report.xml','ir.ui.menu'))])
|
||||
for data_id in model_data_obj.browse(cr,uid,view_id,context):
|
||||
# We use try except, because views or menus may not exist
|
||||
# then, search and group ir.model.data records
|
||||
imd_models = dict( [(m,[]) for m in dmodels])
|
||||
imd_ids = model_data_obj.search(cr,uid,[('module','=', module_rec.name),
|
||||
('model','in',tuple(dmodels))])
|
||||
|
||||
for imd_res in model_data_obj.read(cr, uid, imd_ids, ['model', 'res_id'], context=context):
|
||||
imd_models[imd_res['model']].append(imd_res['res_id'])
|
||||
|
||||
# For each one of the models, get the names of these ids.
|
||||
# We use try except, because views or menus may not exist.
|
||||
try:
|
||||
key = data_id.model
|
||||
res_mod_dic = res[mnames[data_id.module]]
|
||||
if key=='ir.ui.view':
|
||||
v = view_obj.browse(cr,uid,data_id.res_id)
|
||||
res_mod_dic = res[module_rec.id]
|
||||
for v in view_obj.browse(cr, uid, imd_models.get('ir.ui.view', []), context=context):
|
||||
aa = v.inherit_id and '* INHERIT ' or ''
|
||||
res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
|
||||
elif key=='ir.actions.report.xml':
|
||||
res_mod_dic['reports_by_module'].append(report_obj.browse(cr,uid,data_id.res_id).name)
|
||||
elif key=='ir.ui.menu':
|
||||
res_mod_dic['menus_by_module'].append(menu_obj.browse(cr,uid,data_id.res_id).complete_name)
|
||||
|
||||
for rx in report_obj.browse(cr, uid, imd_models.get('ir.actions.report.xml', []), context=context):
|
||||
res_mod_dic['reports_by_module'].append(rx.name)
|
||||
|
||||
for um in menu_obj.browse(cr, uid, imd_models.get('ir.ui.menu', []), context=context):
|
||||
res_mod_dic['menus_by_module'].append(um.complete_name)
|
||||
except KeyError, e:
|
||||
self.__logger.warning(
|
||||
'Data not found for reference %s[%s:%s.%s]', data_id.model,
|
||||
data_id.res_id, data_id.model, data_id.name, exc_info=True)
|
||||
pass
|
||||
'Data not found for items of %s', module_rec.name)
|
||||
except AttributeError, e:
|
||||
self.__logger.warning(
|
||||
'Data not found for items of %s %s', module_rec.name, str(e))
|
||||
except Exception, e:
|
||||
self.__logger.warning('Unknown error while browsing %s[%s]',
|
||||
data_id.model, data_id.res_id, exc_info=True)
|
||||
pass
|
||||
self.__logger.warning('Unknown error while fetching data of %s',
|
||||
module_rec.name, exc_info=True)
|
||||
for key, value in res.iteritems():
|
||||
for k, v in res[key].iteritems() :
|
||||
for k, v in res[key].iteritems():
|
||||
res[key][k] = "\n".join(sorted(v))
|
||||
return res
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -35,9 +35,12 @@ class base_language_import(osv.osv_memory):
|
|||
'name': fields.char('Language Name',size=64 , required=True),
|
||||
'code': fields.char('Code (eg:en__US)',size=5 , required=True),
|
||||
'data': fields.binary('File', required=True),
|
||||
'overwrite': fields.boolean('Overwrite Existing Terms',
|
||||
help="If you enable this option, existing translations (including custom ones) "
|
||||
"will be overwritten and replaced by those in this file"),
|
||||
}
|
||||
|
||||
def import_lang(self, cr, uid, ids, context):
|
||||
def import_lang(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Import Language
|
||||
@param cr: the current row, from the database cursor.
|
||||
|
@ -45,8 +48,11 @@ class base_language_import(osv.osv_memory):
|
|||
@param ids: the ID or list of IDs
|
||||
@param context: A standard dictionary
|
||||
"""
|
||||
|
||||
if context is None:
|
||||
context = {}
|
||||
import_data = self.browse(cr, uid, ids)[0]
|
||||
if import_data.overwrite:
|
||||
context.update(overwrite=True)
|
||||
fileobj = TemporaryFile('w+')
|
||||
fileobj.write(base64.decodestring(import_data.data))
|
||||
|
||||
|
@ -56,7 +62,7 @@ class base_language_import(osv.osv_memory):
|
|||
fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po'
|
||||
fileobj.seek(0)
|
||||
|
||||
tools.trans_load_data(cr, fileobj, fileformat, import_data.code, lang_name=import_data.name)
|
||||
tools.trans_load_data(cr, fileobj, fileformat, import_data.code, lang_name=import_data.name, context=context)
|
||||
tools.trans_update_res_ids(cr)
|
||||
fileobj.close()
|
||||
return {}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<field name="name" width="200"/>
|
||||
<field name="code"/>
|
||||
<field name="data" colspan="4"/>
|
||||
<field name="overwrite"/>
|
||||
</group>
|
||||
<group colspan="8" col="8">
|
||||
<separator string="" colspan="8"/>
|
||||
|
|
|
@ -28,6 +28,8 @@ import base64
|
|||
from tools.translate import _
|
||||
from osv import osv, fields
|
||||
|
||||
ADDONS_PATH = tools.config['addons_path'].split(",")[-1]
|
||||
|
||||
class base_module_import(osv.osv_memory):
|
||||
""" Import Module """
|
||||
|
||||
|
@ -37,7 +39,8 @@ class base_module_import(osv.osv_memory):
|
|||
|
||||
_columns = {
|
||||
'module_file': fields.binary('Module .ZIP file', required=True),
|
||||
'state':fields.selection([('init','init'),('done','done')], 'state', readonly=True),
|
||||
'state':fields.selection([('init','init'),('done','done')],
|
||||
'state', readonly=True),
|
||||
'module_name': fields.char('Module Name', size=128),
|
||||
}
|
||||
|
||||
|
@ -48,26 +51,30 @@ class base_module_import(osv.osv_memory):
|
|||
def importzip(self, cr, uid, ids, context):
|
||||
(data,) = self.browse(cr, uid, ids , context=context)
|
||||
module_data = data.module_file
|
||||
|
||||
val = base64.decodestring(module_data)
|
||||
zip_data = base64.decodestring(module_data)
|
||||
fp = StringIO()
|
||||
fp.write(val)
|
||||
fdata = zipfile.ZipFile(fp, 'r')
|
||||
fname = fdata.namelist()[0]
|
||||
module_name = os.path.split(fname)[0]
|
||||
|
||||
ad = tools.config['addons_path'].split(",")[-1]
|
||||
|
||||
fname = os.path.join(ad, module_name+'.zip')
|
||||
fp.write(zip_data)
|
||||
try:
|
||||
fp = file(fname, 'wb')
|
||||
fp.write(val)
|
||||
fp.close()
|
||||
except IOError:
|
||||
raise osv.except_osv(_('Error !'), _('Can not create the module file: %s !') % (fname,) )
|
||||
file_data = zipfile.ZipFile(fp, 'r')
|
||||
except zipfile.BadZipfile:
|
||||
raise osv.except_osv(_('Error !'), _('File is not a zip file!'))
|
||||
init_file_name = sorted(file_data.namelist())[0]
|
||||
module_name = os.path.split(init_file_name)[0]
|
||||
|
||||
self.pool.get('ir.module.module').update_list(cr, uid, {'module_name': module_name,})
|
||||
self.write(cr, uid, ids, {'state':'done', 'module_name': module_name}, context)
|
||||
file_path = os.path.join(ADDONS_PATH, '%s.zip' % module_name)
|
||||
try:
|
||||
zip_file = open(file_path, 'wb')
|
||||
except IOError:
|
||||
raise osv.except_osv(_('Error !'),
|
||||
_('Can not create the module file: %s !') % \
|
||||
(file_path,) )
|
||||
zip_file.write(zip_data)
|
||||
zip_file.close()
|
||||
|
||||
self.pool.get('ir.module.module').update_list(cr, uid,
|
||||
{'module_name': module_name,})
|
||||
self.write(cr, uid, ids, {'state':'done', 'module_name': module_name},
|
||||
context)
|
||||
return False
|
||||
|
||||
def action_module_open(self, cr, uid, ids, context):
|
||||
|
@ -84,4 +91,4 @@ class base_module_import(osv.osv_memory):
|
|||
base_module_import()
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -12,9 +12,10 @@
|
|||
<group colspan="3" col="1">
|
||||
<field name="config_logo" widget="image" width="220" height="130" nolabel="1" colspan="1"/>
|
||||
<newline/>
|
||||
<label width="220" string="This wizard helps you add a new language to you OpenERP system. After loading a new language it becomes available as default interface language for users and partners."/>
|
||||
<label width="220" string='This wizard helps you to import a new module to your OpenERP system.
|
||||
After importing a new module you can install it by clicking on the button "Install" from the form view.'/>
|
||||
<label width="220"/>
|
||||
<label width="220" string="Please be patient, this operation may take a few minutes (depending on the number of modules currently installed)..."/>
|
||||
<label width="220" string="Please be patient, this operation may take a few minutes..."/>
|
||||
<field name="state" invisible="1"/>
|
||||
</group>
|
||||
<separator orientation="vertical" rowspan="5"/>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
help="Parameters that are used by all resources."
|
||||
domain="[('res_id','=',False)]"/>
|
||||
<separator orientation="vertical"/>
|
||||
<field name="fields_id" />
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</search>
|
||||
|
|
|
@ -143,6 +143,9 @@ class res_company(osv.osv):
|
|||
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
|
||||
'company_registry': fields.char('Company Registry', size=64),
|
||||
}
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', 'The company name must be unique !')
|
||||
]
|
||||
|
||||
def _search(self, cr, uid, args, offset=0, limit=None, order=None,
|
||||
context=None, count=False, access_rights_uid=None):
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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] != '%':
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 user’s 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:
|
|
@ -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"
|
|
@ -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"/>
|
||||
|
|
|
@ -42,28 +42,32 @@
|
|||
<field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="res_widget_user_rule">
|
||||
<field name="name">res.widget.user rule</field>
|
||||
<field name="model_id" ref="model_res_widget_user"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('user_id','=',user.id),('user_id','=',False)]</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<record model="ir.rule" id="res_partner_rule">
|
||||
<field name="name">res.partner company</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="global" eval="True"/>
|
||||
<!-- Show partners from ancestors and descendants companies (or company-less), this is usually a better
|
||||
default for multicompany setups. -->
|
||||
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
||||
</record>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="ir.rule" id="multi_company_default_rule">
|
||||
<field name="name">Multi_company_default company</field>
|
||||
<field name="model_id" ref="model_multi_company_default"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="res_widget_user_rule">
|
||||
<field name="name">res.widget.user rule</field>
|
||||
<field name="model_id" ref="model_res_widget_user"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|', ('user_id','=',user.id),('user_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="res_partner_rule">
|
||||
<field name="name">res.partner company</field>
|
||||
<field name="model_id" ref="model_res_partner"/>
|
||||
<field name="global" eval="True"/>
|
||||
<!-- Show partners from ancestors and descendants companies (or company-less), this is usually a better
|
||||
default for multicompany setups. -->
|
||||
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="multi_company_default_rule">
|
||||
<field name="name">Multi_company_default company</field>
|
||||
<field name="model_id" ref="model_multi_company_default"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
"access_res_country_state_group_user","res_country_state group_user","model_res_country_state","group_partner_manager",1,1,1,1
|
||||
"access_res_currency_group_all","res_currency group_all","model_res_currency",,1,0,0,0
|
||||
"access_res_currency_rate_group_all","res_currency_rate group_all","model_res_currency_rate",,1,0,0,0
|
||||
"access_res_currency_rate_type_group_all","res_currency_rate_type group_all","model_res_currency_rate_type",,1,0,0,0
|
||||
"access_res_currency_group_system","res_currency group_system","model_res_currency","group_system",1,1,1,1
|
||||
"access_res_currency_rate_group_system","res_currency_rate group_system","model_res_currency_rate","group_system",1,1,1,1
|
||||
"access_res_groups_group_erp_manager","res_groups group_erp_manager","model_res_groups","group_erp_manager",1,1,1,1
|
||||
|
@ -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
|
||||
|
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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!" % \
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'
|
||||
|
|
3
setup.py
3
setup.py
|
@ -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'],
|
||||
|
|
Loading…
Reference in New Issue