[MERGE] merged trunk.
bzr revid: vmt@openerp.com-20120813142443-73d6zm10bqhm17mp
This commit is contained in:
@@ -56,6 +56,7 @@
|
||||
'module/wizard/base_module_configuration_view.xml',
|
||||
'module/wizard/base_export_language_view.xml',
|
||||
'module/wizard/base_update_translations_view.xml',
|
||||
'module/wizard/base_module_immediate_install.xml',
|
||||
'res/res_company_view.xml',
|
||||
'res/res_request_view.xml',
|
||||
'res/res_lang_view.xml',
|
||||
@@ -79,6 +80,7 @@
|
||||
'demo_xml': [
|
||||
'base_demo.xml',
|
||||
'res/res_partner_demo.xml',
|
||||
'res/res_partner_demo.yml',
|
||||
'res/res_widget_demo.xml',
|
||||
],
|
||||
'test': [
|
||||
|
||||
@@ -1060,6 +1060,13 @@
|
||||
<field name="company_id" eval="None"/>
|
||||
<field name="customer" eval="False"/>
|
||||
<field name="is_company" eval="True"/>
|
||||
<field name="street">45, Rue du Palais</field>
|
||||
<field name="city">Paris</field>
|
||||
<field name="zip">75016</field>
|
||||
<field ref="base.fr" name="country_id"/>
|
||||
<field name="phone">+33 1 49 51 23 94</field>
|
||||
<field name="email">info@yourcompany.com</field>
|
||||
<field name="website">www.yourcompany.com</field>
|
||||
</record>
|
||||
|
||||
<!-- Currencies -->
|
||||
@@ -1081,8 +1088,9 @@
|
||||
<record id="main_company" model="res.company">
|
||||
<field name="name">Your Company</field>
|
||||
<field name="partner_id" ref="main_partner"/>
|
||||
<field name="rml_footer1">Web: www.companyname.com - Tel: +1-212-555-12345</field>
|
||||
<field name="rml_footer2">IBAN: XX12 3456 7890 1234 5678 - SWIFT: SWIFTCODE - VAT: Company vat number</field>
|
||||
<field name="rml_header1">Your Company Slogan</field>
|
||||
<field name="rml_footer1">Web: http://www.yourcompany.com - Tel: +33 1 49 51 23 94</field>
|
||||
<field name="rml_footer2">Bank Accounts... to be configured in menu Settings > Accounting/Invoicing.</field>
|
||||
<field name="currency_id" ref="base.EUR"/>
|
||||
</record>
|
||||
|
||||
@@ -1623,7 +1631,7 @@
|
||||
</record>
|
||||
|
||||
<record id="res_bank_1" model="res.bank">
|
||||
<field name="name">Reserve</field>
|
||||
<field name="name">International Bank</field>
|
||||
</record>
|
||||
|
||||
<record id="CRC" model="res.currency">
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="demo_address" model="res.partner">
|
||||
<field name="name">Fabien D'souza</field>
|
||||
<field name="street">Chaussee de Namur</field>
|
||||
<field name="zip">1367</field>
|
||||
<field name="city">Gerompont</field>
|
||||
<field name="phone">(+32).81.81.37.00</field>
|
||||
<field name="type">default</field>
|
||||
<field model="res.country" name="country_id" ref="be"/>
|
||||
<!-- Company ID will be set later -->
|
||||
<field name="company_id" eval="None"/>
|
||||
</record>
|
||||
|
||||
<record id="user_demo" model="res.users">
|
||||
<field name="login">demo</field>
|
||||
<field name="password">demo</field>
|
||||
|
||||
@@ -7,14 +7,15 @@ msgstr ""
|
||||
"Project-Id-Version: OpenERP Server 5.0.4\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-02-08 00:44+0000\n"
|
||||
"PO-Revision-Date: 2012-02-16 22:06+0000\n"
|
||||
"Last-Translator: Davide Corio - agilebg.com <davide.corio@agilebg.com>\n"
|
||||
"PO-Revision-Date: 2012-08-10 18:42+0000\n"
|
||||
"Last-Translator: Leonardo Pistone - Agile BG - Domsense "
|
||||
"<leonardo.pistone@domsense.com>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-07-19 04:40+0000\n"
|
||||
"X-Generator: Launchpad (build 15637)\n"
|
||||
"X-Launchpad-Export-Date: 2012-08-11 05:45+0000\n"
|
||||
"X-Generator: Launchpad (build 15780)\n"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.sh
|
||||
@@ -849,6 +850,10 @@ msgid ""
|
||||
"Launch Manually Once: after hacing been launched manually, it sets "
|
||||
"automatically to Done."
|
||||
msgstr ""
|
||||
"Manuale: Esegui manualmente\n"
|
||||
"Automatico: Viene eseguito ogni volta che il sistema viene riconfigurato \n"
|
||||
"Esegui manualmente una volta sola: una volta lanciata l'esecuzione, lo stato "
|
||||
"viene impostato autmaticamente su \"Eseguito\""
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
@@ -1593,7 +1598,7 @@ msgstr "Login"
|
||||
#: model:ir.actions.act_window,name:base.action_wizard_update_translations
|
||||
#: model:ir.ui.menu,name:base.menu_wizard_update_translations
|
||||
msgid "Synchronize Terms"
|
||||
msgstr ""
|
||||
msgstr "Sincronizza Termini"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.actions.server:0
|
||||
@@ -2980,7 +2985,7 @@ msgstr "Armenia"
|
||||
#: model:ir.actions.act_window,name:base.ir_property_form
|
||||
#: model:ir.ui.menu,name:base.menu_ir_property_form_all
|
||||
msgid "Configuration Parameters"
|
||||
msgstr "Parametri Configurazione"
|
||||
msgstr "Parametri di configurazione"
|
||||
|
||||
#. module: base
|
||||
#: constraint:ir.cron:0
|
||||
@@ -3077,7 +3082,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_wiki_quality_manual
|
||||
msgid "Wiki: Quality Manual"
|
||||
msgstr ""
|
||||
msgstr "Wiki: Manuale della Qualità"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.actions.act_window.view,view_mode:0
|
||||
@@ -3105,7 +3110,7 @@ msgstr "Settore Risorse Umane"
|
||||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.menu_dashboard_admin
|
||||
msgid "Administration Dashboard"
|
||||
msgstr ""
|
||||
msgstr "Dashboard di Amministrazione"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/orm.py:4408
|
||||
@@ -3748,7 +3753,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: model:ir.module.category,name:base.module_category_human_resources
|
||||
msgid "Human Resources"
|
||||
msgstr ""
|
||||
msgstr "Risorse Umane"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.action_country
|
||||
@@ -3808,7 +3813,7 @@ msgstr "Partita IVA"
|
||||
#. module: base
|
||||
#: field:res.users,new_password:0
|
||||
msgid "Set password"
|
||||
msgstr ""
|
||||
msgstr "Nuova password"
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
@@ -4737,7 +4742,7 @@ msgstr "Kenia"
|
||||
#: model:ir.actions.act_window,name:base.action_translation
|
||||
#: model:ir.ui.menu,name:base.menu_action_translation
|
||||
msgid "Translated Terms"
|
||||
msgstr ""
|
||||
msgstr "Termini Tradotti"
|
||||
|
||||
#. module: base
|
||||
#: view:res.partner.event:0
|
||||
@@ -6368,7 +6373,7 @@ msgstr "Menu"
|
||||
#. module: base
|
||||
#: selection:ir.actions.todo,type:0
|
||||
msgid "Launch Manually Once"
|
||||
msgstr ""
|
||||
msgstr "Esegui manualmente una volta sola"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.category,name:base.module_category_hidden
|
||||
@@ -6741,6 +6746,8 @@ msgid ""
|
||||
"password, otherwise leave empty. After a change of password, the user has to "
|
||||
"login again."
|
||||
msgstr ""
|
||||
"Per modificare la password scrivila qui, altrimenti lascia il campo vuoto. "
|
||||
"Dopo aver cambiato la password, l'utente deve fare nuovamente login."
|
||||
|
||||
#. module: base
|
||||
#: view:publisher_warranty.contract:0
|
||||
@@ -6929,7 +6936,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: view:ir.property:0
|
||||
msgid "Parameters that are used by all resources."
|
||||
msgstr ""
|
||||
msgstr "Parametri utilizzati da tutte le risorse"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.mz
|
||||
@@ -9394,7 +9401,7 @@ msgstr ""
|
||||
#: model:ir.model,name:base.model_ir_model
|
||||
#: model:ir.ui.menu,name:base.ir_model_model_menu
|
||||
msgid "Models"
|
||||
msgstr ""
|
||||
msgstr "Modelli"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_cron.py:292
|
||||
@@ -9405,7 +9412,7 @@ msgstr ""
|
||||
#. module: base
|
||||
#: selection:ir.actions.todo,type:0
|
||||
msgid "Launch Manually"
|
||||
msgstr ""
|
||||
msgstr "Esegui manualmente"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.be
|
||||
@@ -10753,7 +10760,7 @@ msgstr "Nazione"
|
||||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.next_id_5
|
||||
msgid "Sequences & Identifiers"
|
||||
msgstr ""
|
||||
msgstr "Sequenze e Identificatori"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_l10n_th
|
||||
@@ -12636,7 +12643,7 @@ msgstr "ir.values"
|
||||
#. module: base
|
||||
#: model:res.groups,name:base.group_no_one
|
||||
msgid "Technical Features"
|
||||
msgstr ""
|
||||
msgstr "Funzionalità tecniche"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
@@ -12656,7 +12663,7 @@ msgstr ""
|
||||
#: view:ir.model.data:0
|
||||
#: model:ir.ui.menu,name:base.ir_model_data_menu
|
||||
msgid "External Identifiers"
|
||||
msgstr ""
|
||||
msgstr "Identificatori Esterni"
|
||||
|
||||
#. module: base
|
||||
#: model:res.groups,name:base.group_sale_salesman
|
||||
@@ -12950,6 +12957,7 @@ msgid ""
|
||||
"Please define at least one SMTP server, or provide the SMTP parameters "
|
||||
"explicitly."
|
||||
msgstr ""
|
||||
"Definire almeno un server SMTP, o fornire esplicitamente i parametri SMTP"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.attachment:0
|
||||
@@ -12993,7 +13001,7 @@ msgstr "Gabon"
|
||||
#. module: base
|
||||
#: model:res.groups,name:base.group_multi_company
|
||||
msgid "Multi Companies"
|
||||
msgstr ""
|
||||
msgstr "Multi-azienda"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.model:0
|
||||
@@ -13166,7 +13174,7 @@ msgstr "Varie"
|
||||
#: view:ir.mail_server:0
|
||||
#: model:ir.ui.menu,name:base.menu_mail_servers
|
||||
msgid "Outgoing Mail Servers"
|
||||
msgstr ""
|
||||
msgstr "Server della Posta in Uscita"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.cn
|
||||
|
||||
@@ -7,14 +7,14 @@ msgstr ""
|
||||
"Project-Id-Version: OpenERP Server 5.0.0\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-02-08 00:44+0000\n"
|
||||
"PO-Revision-Date: 2012-02-19 13:01+0000\n"
|
||||
"PO-Revision-Date: 2012-08-09 06:15+0000\n"
|
||||
"Last-Translator: Erwin <Unknown>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-07-19 04:38+0000\n"
|
||||
"X-Generator: Launchpad (build 15637)\n"
|
||||
"X-Launchpad-Export-Date: 2012-08-10 04:58+0000\n"
|
||||
"X-Generator: Launchpad (build 15761)\n"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.sh
|
||||
@@ -1443,7 +1443,7 @@ msgstr ""
|
||||
"U kunt de navolgende extra data toevoegen aan een verkooporder:\n"
|
||||
"* Aanvraagdatum\n"
|
||||
"* Bevestigde datum\n"
|
||||
"* Effectieve datum\n"
|
||||
"* Boek datum\n"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_account_sequence
|
||||
@@ -8110,7 +8110,7 @@ msgstr "Kenmerk"
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_analytic
|
||||
msgid "Analytic Accounting"
|
||||
msgstr "Kostenplaats boekhouding"
|
||||
msgstr "Kostenplaatsen"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.actions.report.xml:0
|
||||
|
||||
@@ -223,11 +223,11 @@ class act_window(osv.osv):
|
||||
help="Model name of the object to open in the view window"),
|
||||
'src_model': fields.char('Source Model', size=64,
|
||||
help="Optional model name of the objects on which this action should be visible"),
|
||||
'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline')], 'Target Window'),
|
||||
'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
|
||||
help="View type: set to 'tree' for a hierarchical tree view, or 'form' for other views"),
|
||||
'target': fields.selection([('current','Current Window'),('new','New Window'),('inline','Inline Edit'),('inlineview','Inline View')], 'Target Window'),
|
||||
'view_mode': fields.char('View Mode', size=250, required=True,
|
||||
help="Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)"),
|
||||
'view_type': fields.selection((('tree','Tree'),('form','Form')), string='View Type', required=True,
|
||||
help="View type: Tree type to use for the tree view, set to 'tree' for a hierarchical tree view, or 'form' for a regular list view"),
|
||||
'usage': fields.char('Action Usage', size=32,
|
||||
help="Used to filter menu and home actions from the user form."),
|
||||
'view_ids': fields.one2many('ir.actions.act_window.view', 'act_window_id', 'Views'),
|
||||
|
||||
@@ -180,8 +180,8 @@ class module(osv.osv):
|
||||
_columns = {
|
||||
'name': fields.char("Technical Name", size=128, readonly=True, required=True, select=True),
|
||||
'category_id': fields.many2one('ir.module.category', 'Category', readonly=True, select=True),
|
||||
'shortdesc': fields.char('Module Name', size=256, readonly=True, translate=True),
|
||||
'summary': fields.char('Summary', size=256, readonly=True, translate=True),
|
||||
'shortdesc': fields.char('Module Name', size=64, readonly=True, translate=True),
|
||||
'summary': fields.char('Summary', size=64, readonly=True, translate=True),
|
||||
'description': fields.text("Description", readonly=True, translate=True),
|
||||
'author': fields.char("Author", size=128, readonly=True),
|
||||
'maintainer': fields.char('Maintainer', size=128, readonly=True),
|
||||
@@ -358,22 +358,7 @@ class module(osv.osv):
|
||||
:returns: next res.config item to execute
|
||||
:rtype: dict[str, object]
|
||||
"""
|
||||
self.button_install(cr, uid, ids, context=context)
|
||||
cr.commit()
|
||||
_, pool = pooler.restart_pool(cr.dbname, update_module=True)
|
||||
|
||||
config = pool.get('res.config').next(cr, uid, [], context=context) or {}
|
||||
if config.get('type') not in ('ir.actions.reload', 'ir.actions.act_window_close'):
|
||||
return config
|
||||
|
||||
# reload the client; open the first available root menu
|
||||
menu_obj = self.pool.get('ir.ui.menu')
|
||||
menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
'params': {'menu_id': menu_ids and menu_ids[0] or False},
|
||||
}
|
||||
return self._button_immediate_function(cr, uid, ids, self.button_install, context=context)
|
||||
|
||||
def button_install_cancel(self, cr, uid, ids, context=None):
|
||||
self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
|
||||
@@ -418,8 +403,35 @@ class module(osv.osv):
|
||||
known_dep_ids, exclude_states,context))
|
||||
return list(known_dep_ids)
|
||||
|
||||
def _button_immediate_function(self, cr, uid, ids, function, context=None):
|
||||
function(cr, uid, ids, context=context)
|
||||
|
||||
cr.commit()
|
||||
_, pool = pooler.restart_pool(cr.dbname, update_module=True)
|
||||
|
||||
config = pool.get('res.config').next(cr, uid, [], context=context) or {}
|
||||
if config.get('type') not in ('ir.actions.reload', 'ir.actions.act_window_close'):
|
||||
return config
|
||||
|
||||
# reload the client; open the first available root menu
|
||||
menu_obj = self.pool.get('ir.ui.menu')
|
||||
menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
|
||||
|
||||
return {
|
||||
'type' : 'ir.actions.client',
|
||||
'tag' : 'reload',
|
||||
'params' : {'menu_id' : menu_ids and menu_ids[0] or False}
|
||||
}
|
||||
|
||||
def button_immediate_uninstall(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Uninstall the selected module(s) immediately and fully,
|
||||
returns the next res.config action to execute
|
||||
"""
|
||||
return self._button_immediate_function(cr, uid, ids, self.button_uninstall, context=context)
|
||||
|
||||
def button_uninstall(self, cr, uid, ids, context=None):
|
||||
if any(m.name == 'base' for m in self.browse(cr, uid, ids)):
|
||||
if any(m.name == 'base' for m in self.browse(cr, uid, ids, context=context)):
|
||||
raise orm.except_orm(_('Error'), _("The `base` module cannot be uninstalled"))
|
||||
dep_ids = self.downstream_dependencies(cr, uid, ids, context=context)
|
||||
self.write(cr, uid, ids + dep_ids, {'state': 'to remove'})
|
||||
@@ -429,6 +441,13 @@ class module(osv.osv):
|
||||
self.write(cr, uid, ids, {'state': 'installed'})
|
||||
return True
|
||||
|
||||
def button_immediate_upgrade(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
Upgrade the selected module(s) immediately and fully,
|
||||
return the next res.config action to execute
|
||||
"""
|
||||
return self._button_immediate_function(cr, uid, ids, self.button_upgrade, context=context)
|
||||
|
||||
def button_upgrade(self, cr, uid, ids, context=None):
|
||||
depobj = self.pool.get('ir.module.module.dependency')
|
||||
todo = self.browse(cr, uid, ids, context=context)
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
<h1><field name="shortdesc"/></h1>
|
||||
<h2 class="oe_fade"><field name="summary"/></h2>
|
||||
<button name="button_immediate_install" states="uninstalled" string="Install" type="object" class="oe_highlight"/>
|
||||
<button name="button_upgrade" states="installed" string="Upgrade" type="object" class="oe_highlight"/>
|
||||
<button name="button_uninstall" states="installed" string="Uninstall" type="object"
|
||||
<button name="button_immediate_upgrade" states="installed" string="Upgrade" type="object" class="oe_highlight"/>
|
||||
<button name="button_immediate_uninstall" states="installed" string="Uninstall" type="object"
|
||||
confirm="Do you confirm the uninstallation of this module? This will permanently erase all data currently stored by the module!"/>
|
||||
<button name="button_uninstall_cancel" states="to remove" string="Cancel Uninstall" type="object"/>
|
||||
<button name="button_upgrade_cancel" states="to upgrade" string="Cancel Upgrade" type="object"/>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<form string="Import Translation" version="7.0">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="code" string="Code" placeholder="en_US"/>
|
||||
<field name="code" string="Code" placeholder="e.g. en_US"/>
|
||||
<field name="data"/>
|
||||
<field name="overwrite"/>
|
||||
</group>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="action_server_module_immediate_install" model="ir.actions.server">
|
||||
<field name="name">Module Immediate Install</field>
|
||||
<field name="condition">True</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="model_id" ref="model_ir_module_module" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">self.button_immediate_install(cr, uid, context.get('active_ids', []), context=context)</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.values" id="action_module_immediate_install">
|
||||
<field name="name">action_module_immediate_install</field>
|
||||
<field name="action_id" ref="action_server_module_immediate_install" />
|
||||
<field name="value" eval="'ir.actions.server,' + str(ref('action_server_module_immediate_install'))" />
|
||||
<field name="key">action</field>
|
||||
<field name="model_id" ref="model_ir_module_module" />
|
||||
<field name="model">ir.module.module</field>
|
||||
<field name="key2">client_action_multi</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -105,8 +105,8 @@
|
||||
<field name="zip" class="oe_inline" placeholder="ZIP"/>
|
||||
<field name="city" class="oe_inline" placeholder="City"/>
|
||||
</div>
|
||||
<field name="state_id" placeholder="State"/>
|
||||
<field name="country_id" placeholder="Country"/>
|
||||
<field name="state_id" placeholder="State" options='{"no_open": true}'/>
|
||||
<field name="country_id" placeholder="Country" options='{"no_open": true}'/>
|
||||
</div>
|
||||
</group>
|
||||
<group name="bank" string="Information About the Bank">
|
||||
|
||||
@@ -114,7 +114,7 @@ class res_company(osv.osv):
|
||||
'rml_header': fields.text('RML Header', required=True),
|
||||
'rml_header2': fields.text('RML Internal Header', required=True),
|
||||
'rml_header3': fields.text('RML Internal Header for Landscape Reports', required=True),
|
||||
'logo': fields.related('partner_id', 'photo', string="Logo", type="binary"),
|
||||
'logo': fields.related('partner_id', 'image', string="Logo", type="binary"),
|
||||
'currency_id': fields.many2one('res.currency', 'Currency', required=True),
|
||||
'currency_ids': fields.one2many('res.currency', 'company_id', 'Currency'),
|
||||
'user_ids': fields.many2many('res.users', 'res_company_users_rel', 'cid', 'user_id', 'Accepted Users'),
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</h1>
|
||||
<label for="rml_header1" class="oe_edit_only"/>
|
||||
<div>
|
||||
<field name="rml_header1" placeholder="Global Business Solutions"/>
|
||||
<field name="rml_header1" placeholder="e.g. Global Business Solutions"/>
|
||||
</div>
|
||||
</div>
|
||||
<group col="4">
|
||||
@@ -50,10 +50,10 @@
|
||||
<field name="street2"/>
|
||||
<div>
|
||||
<field name="city" placeholder="City" style="width: 40%%"/>
|
||||
<field name="state_id" class="oe_no_button" placeholder="State" style="width: 24%%"/>
|
||||
<field name="state_id" class="oe_no_button" placeholder="State" style="width: 24%%" options='{"no_open": true}'/>
|
||||
<field name="zip" placeholder="ZIP" style="width: 34%%"/>
|
||||
</div>
|
||||
<field name="country_id" placeholder="Country" class="oe_no_button"/>
|
||||
<field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": true}'/>
|
||||
</div>
|
||||
<field name="website" widget="url" placeholder="e.g. www.openerp.com"/>
|
||||
</group>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="country_id"/>
|
||||
<field name="country_id" options='{"no_open": true}'/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -133,6 +133,15 @@ class res_partner(osv.osv):
|
||||
res[partner.id] =self._display_address(cr, uid, partner, context=context)
|
||||
return res
|
||||
|
||||
def _get_image(self, cr, uid, ids, name, args, context=None):
|
||||
result = dict.fromkeys(ids, False)
|
||||
for obj in self.browse(cr, uid, ids, context=context):
|
||||
result[obj.id] = tools.image_get_resized_images(obj.image)
|
||||
return result
|
||||
|
||||
def _set_image(self, cr, uid, id, name, value, args, context=None):
|
||||
return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
|
||||
|
||||
_order = "name"
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=128, required=True, select=True),
|
||||
@@ -174,7 +183,26 @@ class res_partner(osv.osv):
|
||||
'birthdate': fields.char('Birthdate', size=64),
|
||||
'is_company': fields.boolean('Company', help="Check if the contact is a company, otherwise it is a person"),
|
||||
'use_parent_address': fields.boolean('Use Company Address', help="Select this if you want to set company's address information for this contact"),
|
||||
'photo': fields.binary('Photo'),
|
||||
'image': fields.binary("Image",
|
||||
help="This field holds the image used as avatar for the "\
|
||||
"partner. The image is base64 encoded, and PIL-supported. "\
|
||||
"It is limited to a 1024x1024 px image."),
|
||||
'image_medium': fields.function(_get_image, fnct_inv=_set_image,
|
||||
string="Medium-sized image", type="binary", multi="_get_image",
|
||||
store = {
|
||||
'res.partner': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
|
||||
},
|
||||
help="Medium-sized image of the partner. It is automatically "\
|
||||
"resized as a 180x180 px image, with aspect ratio preserved. "\
|
||||
"Use this field in form views or some kanban views."),
|
||||
'image_small': fields.function(_get_image, fnct_inv=_set_image,
|
||||
string="Small-sized image", type="binary", multi="_get_image",
|
||||
store = {
|
||||
'res.partner': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
|
||||
},
|
||||
help="Small-sized image of the partner. It is automatically "\
|
||||
"resized as a 50x50 px image, with aspect ratio preserved. "\
|
||||
"Use this field anywhere a small image is required."),
|
||||
'company_id': fields.many2one('res.company', 'Company', select=1),
|
||||
'color': fields.integer('Color Index'),
|
||||
'contact_address': fields.function(_address_display, type='char', string='Complete Address'),
|
||||
@@ -187,12 +215,12 @@ class res_partner(osv.osv):
|
||||
return [context['category_id']]
|
||||
return False
|
||||
|
||||
def _get_photo(self, cr, uid, is_company, context=None):
|
||||
def _get_default_image(self, cr, uid, is_company, context=None):
|
||||
if is_company:
|
||||
path = os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'company_icon.png')
|
||||
image_path = os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'company_icon.png')
|
||||
else:
|
||||
path = os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'photo.png')
|
||||
return open(path, 'rb').read().encode('base64')
|
||||
image_path = os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'photo.png')
|
||||
return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
|
||||
|
||||
_defaults = {
|
||||
'active': True,
|
||||
@@ -203,7 +231,9 @@ class res_partner(osv.osv):
|
||||
'is_company': False,
|
||||
'type': 'default',
|
||||
'use_parent_address': True,
|
||||
'photo': lambda self, cr, uid, context: self._get_photo(cr, uid, False, context),
|
||||
'image': lambda self, cr, uid, context: self._get_default_image(cr, uid, False, context),
|
||||
'image_small': lambda self, cr, uid, context: self._get_default_image(cr, uid, False, context),
|
||||
'image_medium': lambda self, cr, uid, context: self._get_default_image(cr, uid, False, context),
|
||||
}
|
||||
|
||||
def copy(self, cr, uid, id, default=None, context=None):
|
||||
@@ -214,8 +244,9 @@ class res_partner(osv.osv):
|
||||
return super(res_partner, self).copy(cr, uid, id, default, context)
|
||||
|
||||
def onchange_type(self, cr, uid, ids, is_company, context=None):
|
||||
value = {'title': False,
|
||||
'photo': self._get_photo(cr, uid, is_company, context)}
|
||||
# get value as for an onchange on the image
|
||||
value = tools.image_get_resized_images(self._get_default_image(cr, uid, is_company, context), return_big=True)
|
||||
value['title'] = False
|
||||
if is_company:
|
||||
value['parent_id'] = False
|
||||
domain = {'title': [('domain', '=', 'partner')]}
|
||||
@@ -280,8 +311,9 @@ class res_partner(osv.osv):
|
||||
domain_siblings = [('parent_id', '=', vals['parent_id']), ('use_parent_address', '=', True)]
|
||||
update_ids = [vals['parent_id']] + self.search(cr, uid, domain_siblings, context=context)
|
||||
self.update_address(cr, uid, update_ids, vals, context)
|
||||
if 'photo' not in vals :
|
||||
vals['photo'] = self._get_photo(cr, uid, vals.get('is_company', False) or context.get('default_is_company'), context)
|
||||
if 'image' not in vals :
|
||||
image_value = self._get_default_image(cr, uid, vals.get('is_company', False) or context.get('default_is_company'), context)
|
||||
vals.update(tools.image_get_resized_images(image_value, return_big=True))
|
||||
return super(res_partner,self).create(cr, uid, vals, context=context)
|
||||
|
||||
def update_address(self, cr, uid, ids, vals, context=None):
|
||||
@@ -329,12 +361,13 @@ class res_partner(osv.osv):
|
||||
email = contact_regex_res[0][1]
|
||||
rec_id = self.create(cr, uid, {self._rec_name: name, 'email': email}, context);
|
||||
return self.name_get(cr, uid, [rec_id], context)[0]
|
||||
elif email_regex:
|
||||
elif email_regex_res:
|
||||
email = '%s' % (email_regex_res[0])
|
||||
rec_id = self.create(cr, uid, {self._rec_name: email, 'email': email}, context);
|
||||
return self.name_get(cr, uid, [rec_id], context)[0]
|
||||
else:
|
||||
return super(res_partner, self).create(cr, uid, name, context)
|
||||
rec_id = super(res_partner, self).create(cr, uid, {self._rec_name: name}, context)
|
||||
return self.name_get(cr, uid, [rec_id], context)[0]
|
||||
|
||||
def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
|
||||
if not args:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,261 @@
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_1}:
|
||||
name: Tang Tsui
|
||||
parent_id: base.res_partner_1
|
||||
use_parent_address: True
|
||||
function: Service Manager
|
||||
email: tang@asustek.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_2}:
|
||||
name: Joseph Walters
|
||||
parent_id: base.res_partner_1
|
||||
use_parent_address: True
|
||||
function: Store Manager
|
||||
email: joseph.walters@asustek.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_3}:
|
||||
name: Thomas Passot
|
||||
parent_id: base.res_partner_2
|
||||
use_parent_address: True
|
||||
function: Functional Consultant
|
||||
email: p.thomas@agrolait.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_4}:
|
||||
name: Michel Fletcher
|
||||
parent_id: base.res_partner_2
|
||||
use_parent_address: True
|
||||
function: Analyst
|
||||
email: m.fletcher@agrolait.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_5}:
|
||||
name: Chao Wang
|
||||
parent_id: base.res_partner_3
|
||||
use_parent_address: True
|
||||
function: Marketing Manager
|
||||
email: chao_wang@chinaexport.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_6}:
|
||||
name: Zhi Ch'ang
|
||||
parent_id: base.res_partner_3
|
||||
use_parent_address: True
|
||||
function: Supervisor
|
||||
email: zhi_chang@chinaexport.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_7}:
|
||||
name: Richard Ellis
|
||||
parent_id: base.res_partner_4
|
||||
use_parent_address: True
|
||||
function: Production Supervisor
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_8}:
|
||||
name: Paul Williams
|
||||
parent_id: base.res_partner_4
|
||||
use_parent_address: True
|
||||
function: Line Mechanic
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_9}:
|
||||
name: Brian Williams
|
||||
parent_id: base.res_partner_4
|
||||
use_parent_address: True
|
||||
function: Computer Technician
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_10}:
|
||||
name: David Simpson
|
||||
parent_id: base.res_partner_5
|
||||
use_parent_address: True
|
||||
function: Senior Consultant
|
||||
email: david.s@tech.info
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_11}:
|
||||
name: John M. Brown
|
||||
parent_id: base.res_partner_5
|
||||
use_parent_address: True
|
||||
function: Director
|
||||
email: john.b@tech.info
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_12}:
|
||||
name: James Miller
|
||||
parent_id: base.res_partner_6
|
||||
use_parent_address: True
|
||||
function: Electrical Supervisor
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_13}:
|
||||
name: Charlie Bernard
|
||||
parent_id: base.res_partner_7
|
||||
use_parent_address: True
|
||||
function: Senior Associate
|
||||
email: charlie.bernard@wealthyandsons.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_14}:
|
||||
name: Jessica Dupont
|
||||
parent_id: base.res_partner_7
|
||||
use_parent_address: True
|
||||
function: Analyst
|
||||
email: jessica.dupont@wealthyandsons.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_15}:
|
||||
name: Phillipp Miller
|
||||
parent_id: base.res_partner_8
|
||||
use_parent_address: True
|
||||
function: Creative Director
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_16}:
|
||||
name: Ayaan Agarwal
|
||||
parent_id: base.res_partner_9
|
||||
use_parent_address: True
|
||||
function: Director
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_17}:
|
||||
name: Daniel Jackson
|
||||
parent_id: base.res_partner_10
|
||||
use_parent_address: True
|
||||
function: Managing Partner
|
||||
email: daniel@jackson.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_18}:
|
||||
name: William Thomas
|
||||
parent_id: base.res_partner_10
|
||||
use_parent_address: True
|
||||
function: Senior Consultant
|
||||
email: william@jackson.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_19}:
|
||||
name: Sergio Pérez
|
||||
parent_id: base.res_partner_11
|
||||
use_parent_address: True
|
||||
function: Accountant
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_20}:
|
||||
name: Laura Castro
|
||||
parent_id: base.res_partner_11
|
||||
use_parent_address: True
|
||||
function: Goods Supervisor
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_21}:
|
||||
name: Luc Maurer
|
||||
parent_id: base.res_partner_12
|
||||
use_parent_address: True
|
||||
function: Director
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_22}:
|
||||
name: Laith Jubair
|
||||
parent_id: base.res_partner_13
|
||||
use_parent_address: True
|
||||
function: Director
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_23}:
|
||||
name: Angel Cook
|
||||
parent_id: base.res_partner_14
|
||||
use_parent_address: True
|
||||
function: General Manager
|
||||
email: angel.cook@chamberworks.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_24}:
|
||||
name: Robert Anderson
|
||||
parent_id: base.res_partner_14
|
||||
use_parent_address: True
|
||||
function: System Analyst
|
||||
email: robert.anderson@chamberworks.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_25}:
|
||||
name: Jacob Taylor
|
||||
parent_id: base.res_partner_15
|
||||
use_parent_address: True
|
||||
function: Order Clerk
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_26}:
|
||||
name: Arthur Gomez
|
||||
parent_id: base.res_partner_16
|
||||
use_parent_address: True
|
||||
function: Software Developer
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_27}:
|
||||
name: Julia Rivero
|
||||
parent_id: base.res_partner_16
|
||||
use_parent_address: True
|
||||
function: Technical Director
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_28}:
|
||||
name: Benjamin Flores
|
||||
parent_id: base.res_partner_17
|
||||
use_parent_address: True
|
||||
function: Business Executive
|
||||
email: ben@nebula.ar
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_29}:
|
||||
name: George Wilson
|
||||
parent_id: base.res_partner_18
|
||||
use_parent_address: True
|
||||
function: Chief Information Officer (CIO)
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_30}:
|
||||
name: Lucas Jones
|
||||
parent_id: base.res_partner_18
|
||||
use_parent_address: True
|
||||
function: Functional Consultant
|
||||
email: jones@thinkbig.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_31}:
|
||||
name: Edward Foster
|
||||
parent_id: base.res_partner_19
|
||||
use_parent_address: True
|
||||
function: Sales Representative
|
||||
email: efoster@seagate.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_32}:
|
||||
name: Robin Smith
|
||||
parent_id: base.res_partner_21
|
||||
use_parent_address: True
|
||||
function: Sales Manager
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_33}:
|
||||
name: Morgan Rose
|
||||
parent_id: base.res_partner_21
|
||||
use_parent_address: True
|
||||
function: Financial Manager
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_34}:
|
||||
name: Kevin Clarke
|
||||
parent_id: base.res_partner_21
|
||||
use_parent_address: True
|
||||
function: Knowledge Manager
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_address_35}:
|
||||
name: Peter Mitchell
|
||||
parent_id: base.res_partner_22
|
||||
use_parent_address: True
|
||||
function: Store Manager
|
||||
email:
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_main1}:
|
||||
name: Mark Davis
|
||||
customer: False
|
||||
parent_id: base.main_partner
|
||||
use_parent_address: True
|
||||
function: Chief Executive Officer (CEO)
|
||||
email: mark@yourcompany.com
|
||||
-
|
||||
!record {model: 'res.partner', id: base.res_partner_main2}:
|
||||
name: Roger Scott
|
||||
customer: False
|
||||
parent_id: base.main_partner
|
||||
use_parent_address: True
|
||||
function: Chief Operations Officer (COO)
|
||||
email: roger@yourcompany.com
|
||||
@@ -95,27 +95,28 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Partners" version="7.0">
|
||||
<sheet>
|
||||
<field name="photo" widget='image' class="oe_avatar oe_left"/>
|
||||
<field name="image_small" widget='image' class="oe_avatar oe_left"/>
|
||||
<div class="oe_title">
|
||||
<div class="oe_edit_only">
|
||||
<label for="name"/> (
|
||||
<field name="is_company" on_change="onchange_type(is_company)" class="oe_inline"/> <label for="is_company" string="Is a Company?"/>)
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name" default_focus="1" placeholder="Name" />
|
||||
</h1>
|
||||
<field name="parent_id"
|
||||
string="Work for Company"
|
||||
placeholder="Company"
|
||||
domain="[('is_company', '=', True)]" context="{'default_is_company': True}"
|
||||
attrs="{'invisible': [('is_company','=', True)]}"
|
||||
on_change="onchange_address(use_parent_address, parent_id)"/>
|
||||
<div class="oe_edit_only">
|
||||
<field name="is_company" on_change="onchange_type(is_company)" class="oe_inline"/> <label for="is_company" string="Company?"/>
|
||||
<field name="category_id" widget="many2many_tags" placeholder="Select Tags"/>
|
||||
</div>
|
||||
<field name="category_id" widget="many2many_tags" placeholder="Tags..."/>
|
||||
</div>
|
||||
<div class="oe_right oe_button_box" name="buttons"> </div>
|
||||
|
||||
<group>
|
||||
<group>
|
||||
<label for="type" attrs="{'invisible': [('parent_id','=', False)]}"/>
|
||||
<div attrs="{'invisible': [('parent_id','=', False)]}">
|
||||
<label for="type" attrs="{'invisible': [('parent_id','=', False)]}" invisible="1"/>
|
||||
<div attrs="{'invisible': [('parent_id','=', False)]}" invisible="1" name="div_type">
|
||||
<field class="oe_inline"
|
||||
name="type"/>
|
||||
<label for="use_parent_address" class="oe_edit_only"/>
|
||||
@@ -129,10 +130,10 @@
|
||||
<field name="street2"/>
|
||||
<div class="address_format">
|
||||
<field name="city" placeholder="City" style="width: 40%%"/>
|
||||
<field name="state_id" class="oe_no_button" placeholder="State" style="width: 24%%"/>
|
||||
<field name="state_id" class="oe_no_button" placeholder="State" style="width: 24%%" options='{"no_open": true}'/>
|
||||
<field name="zip" placeholder="ZIP" style="width: 34%%"/>
|
||||
</div>
|
||||
<field name="country_id" placeholder="Country" class="oe_no_button"/>
|
||||
<field name="country_id" placeholder="Country" class="oe_no_button" options='{"no_open": true}'/>
|
||||
</div>
|
||||
<field name="website" widget="url" placeholder="e.g. www.openerp.com"/>
|
||||
</group>
|
||||
@@ -144,6 +145,7 @@
|
||||
<field name="fax"/>
|
||||
<field name="email" widget="email"/>
|
||||
<field name="title" domain="[('domain', '=', 'contact')]"
|
||||
groups="base.group_no_one"
|
||||
options='{"no_open": true}' attrs="{'invisible': [('is_company','=', True)]}" />
|
||||
</group>
|
||||
</group>
|
||||
@@ -162,7 +164,7 @@
|
||||
<field name="phone"/>
|
||||
<field name="street"/>
|
||||
<field name="street2"/>
|
||||
<field name="photo"/>
|
||||
<field name="image_small"/>
|
||||
<field name="zip"/>
|
||||
<field name="city"/>
|
||||
<field name="country_id"/>
|
||||
@@ -175,7 +177,7 @@
|
||||
<a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
|
||||
<div class="oe_module_vignette">
|
||||
<a type="edit">
|
||||
<img t-att-src="kanban_image('res.partner', 'photo', record.id.value)" class="oe_avatar oe_kanban_avatar_toto"/>
|
||||
<img t-att-src="kanban_image('res.partner', 'image_small', record.id.value)" class="oe_avatar oe_kanban_avatar_smallbox"/>
|
||||
</a>
|
||||
<div class="oe_module_desc">
|
||||
<div class="oe_kanban_box_content oe_kanban_color_bglight oe_kanban_box_show_onclick_trigger oe_kanban_color_border">
|
||||
@@ -201,17 +203,33 @@
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form string="Contact" version="7.0">
|
||||
<field name="image_small" widget='image' class="oe_avatar oe_left"/>
|
||||
<div class="oe_title">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="category_id" widget="many2many_tags" placeholder="Tags..."/>
|
||||
<field name="function" placeholder="e.g. Sales Director"/>
|
||||
<field name="email"/>
|
||||
<field name="phone"/>
|
||||
<field name="mobile"/>
|
||||
</group>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Internal Notes">
|
||||
<field name="comment" placeholder="Internal notes about this customer..."/>
|
||||
</page>
|
||||
<page string="Sales & Purchases" attrs="{'invisible': [('customer', '=', False), ('supplier', '=', False)]}">
|
||||
<group>
|
||||
<group>
|
||||
<field name="user_id" />
|
||||
<field name="user_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="customer"/>
|
||||
<field name="supplier" invisible="1"/>
|
||||
<field name="supplier" invisible="not context.get('default_supplier')"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="ref"/>
|
||||
@@ -226,9 +244,6 @@
|
||||
<!-- The History page becomes visible as soon as there is something to display inside -->
|
||||
<page string="History" name="page_history" invisible="True">
|
||||
</page>
|
||||
<page string="Internal Notes">
|
||||
<field name="comment" placeholder="Internal notes about this customer..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
@@ -251,7 +266,7 @@
|
||||
<filter string="Customers" name="customer" icon="terp-personal" domain="[('customer','=',1)]" help="Customer Partners"/>
|
||||
<separator/>
|
||||
<filter string="Suppliers" name="supplier" icon="terp-personal" domain="[('supplier','=',1)]" help="Supplier Partners"/>
|
||||
<field name="category_id"/>
|
||||
<field name="category_id" string="Category" filter_domain="[('category_id','ilike', self)]"/>
|
||||
<field name="user_id"/>
|
||||
<field name="parent_id" filter_domain="[('parent_id','child_of',[self])]"/>
|
||||
<group expand="0" string="Group By...">
|
||||
@@ -278,7 +293,7 @@
|
||||
<field name="phone"/>
|
||||
<field name="street"/>
|
||||
<field name="street2"/>
|
||||
<field name="photo"/>
|
||||
<field name="image_small"/>
|
||||
<field name="zip"/>
|
||||
<field name="city"/>
|
||||
<field name="country_id"/>
|
||||
@@ -289,7 +304,7 @@
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_vignette">
|
||||
<a type="edit">
|
||||
<img t-att-src="kanban_image('res.partner', 'photo', record.id.value)" class="oe_kanban_image"/>
|
||||
<img t-att-src="kanban_image('res.partner', 'image_small', record.id.value)" class="oe_kanban_image"/>
|
||||
</a>
|
||||
<div class="oe_kanban_details">
|
||||
<h4 class="oe_partner_heading"><a type="edit"><field name="name"/></a></h4>
|
||||
|
||||
@@ -25,7 +25,6 @@ from functools import partial
|
||||
|
||||
import pytz
|
||||
|
||||
import io, StringIO
|
||||
from lxml import etree
|
||||
from lxml.builder import E
|
||||
import netsvc
|
||||
@@ -33,7 +32,6 @@ import openerp
|
||||
import openerp.exceptions
|
||||
from osv import fields,osv
|
||||
from osv.orm import browse_record
|
||||
from PIL import Image
|
||||
import pooler
|
||||
import random
|
||||
from service import security
|
||||
@@ -152,33 +150,6 @@ class users(osv.osv):
|
||||
body=(self.get_welcome_mail_body(cr, uid, context=context) % user))
|
||||
return ir_mail_server.send_email(cr, uid, msg, context=context)
|
||||
|
||||
def onchange_avatar(self, cr, uid, ids, value, context=None):
|
||||
if not value:
|
||||
return {'value': {'avatar_big': value, 'avatar': value} }
|
||||
return {'value': {'avatar_big': self._avatar_resize(cr, uid, value, 540, 450, context=context), 'avatar': self._avatar_resize(cr, uid, value, context=context)} }
|
||||
|
||||
def _set_avatar(self, cr, uid, id, name, value, args, context=None):
|
||||
if not value:
|
||||
vals = {'avatar_big': value}
|
||||
else:
|
||||
vals = {'avatar_big': self._avatar_resize(cr, uid, value, 540, 450, context=context)}
|
||||
return self.write(cr, uid, [id], vals, context=context)
|
||||
|
||||
def _avatar_resize(self, cr, uid, avatar, height=180, width=150, context=None):
|
||||
image_stream = io.BytesIO(avatar.decode('base64'))
|
||||
img = Image.open(image_stream)
|
||||
img.thumbnail((height, width), Image.ANTIALIAS)
|
||||
img_stream = StringIO.StringIO()
|
||||
img.save(img_stream, "PNG")
|
||||
return img_stream.getvalue().encode('base64')
|
||||
|
||||
def _get_avatar(self, cr, uid, ids, name, args, context=None):
|
||||
result = dict.fromkeys(ids, False)
|
||||
for user in self.browse(cr, uid, ids, context=context):
|
||||
if user.avatar_big:
|
||||
result[user.id] = self._avatar_resize(cr, uid, user.avatar_big, context=context)
|
||||
return result
|
||||
|
||||
def _set_new_password(self, cr, uid, id, name, value, args, context=None):
|
||||
if value is False:
|
||||
# Do not update the password if no value is provided, ignore silently.
|
||||
@@ -194,6 +165,15 @@ class users(osv.osv):
|
||||
def _get_password(self, cr, uid, ids, arg, karg, context=None):
|
||||
return dict.fromkeys(ids, '')
|
||||
|
||||
def _get_image(self, cr, uid, ids, name, args, context=None):
|
||||
result = dict.fromkeys(ids, False)
|
||||
for obj in self.browse(cr, uid, ids, context=context):
|
||||
result[obj.id] = tools.image_get_resized_images(obj.image)
|
||||
return result
|
||||
|
||||
def _set_image(self, cr, uid, id, name, value, args, context=None):
|
||||
return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
|
||||
|
||||
_columns = {
|
||||
'id': fields.integer('ID'),
|
||||
'name': fields.char('User Name', size=64, required=True, select=True,
|
||||
@@ -208,11 +188,26 @@ class users(osv.osv):
|
||||
"otherwise leave empty. After a change of password, the user has to login again."),
|
||||
'user_email': fields.char('Email', size=64),
|
||||
'signature': fields.text('Signature', size=64),
|
||||
'avatar_big': fields.binary('Big-sized avatar', help="This field holds the image used as avatar for the user. The avatar field is used as an interface to access this field. The image is base64 encoded, and PIL-supported. It is stored as a 540x450 px image, in case a bigger image must be used."),
|
||||
'avatar': fields.function(_get_avatar, fnct_inv=_set_avatar, string='Avatar', type="binary",
|
||||
'image': fields.binary("Avatar",
|
||||
help="This field holds the image used as avatar for the "\
|
||||
"user. The image is base64 encoded, and PIL-supported. "\
|
||||
"It is limited to a 1024x1024 px image."),
|
||||
'image_medium': fields.function(_get_image, fnct_inv=_set_image,
|
||||
string="Medium-sized avatar", type="binary", multi="_get_image",
|
||||
store = {
|
||||
'res.users': (lambda self, cr, uid, ids, c={}: ids, ['avatar_big'], 10),
|
||||
}, help="Image used as avatar for the user. It is automatically resized as a 180x150 px image. This field serves as an interface to the avatar_big field."),
|
||||
'res.users': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
|
||||
},
|
||||
help="Medium-sized image of the user. It is automatically "\
|
||||
"resized as a 180x180 px image, with aspect ratio preserved. "\
|
||||
"Use this field in form views or some kanban views."),
|
||||
'image_small': fields.function(_get_image, fnct_inv=_set_image,
|
||||
string="Smal-sized avatar", type="binary", multi="_get_image",
|
||||
store = {
|
||||
'res.users': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
|
||||
},
|
||||
help="Small-sized image of the user. It is automatically "\
|
||||
"resized as a 50x50 px image, with aspect ratio preserved. "\
|
||||
"Use this field anywhere a small image is required."),
|
||||
'active': fields.boolean('Active'),
|
||||
'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at logon for this user, in addition to the standard menu."),
|
||||
'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
|
||||
@@ -320,16 +315,18 @@ class users(osv.osv):
|
||||
pass
|
||||
return result
|
||||
|
||||
def _get_avatar(self, cr, uid, context=None):
|
||||
# default avatar file name: avatar0 -> avatar6.png, choose randomly
|
||||
avatar_path = openerp.modules.get_module_resource('base', 'static/src/img', 'avatar%d.png' % random.randint(0, 6))
|
||||
return self._avatar_resize(cr, uid, open(avatar_path, 'rb').read().encode('base64'), context=context)
|
||||
def _get_default_image(self, cr, uid, context=None):
|
||||
# default image file name: avatar0 -> avatar6.png, choose randomly
|
||||
image_path = openerp.modules.get_module_resource('base', 'static/src/img', 'avatar%d.png' % random.randint(0, 6))
|
||||
return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
|
||||
|
||||
_defaults = {
|
||||
'password' : '',
|
||||
'context_lang': lambda self, cr, uid, context: context.get('lang', 'en_US'),
|
||||
'context_tz': lambda self, cr, uid, context: context.get('tz', False),
|
||||
'avatar': _get_avatar,
|
||||
'image': _get_default_image,
|
||||
'image_small': _get_default_image,
|
||||
'image_medium': _get_default_image,
|
||||
'active' : True,
|
||||
'menu_id': _get_menu,
|
||||
'company_id': _get_company,
|
||||
@@ -338,7 +335,7 @@ class users(osv.osv):
|
||||
}
|
||||
|
||||
# User can write to a few of her own fields (but not her groups for example)
|
||||
SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'user_email', 'name', 'avatar', 'avatar_big']
|
||||
SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'user_email', 'name', 'image', 'image_medium', 'image_small']
|
||||
|
||||
def write(self, cr, uid, ids, values, context=None):
|
||||
if not hasattr(ids, '__iter__'):
|
||||
@@ -539,6 +536,19 @@ class users(osv.osv):
|
||||
return self.write(cr, uid, uid, {'password': new_passwd})
|
||||
raise osv.except_osv(_('Warning!'), _("Setting empty passwords is not allowed for security reasons!"))
|
||||
|
||||
def preference_save(self, cr, uid, ids, context=None):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
|
||||
def preference_change_password(self, cr, uid, ids, context=None):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'change_password',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def has_group(self, cr, uid, group_ext_id):
|
||||
"""Checks whether user belongs to given group.
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<form string="Users" version="7.0">
|
||||
<field name="id" invisible="1"/>
|
||||
<sheet>
|
||||
<field name="avatar" widget='image' on_change="onchange_avatar(avatar)" class="oe_avatar oe_left"/>
|
||||
<field name="image_medium" widget='image' class="oe_avatar oe_left"/>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name"/></h1>
|
||||
@@ -195,9 +195,8 @@
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Users" version="7.0">
|
||||
<sheet>
|
||||
<div class="oe_right oe_avatar">
|
||||
<field name="avatar" widget='image' on_change="onchange_avatar(avatar)"/>
|
||||
<field name="image_small" widget='image' class="oe_image_small"/>
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name" readonly="1" class="oe_inline"/>
|
||||
@@ -215,7 +214,11 @@
|
||||
<field name="user_email" widget="email" readonly="0"/>
|
||||
<field name="signature" readonly="0"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="preference_save" type="object" string="Save"/>
|
||||
<button name="preference_cancel" special="cancel" string="Cancel"/>
|
||||
<button name="preference_change_password" type="object" string="Change password"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
@@ -223,14 +226,9 @@
|
||||
<field name="name">Change My Preferences</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.users</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form,tree</field>
|
||||
<field name="domain">[('id','=',uid)]</field>
|
||||
</record>
|
||||
<record id="action_res_users_my_view1" model="ir.actions.act_window.view">
|
||||
<field eval="20" name="sequence"/>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="act_window_id" ref="action_res_users_my"/>
|
||||
<field name="view_mode">form</field>
|
||||
</record>
|
||||
<record id="action_res_users_my_view2" model="ir.actions.act_window.view">
|
||||
<field eval="10" name="sequence"/>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
|
||||
<assert model="res.request" id="res_partner_asustek_request" string="Reference field request returns a browsable object">
|
||||
<test expr="ref_doc1.name">ASUStek</test>
|
||||
<test expr="ref_doc1.name">ASUSTeK</test>
|
||||
</assert>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
Test hierarchical search in M2M with child ID1
|
||||
-
|
||||
!python {model: res.partner }: |
|
||||
ids = self.search(cr, uid, [('category_id', 'child_of','Components Supplier')])
|
||||
ids = self.search(cr, uid, [('category_id', 'child_of','Customer')])
|
||||
assert len(ids) >= 1, ids
|
||||
-
|
||||
Test hierarchical search in M2M with child ID2
|
||||
-
|
||||
!python {model: res.partner }: |
|
||||
ids = self.search(cr, uid, [('category_id', 'child_of','Miscellaneous Suppliers')])
|
||||
assert len(ids) >= 1, ids
|
||||
ids = self.search(cr, uid, [('category_id', 'child_of','Manufacturer')])
|
||||
assert len(ids) == 2, ids
|
||||
|
||||
-
|
||||
"1.0 Setup test partner categories: parent root"
|
||||
@@ -464,8 +464,8 @@
|
||||
!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
|
||||
all_ids = self.search(cr, uid, [('name', '=ilike', 'v%')])
|
||||
assert len(all_ids) >= 1, "Must match one partner (Vicking Direct), got %r"%all_ids
|
||||
-
|
||||
Check that =like/=ilike expressions (no wildcard variants of like/ilike) are working on translated field.
|
||||
-
|
||||
@@ -512,7 +512,7 @@
|
||||
Testing for Many2Many field with category supplier and active=False
|
||||
-
|
||||
!python {model: res.partner }: |
|
||||
vals = {'category_id': [(6, 0, [ref("base.res_partner_category_8")])],
|
||||
vals = {'category_id': [(6, 0, [ref("base.res_partner_category_1")])],
|
||||
'name': 'OpenERP Test',
|
||||
'active': False,
|
||||
'child_ids': [(0, 0, {'name': 'address of OpenERP Test', 'country_id': ref("base.be")})]
|
||||
|
||||
@@ -24,11 +24,7 @@
|
||||
|
||||
"""
|
||||
|
||||
import openerp.modules.db
|
||||
import openerp.modules.graph
|
||||
import openerp.modules.loading
|
||||
import openerp.modules.migration
|
||||
import openerp.modules.module
|
||||
from . import db, graph, loading, migration, module, registry
|
||||
|
||||
# TODO temporarily expose those things
|
||||
from openerp.modules.module import \
|
||||
|
||||
+14
-5
@@ -68,10 +68,7 @@ def initialize(cr):
|
||||
category_id = create_categories(cr, categories)
|
||||
|
||||
if info['installable']:
|
||||
if info['auto_install'] and not info['depends']:
|
||||
state = 'to install'
|
||||
else:
|
||||
state = 'uninstalled'
|
||||
state = 'uninstalled'
|
||||
else:
|
||||
state = 'uninstallable'
|
||||
|
||||
@@ -95,7 +92,19 @@ def initialize(cr):
|
||||
for d in dependencies:
|
||||
cr.execute('INSERT INTO ir_module_module_dependency \
|
||||
(module_id,name) VALUES (%s, %s)', (id, d))
|
||||
cr.commit()
|
||||
|
||||
# Install recursively all auto-installing modules
|
||||
while True:
|
||||
cr.execute("""SELECT m.name FROM ir_module_module m WHERE m.auto_install AND state != 'to install'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ir_module_module_dependency d JOIN ir_module_module mdep ON (d.name = mdep.name)
|
||||
WHERE d.module_id = m.id AND mdep.state != 'to install'
|
||||
)""")
|
||||
to_auto_install = [x[0] for x in cr.fetchall()]
|
||||
if not to_auto_install: break
|
||||
cr.execute("""UPDATE ir_module_module SET state='to install' WHERE name in %s""", (tuple(to_auto_install),))
|
||||
|
||||
cr.commit()
|
||||
|
||||
def create_categories(cr, categories):
|
||||
""" Create the ir_module_category entries for some categories.
|
||||
|
||||
@@ -171,11 +171,6 @@ class integer(_column):
|
||||
|
||||
def __init__(self, string='unknown', required=False, **args):
|
||||
super(integer, self).__init__(string=string, required=required, **args)
|
||||
if required:
|
||||
_logger.debug(
|
||||
"required=True is deprecated: making an integer field"
|
||||
" `required` has no effect, as NULL values are "
|
||||
"automatically turned into 0.")
|
||||
|
||||
class reference(_column):
|
||||
_type = 'reference'
|
||||
@@ -246,11 +241,6 @@ class float(_column):
|
||||
self.digits = digits
|
||||
# synopsis: digits_compute(cr) -> (precision, scale)
|
||||
self.digits_compute = digits_compute
|
||||
if required:
|
||||
_logger.debug(
|
||||
"required=True is deprecated: making a float field"
|
||||
" `required` has no effect, as NULL values are "
|
||||
"automatically turned into 0.0.")
|
||||
|
||||
def digits_change(self, cr):
|
||||
if self.digits_compute:
|
||||
|
||||
+5
-7
@@ -2442,9 +2442,9 @@ class BaseModel(object):
|
||||
context=context)
|
||||
|
||||
result_template = dict.fromkeys(aggregated_fields, False)
|
||||
result_template.update({groupby + '_count':0})
|
||||
result_template[groupby + '_count'] = 0
|
||||
if groupby_list and len(groupby_list) > 1:
|
||||
result_template.update(__context={'group_by': groupby_list[1:]})
|
||||
result_template['__context'] = {'group_by': groupby_list[1:]}
|
||||
|
||||
# Merge the left_side (current results as dicts) with the right_side (all
|
||||
# possible values as m2o pairs). Both lists are supposed to be using the
|
||||
@@ -2463,10 +2463,8 @@ class BaseModel(object):
|
||||
grouped_value = right_side[0]
|
||||
if not grouped_value in known_values:
|
||||
line = dict(result_template)
|
||||
line.update({
|
||||
groupby: right_side,
|
||||
'__domain': [(groupby,'=',grouped_value)] + domain,
|
||||
})
|
||||
line[groupby] = right_side
|
||||
line['__domain'] = [(groupby,'=',grouped_value)] + domain
|
||||
result.append(line)
|
||||
known_values[grouped_value] = line
|
||||
while read_group_result or all_groups:
|
||||
@@ -4197,7 +4195,7 @@ class BaseModel(object):
|
||||
cr.execute("SELECT nextval('"+self._sequence+"')")
|
||||
except:
|
||||
raise except_orm(_('UserError'),
|
||||
_('You cannot perform this operation. New Record Creation is not allowed for this object as this object is for reporting purpose.'))
|
||||
_('You cannot perform this operation. New Record Creation is not allowed for this object as this object is for reporting purpose.'))
|
||||
|
||||
id_new = cr.fetchone()[0]
|
||||
for table in tocreate:
|
||||
|
||||
@@ -8,20 +8,17 @@ Tests can be explicitely added to the `fast_suite` or `checks` lists or not.
|
||||
See the :ref:`test-framework` section in the :ref:`features` list.
|
||||
"""
|
||||
|
||||
import test_expression
|
||||
import test_ir_sequence
|
||||
import test_orm
|
||||
import test_view_validation
|
||||
import test_uninstall
|
||||
from . import test_expression, test_ir_sequence, test_orm,\
|
||||
test_view_validation, test_uninstall
|
||||
|
||||
fast_suite = [
|
||||
test_ir_sequence,
|
||||
]
|
||||
]
|
||||
|
||||
checks = [
|
||||
test_expression,
|
||||
test_orm,
|
||||
test_view_validation,
|
||||
]
|
||||
]
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem icon="STOCK_PREFERENCES" id="base.menu_tests" name="Tests"/>
|
||||
<menuitem icon="STOCK_PREFERENCES" id="base.menu_tests" name="Tests" sequence="1000000"/>
|
||||
|
||||
<menuitem id="menu_test_exceptions" parent="base.menu_tests" name="Test exceptions"/>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import models
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'test-import-export',
|
||||
'version': '0.1',
|
||||
'category': 'Tests',
|
||||
'description': """A module to test import/export.""",
|
||||
'author': 'OpenERP SA',
|
||||
'maintainer': 'OpenERP SA',
|
||||
'website': 'http://www.openerp.com',
|
||||
'depends': ['base'],
|
||||
'data': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp.osv import orm, fields
|
||||
|
||||
def selection_fn(obj, cr, uid, context=None):
|
||||
return list(enumerate(["Corge", "Grault", "Wheee", "Moog"]))
|
||||
|
||||
def function_fn(model, cr, uid, ids, field_name, arg, context):
|
||||
return dict((id, 3) for id in ids)
|
||||
def function_fn_write(model, cr, uid, id, field_name, field_value, fnct_inv_arg, context):
|
||||
""" just so CreatorCase.export can be used
|
||||
"""
|
||||
pass
|
||||
|
||||
models = [
|
||||
('boolean', fields.boolean()),
|
||||
('integer', fields.integer()),
|
||||
('float', fields.float()),
|
||||
('decimal', fields.float(digits=(16, 3))),
|
||||
('string.bounded', fields.char('unknown', size=16)),
|
||||
('string', fields.char('unknown', size=None)),
|
||||
('date', fields.date()),
|
||||
('datetime', fields.datetime()),
|
||||
('text', fields.text()),
|
||||
('selection', fields.selection([(1, "Foo"), (2, "Bar"), (3, "Qux")])),
|
||||
('selection.function', fields.selection(selection_fn)),
|
||||
# just relate to an integer
|
||||
('many2one', fields.many2one('export.integer')),
|
||||
('one2many', fields.one2many('export.one2many.child', 'parent_id')),
|
||||
('many2many', fields.many2many('export.many2many.other')),
|
||||
('function', fields.function(function_fn, fnct_inv=function_fn_write, type="integer")),
|
||||
# related: specialization of fields.function, should work the same way
|
||||
# TODO: reference
|
||||
]
|
||||
for name, field in models:
|
||||
attrs = {
|
||||
'_name': 'export.%s' % name,
|
||||
'_columns': {
|
||||
'const': fields.integer(),
|
||||
'value': field
|
||||
},
|
||||
'_defaults': {'const': 4},
|
||||
'name_get': (lambda self, cr, uid, ids, context=None:
|
||||
[(record.id, "%s:%s" % (self._name, record.value))
|
||||
for record in self.browse(cr, uid, ids, context=context)]),
|
||||
'name_search': (lambda self, cr, uid, name, operator, context=None:
|
||||
self.name_get(cr, uid,
|
||||
self.search(cr, uid, [['value', operator, int(name.split(':')[1])]])
|
||||
, context=context)
|
||||
if isinstance(name, basestring) and name.split(':')[0] == self._name
|
||||
else [])
|
||||
}
|
||||
NewModel = type(
|
||||
'Export%s' % ''.join(section.capitalize() for section in name.split('.')),
|
||||
(orm.Model,),
|
||||
attrs)
|
||||
|
||||
class One2ManyChild(orm.Model):
|
||||
_name = 'export.one2many.child'
|
||||
# FIXME: orm.py:1161, fix to name_get on m2o field
|
||||
_rec_name = 'value'
|
||||
|
||||
_columns = {
|
||||
'parent_id': fields.many2one('export.one2many'),
|
||||
'str': fields.char('unknown', size=None),
|
||||
'value': fields.integer()
|
||||
}
|
||||
def name_get(self, cr, uid, ids, context=None):
|
||||
return [(record.id, "%s:%s" % (self._name, record.value))
|
||||
for record in self.browse(cr, uid, ids, context=context)]
|
||||
|
||||
class One2ManyMultiple(orm.Model):
|
||||
_name = 'export.one2many.multiple'
|
||||
|
||||
_columns = {
|
||||
'const': fields.integer(),
|
||||
'child1': fields.one2many('export.one2many.child.1', 'parent_id'),
|
||||
'child2': fields.one2many('export.one2many.child.2', 'parent_id'),
|
||||
}
|
||||
_defaults = { 'const': 36 }
|
||||
|
||||
class One2ManyChildMultiple(orm.Model):
|
||||
_name = 'export.one2many.multiple.child'
|
||||
# FIXME: orm.py:1161, fix to name_get on m2o field
|
||||
_rec_name = 'value'
|
||||
|
||||
_columns = {
|
||||
'parent_id': fields.many2one('export.one2many.multiple'),
|
||||
'str': fields.char('unknown', size=None),
|
||||
'value': fields.integer()
|
||||
}
|
||||
def name_get(self, cr, uid, ids, context=None):
|
||||
return [(record.id, "%s:%s" % (self._name, record.value))
|
||||
for record in self.browse(cr, uid, ids, context=context)]
|
||||
class One2ManyChild1(orm.Model):
|
||||
_name = 'export.one2many.child.1'
|
||||
_inherit = 'export.one2many.multiple.child'
|
||||
class One2ManyChild2(orm.Model):
|
||||
_name = 'export.one2many.child.2'
|
||||
_inherit = 'export.one2many.multiple.child'
|
||||
|
||||
class Many2ManyChild(orm.Model):
|
||||
_name = 'export.many2many.other'
|
||||
# FIXME: orm.py:1161, fix to name_get on m2o field
|
||||
_rec_name = 'value'
|
||||
|
||||
_columns = {
|
||||
'str': fields.char('unknown', size=None),
|
||||
'value': fields.integer()
|
||||
}
|
||||
def name_get(self, cr, uid, ids, context=None):
|
||||
return [(record.id, "%s:%s" % (self._name, record.value))
|
||||
for record in self.browse(cr, uid, ids, context=context)]
|
||||
def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
|
||||
return (self.name_get(cr, user,
|
||||
self.search(cr, user, [['value', operator, int(name.split(':')[1])]])
|
||||
, context=context)
|
||||
if isinstance(name, basestring) and name.split(':')[0] == self._name
|
||||
else [])
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_export, test_import
|
||||
|
||||
fast_suite = [
|
||||
]
|
||||
|
||||
checks = [
|
||||
test_export,
|
||||
test_import,
|
||||
]
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
@@ -0,0 +1,589 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import itertools
|
||||
import openerp.modules.registry
|
||||
import openerp
|
||||
|
||||
from openerp.tests import common
|
||||
|
||||
|
||||
class CreatorCase(common.TransactionCase):
|
||||
model_name = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreatorCase, self).__init__(*args, **kwargs)
|
||||
self.model = None
|
||||
|
||||
def setUp(self):
|
||||
super(CreatorCase, self).setUp()
|
||||
self.model = self.registry(self.model_name)
|
||||
def make(self, value):
|
||||
id = self.model.create(self.cr, openerp.SUPERUSER_ID, {'value': value})
|
||||
return self.model.browse(self.cr, openerp.SUPERUSER_ID, [id])[0]
|
||||
def export(self, value, fields=('value',), context=None):
|
||||
record = self.make(value)
|
||||
return self.model._BaseModel__export_row(
|
||||
self.cr, openerp.SUPERUSER_ID, record,
|
||||
[f.split('/') for f in fields],
|
||||
context=context)
|
||||
|
||||
class test_boolean_field(CreatorCase):
|
||||
model_name = 'export.boolean'
|
||||
|
||||
def test_true(self):
|
||||
self.assertEqual(
|
||||
self.export(True),
|
||||
[[u'True']])
|
||||
def test_false(self):
|
||||
""" ``False`` value to boolean fields is unique in being exported as a
|
||||
(unicode) string, not a boolean
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.export(False),
|
||||
[[u'False']])
|
||||
|
||||
class test_integer_field(CreatorCase):
|
||||
model_name = 'export.integer'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(self.model.search(self.cr, openerp.SUPERUSER_ID, []), [],
|
||||
"Test model should have no records")
|
||||
def test_0(self):
|
||||
self.assertEqual(
|
||||
self.export(0),
|
||||
[[False]])
|
||||
|
||||
def test_basic_value(self):
|
||||
self.assertEqual(
|
||||
self.export(42),
|
||||
[[u'42']])
|
||||
|
||||
def test_negative(self):
|
||||
self.assertEqual(
|
||||
self.export(-32),
|
||||
[[u'-32']])
|
||||
|
||||
def test_huge(self):
|
||||
self.assertEqual(
|
||||
self.export(2**31-1),
|
||||
[[unicode(2**31-1)]])
|
||||
|
||||
class test_float_field(CreatorCase):
|
||||
model_name = 'export.float'
|
||||
|
||||
def test_0(self):
|
||||
self.assertEqual(
|
||||
self.export(0.0),
|
||||
[[False]])
|
||||
|
||||
def test_epsilon(self):
|
||||
self.assertEqual(
|
||||
self.export(0.000000000027),
|
||||
[[u'2.7e-11']])
|
||||
|
||||
def test_negative(self):
|
||||
self.assertEqual(
|
||||
self.export(-2.42),
|
||||
[[u'-2.42']])
|
||||
|
||||
def test_positive(self):
|
||||
self.assertEqual(
|
||||
self.export(47.36),
|
||||
[[u'47.36']])
|
||||
|
||||
def test_big(self):
|
||||
self.assertEqual(
|
||||
self.export(87654321.4678),
|
||||
[[u'87654321.4678']])
|
||||
|
||||
class test_decimal_field(CreatorCase):
|
||||
model_name = 'export.decimal'
|
||||
|
||||
def test_0(self):
|
||||
self.assertEqual(
|
||||
self.export(0.0),
|
||||
[[False]])
|
||||
|
||||
def test_epsilon(self):
|
||||
""" epsilon gets sliced to 0 due to precision
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.export(0.000000000027),
|
||||
[[False]])
|
||||
|
||||
def test_negative(self):
|
||||
self.assertEqual(
|
||||
self.export(-2.42),
|
||||
[[u'-2.42']])
|
||||
|
||||
def test_positive(self):
|
||||
self.assertEqual(
|
||||
self.export(47.36),
|
||||
[[u'47.36']])
|
||||
|
||||
def test_big(self):
|
||||
self.assertEqual(
|
||||
self.export(87654321.4678), [[u'87654321.468']])
|
||||
|
||||
class test_string_field(CreatorCase):
|
||||
model_name = 'export.string.bounded'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(""),
|
||||
[[False]])
|
||||
def test_within_bounds(self):
|
||||
self.assertEqual(
|
||||
self.export("foobar"),
|
||||
[[u"foobar"]])
|
||||
def test_out_of_bounds(self):
|
||||
self.assertEqual(
|
||||
self.export("C for Sinking, "
|
||||
"Java for Drinking, "
|
||||
"Smalltalk for Thinking. "
|
||||
"...and Power to the Penguin!"),
|
||||
[[u"C for Sinking, J"]])
|
||||
|
||||
class test_unbound_string_field(CreatorCase):
|
||||
model_name = 'export.string'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(""),
|
||||
[[False]])
|
||||
def test_small(self):
|
||||
self.assertEqual(
|
||||
self.export("foobar"),
|
||||
[[u"foobar"]])
|
||||
def test_big(self):
|
||||
self.assertEqual(
|
||||
self.export("We flew down weekly to meet with IBM, but they "
|
||||
"thought the way to measure software was the amount "
|
||||
"of code we wrote, when really the better the "
|
||||
"software, the fewer lines of code."),
|
||||
[[u"We flew down weekly to meet with IBM, but they thought the "
|
||||
u"way to measure software was the amount of code we wrote, "
|
||||
u"when really the better the software, the fewer lines of "
|
||||
u"code."]])
|
||||
|
||||
class test_text(CreatorCase):
|
||||
model_name = 'export.text'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(""),
|
||||
[[False]])
|
||||
def test_small(self):
|
||||
self.assertEqual(
|
||||
self.export("foobar"),
|
||||
[[u"foobar"]])
|
||||
def test_big(self):
|
||||
self.assertEqual(
|
||||
self.export("So, `bind' is `let' and monadic programming is"
|
||||
" equivalent to programming in the A-normal form. That"
|
||||
" is indeed all there is to monads"),
|
||||
[[u"So, `bind' is `let' and monadic programming is equivalent to"
|
||||
u" programming in the A-normal form. That is indeed all there"
|
||||
u" is to monads"]])
|
||||
|
||||
class test_date(CreatorCase):
|
||||
model_name = 'export.date'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(False),
|
||||
[[False]])
|
||||
def test_basic(self):
|
||||
self.assertEqual(
|
||||
self.export('2011-11-07'),
|
||||
[[u'2011-11-07']])
|
||||
|
||||
class test_datetime(CreatorCase):
|
||||
model_name = 'export.datetime'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(False),
|
||||
[[False]])
|
||||
def test_basic(self):
|
||||
self.assertEqual(
|
||||
self.export('2011-11-07 21:05:48'),
|
||||
[[u'2011-11-07 21:05:48']])
|
||||
def test_tz(self):
|
||||
""" Export ignores the timezone and always exports to UTC
|
||||
|
||||
.. note:: on the other hand, export uses user lang for name_get
|
||||
"""
|
||||
# NOTE: ignores user timezone, always exports to UTC
|
||||
self.assertEqual(
|
||||
self.export('2011-11-07 21:05:48', context={'tz': 'Pacific/Norfolk'}),
|
||||
[[u'2011-11-07 21:05:48']])
|
||||
|
||||
class test_selection(CreatorCase):
|
||||
model_name = 'export.selection'
|
||||
translations_fr = [
|
||||
("Qux", "toto"),
|
||||
("Bar", "titi"),
|
||||
("Foo", "tete"),
|
||||
]
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(False),
|
||||
[[False]])
|
||||
|
||||
def test_value(self):
|
||||
""" selections export the *label* for their value
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.export(2),
|
||||
[[u"Bar"]])
|
||||
|
||||
def test_localized_export(self):
|
||||
self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'name': u'Français',
|
||||
'code': 'fr_FR',
|
||||
'translatable': True,
|
||||
'date_format': '%d.%m.%Y',
|
||||
'decimal_point': ',',
|
||||
'thousand_sep': ' ',
|
||||
})
|
||||
Translations = self.registry('ir.translation')
|
||||
for source, value in self.translations_fr:
|
||||
Translations.create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'name': 'export.selection,value',
|
||||
'lang': 'fr_FR',
|
||||
'type': 'selection',
|
||||
'src': source,
|
||||
'value': value
|
||||
})
|
||||
self.assertEqual(
|
||||
self.export(2, context={'lang': 'fr_FR'}),
|
||||
[[u'Bar']])
|
||||
|
||||
class test_selection_function(CreatorCase):
|
||||
model_name = 'export.selection.function'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(False),
|
||||
[[False]])
|
||||
|
||||
def test_value(self):
|
||||
# FIXME: selection functions export the *value* itself
|
||||
self.assertEqual(
|
||||
self.export(1),
|
||||
[[u'1']])
|
||||
self.assertEqual(
|
||||
self.export(3),
|
||||
[[u'3']])
|
||||
# fucking hell
|
||||
self.assertEqual(
|
||||
self.export(0),
|
||||
[[False]])
|
||||
|
||||
class test_m2o(CreatorCase):
|
||||
model_name = 'export.many2one'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(False),
|
||||
[[False]])
|
||||
def test_basic(self):
|
||||
""" Exported value is the name_get of the related object
|
||||
"""
|
||||
integer_id = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
name = dict(self.registry('export.integer').name_get(
|
||||
self.cr, openerp.SUPERUSER_ID,[integer_id]))[integer_id]
|
||||
self.assertEqual(
|
||||
self.export(integer_id),
|
||||
[[name]])
|
||||
def test_path(self):
|
||||
""" Can recursively export fields of m2o via path
|
||||
"""
|
||||
integer_id = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
self.assertEqual(
|
||||
self.export(integer_id, fields=['value/.id', 'value/value']),
|
||||
[[unicode(integer_id), u'42']])
|
||||
def test_external_id(self):
|
||||
integer_id = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
# __export__.$class.$id
|
||||
external_id = u'__export__.export_many2one_%d' % integer_id
|
||||
self.assertEqual(
|
||||
self.export(integer_id, fields=['value/id']),
|
||||
[[external_id]])
|
||||
|
||||
class test_o2m(CreatorCase):
|
||||
model_name = 'export.one2many'
|
||||
commands = [
|
||||
(0, False, {'value': 4, 'str': 'record1'}),
|
||||
(0, False, {'value': 42, 'str': 'record2'}),
|
||||
(0, False, {'value': 36, 'str': 'record3'}),
|
||||
(0, False, {'value': 4, 'str': 'record4'}),
|
||||
(0, False, {'value': 13, 'str': 'record5'}),
|
||||
]
|
||||
names = [
|
||||
u'export.one2many.child:%d' % d['value']
|
||||
for c, _, d in commands
|
||||
]
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(False),
|
||||
[[False]])
|
||||
|
||||
def test_single(self):
|
||||
self.assertEqual(
|
||||
self.export([(0, False, {'value': 42})]),
|
||||
# name_get result
|
||||
[[u'export.one2many.child:42']])
|
||||
|
||||
def test_single_subfield(self):
|
||||
self.assertEqual(
|
||||
self.export([(0, False, {'value': 42})],
|
||||
fields=['value', 'value/value']),
|
||||
[[u'export.one2many.child:42', u'42']])
|
||||
|
||||
def test_integrate_one_in_parent(self):
|
||||
self.assertEqual(
|
||||
self.export([(0, False, {'value': 42})],
|
||||
fields=['const', 'value/value']),
|
||||
[[u'4', u'42']])
|
||||
|
||||
def test_multiple_records(self):
|
||||
self.assertEqual(
|
||||
self.export(self.commands, fields=['const', 'value/value']),
|
||||
[
|
||||
[u'4', u'4'],
|
||||
[u'', u'42'],
|
||||
[u'', u'36'],
|
||||
[u'', u'4'],
|
||||
[u'', u'13'],
|
||||
])
|
||||
|
||||
def test_multiple_records_name(self):
|
||||
self.assertEqual(
|
||||
self.export(self.commands, fields=['const', 'value']),
|
||||
[[
|
||||
u'4', u','.join(self.names)
|
||||
]])
|
||||
|
||||
def test_multiple_records_id(self):
|
||||
export = self.export(self.commands, fields=['const', 'value/.id'])
|
||||
O2M_c = self.registry('export.one2many.child')
|
||||
ids = O2M_c.browse(self.cr, openerp.SUPERUSER_ID,
|
||||
O2M_c.search(self.cr, openerp.SUPERUSER_ID, []))
|
||||
self.assertEqual(
|
||||
export,
|
||||
[
|
||||
['4', str(ids[0].id)],
|
||||
['', str(ids[1].id)],
|
||||
['', str(ids[2].id)],
|
||||
['', str(ids[3].id)],
|
||||
['', str(ids[4].id)],
|
||||
])
|
||||
|
||||
def test_multiple_records_with_name_before(self):
|
||||
self.assertEqual(
|
||||
self.export(self.commands, fields=['const', 'value', 'value/value']),
|
||||
[[ # exports sub-fields of very first o2m
|
||||
u'4', u','.join(self.names), u'4'
|
||||
]])
|
||||
|
||||
def test_multiple_records_with_name_after(self):
|
||||
self.assertEqual(
|
||||
self.export(self.commands, fields=['const', 'value/value', 'value']),
|
||||
[ # completely ignores name_get request
|
||||
[u'4', u'4', ''],
|
||||
['', u'42', ''],
|
||||
['', u'36', ''],
|
||||
['', u'4', ''],
|
||||
['', u'13', ''],
|
||||
])
|
||||
|
||||
def test_multiple_subfields_neighbour(self):
|
||||
self.assertEqual(
|
||||
self.export(self.commands, fields=['const', 'value/str','value/value']),
|
||||
[
|
||||
[u'4', u'record1', u'4'],
|
||||
['', u'record2', u'42'],
|
||||
['', u'record3', u'36'],
|
||||
['', u'record4', u'4'],
|
||||
['', u'record5', u'13'],
|
||||
])
|
||||
|
||||
def test_multiple_subfields_separated(self):
|
||||
self.assertEqual(
|
||||
self.export(self.commands, fields=['value/str', 'const', 'value/value']),
|
||||
[
|
||||
[u'record1', u'4', u'4'],
|
||||
[u'record2', '', u'42'],
|
||||
[u'record3', '', u'36'],
|
||||
[u'record4', '', u'4'],
|
||||
[u'record5', '', u'13'],
|
||||
])
|
||||
|
||||
class test_o2m_multiple(CreatorCase):
|
||||
model_name = 'export.one2many.multiple'
|
||||
|
||||
def make(self, value=None, **values):
|
||||
if value is not None: values['value'] = value
|
||||
id = self.model.create(self.cr, openerp.SUPERUSER_ID, values)
|
||||
return self.model.browse(self.cr, openerp.SUPERUSER_ID, [id])[0]
|
||||
def export(self, value=None, fields=('child1', 'child2',), context=None, **values):
|
||||
record = self.make(value, **values)
|
||||
return self.model._BaseModel__export_row(
|
||||
self.cr, openerp.SUPERUSER_ID, record,
|
||||
[f.split('/') for f in fields],
|
||||
context=context)
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(child1=False, child2=False),
|
||||
[[False, False]])
|
||||
|
||||
def test_single_per_side(self):
|
||||
self.assertEqual(
|
||||
self.export(child1=False, child2=[(0, False, {'value': 42})]),
|
||||
[[False, u'export.one2many.child.2:42']])
|
||||
|
||||
self.assertEqual(
|
||||
self.export(child1=[(0, False, {'value': 43})], child2=False),
|
||||
[[u'export.one2many.child.1:43', False]])
|
||||
|
||||
self.assertEqual(
|
||||
self.export(child1=[(0, False, {'value': 43})],
|
||||
child2=[(0, False, {'value': 42})]),
|
||||
[[u'export.one2many.child.1:43', u'export.one2many.child.2:42']])
|
||||
|
||||
def test_single_integrate_subfield(self):
|
||||
fields = ['const', 'child1/value', 'child2/value']
|
||||
self.assertEqual(
|
||||
self.export(child1=False, child2=[(0, False, {'value': 42})],
|
||||
fields=fields),
|
||||
[[u'36', False, u'42']])
|
||||
|
||||
self.assertEqual(
|
||||
self.export(child1=[(0, False, {'value': 43})], child2=False,
|
||||
fields=fields),
|
||||
[[u'36', u'43', False]])
|
||||
|
||||
self.assertEqual(
|
||||
self.export(child1=[(0, False, {'value': 43})],
|
||||
child2=[(0, False, {'value': 42})],
|
||||
fields=fields),
|
||||
[[u'36', u'43', u'42']])
|
||||
|
||||
def test_multiple(self):
|
||||
""" With two "concurrent" o2ms, exports the first line combined, then
|
||||
exports the rows for the first o2m, then the rows for the second o2m.
|
||||
"""
|
||||
fields = ['const', 'child1/value', 'child2/value']
|
||||
child1 = [(0, False, {'value': v, 'str': 'record%.02d' % index})
|
||||
for index, v in zip(itertools.count(), [4, 42, 36, 4, 13])]
|
||||
child2 = [(0, False, {'value': v, 'str': 'record%.02d' % index})
|
||||
for index, v in zip(itertools.count(10), [8, 12, 8, 55, 33, 13])]
|
||||
|
||||
self.assertEqual(
|
||||
self.export(child1=child1, child2=False, fields=fields),
|
||||
[
|
||||
[u'36', u'4', False],
|
||||
['', u'42', ''],
|
||||
['', u'36', ''],
|
||||
['', u'4', ''],
|
||||
['', u'13', ''],
|
||||
])
|
||||
self.assertEqual(
|
||||
self.export(child1=False, child2=child2, fields=fields),
|
||||
[
|
||||
[u'36', False, u'8'],
|
||||
['', '', u'12'],
|
||||
['', '', u'8'],
|
||||
['', '', u'55'],
|
||||
['', '', u'33'],
|
||||
['', '', u'13'],
|
||||
])
|
||||
self.assertEqual(
|
||||
self.export(child1=child1, child2=child2, fields=fields),
|
||||
[
|
||||
[u'36', u'4', u'8'],
|
||||
['', u'42', ''],
|
||||
['', u'36', ''],
|
||||
['', u'4', ''],
|
||||
['', u'13', ''],
|
||||
['', '', u'12'],
|
||||
['', '', u'8'],
|
||||
['', '', u'55'],
|
||||
['', '', u'33'],
|
||||
['', '', u'13'],
|
||||
])
|
||||
|
||||
class test_m2m(CreatorCase):
|
||||
model_name = 'export.many2many'
|
||||
commands = [
|
||||
(0, False, {'value': 4, 'str': 'record000'}),
|
||||
(0, False, {'value': 42, 'str': 'record001'}),
|
||||
(0, False, {'value': 36, 'str': 'record010'}),
|
||||
(0, False, {'value': 4, 'str': 'record011'}),
|
||||
(0, False, {'value': 13, 'str': 'record100'}),
|
||||
]
|
||||
names = [
|
||||
u'export.many2many.other:%d' % d['value']
|
||||
for c, _, d in commands
|
||||
]
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.export(False),
|
||||
[[False]])
|
||||
|
||||
def test_single(self):
|
||||
self.assertEqual(
|
||||
self.export([(0, False, {'value': 42})]),
|
||||
# name_get result
|
||||
[[u'export.many2many.other:42']])
|
||||
|
||||
def test_single_subfield(self):
|
||||
self.assertEqual(
|
||||
self.export([(0, False, {'value': 42})],
|
||||
fields=['value', 'value/value']),
|
||||
[[u'export.many2many.other:42', u'42']])
|
||||
|
||||
def test_integrate_one_in_parent(self):
|
||||
self.assertEqual(
|
||||
self.export([(0, False, {'value': 42})],
|
||||
fields=['const', 'value/value']),
|
||||
[[u'4', u'42']])
|
||||
|
||||
def test_multiple_records(self):
|
||||
self.assertEqual(
|
||||
self.export(self.commands, fields=['const', 'value/value']),
|
||||
[
|
||||
[u'4', u'4'],
|
||||
[u'', u'42'],
|
||||
[u'', u'36'],
|
||||
[u'', u'4'],
|
||||
[u'', u'13'],
|
||||
])
|
||||
|
||||
def test_multiple_records_name(self):
|
||||
self.assertEqual(
|
||||
self.export(self.commands, fields=['const', 'value']),
|
||||
[[ # FIXME: hardcoded comma, import uses config.csv_internal_sep
|
||||
# resolution: remove configurable csv_internal_sep
|
||||
u'4', u','.join(self.names)
|
||||
]])
|
||||
|
||||
# essentially same as o2m, so boring
|
||||
|
||||
class test_function(CreatorCase):
|
||||
model_name = 'export.function'
|
||||
|
||||
def test_value(self):
|
||||
""" Exports value normally returned by accessing the function field
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.export(42),
|
||||
[[u'3']])
|
||||
@@ -0,0 +1,906 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import openerp.modules.registry
|
||||
import openerp
|
||||
|
||||
from openerp.tests import common
|
||||
|
||||
def ok(n):
|
||||
""" Successful import of ``n`` records
|
||||
|
||||
:param int n: number of records which should have been imported
|
||||
"""
|
||||
return n, 0, 0, 0
|
||||
|
||||
def error(row, message, record=None, **kwargs):
|
||||
""" Failed import of the record ``record`` at line ``row``, with the error
|
||||
message ``message``
|
||||
|
||||
:param str message:
|
||||
:param dict record:
|
||||
"""
|
||||
return (
|
||||
-1, dict(record or {}, **kwargs),
|
||||
"Line %d : %s" % (row, message),
|
||||
'')
|
||||
|
||||
def values(seq, field='value'):
|
||||
return [item[field] for item in seq]
|
||||
|
||||
class ImporterCase(common.TransactionCase):
|
||||
model_name = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ImporterCase, self).__init__(*args, **kwargs)
|
||||
self.model = None
|
||||
|
||||
def setUp(self):
|
||||
super(ImporterCase, self).setUp()
|
||||
self.model = self.registry(self.model_name)
|
||||
|
||||
def import_(self, fields, rows, context=None):
|
||||
return self.model.import_data(
|
||||
self.cr, openerp.SUPERUSER_ID, fields, rows, context=context)
|
||||
def read(self, fields=('value',), domain=(), context=None):
|
||||
return self.model.read(
|
||||
self.cr, openerp.SUPERUSER_ID,
|
||||
self.model.search(self.cr, openerp.SUPERUSER_ID, domain, context=context),
|
||||
fields=fields, context=context)
|
||||
def browse(self, domain=(), context=None):
|
||||
return self.model.browse(
|
||||
self.cr, openerp.SUPERUSER_ID,
|
||||
self.model.search(self.cr, openerp.SUPERUSER_ID, domain, context=context),
|
||||
context=context)
|
||||
|
||||
def xid(self, record):
|
||||
ModelData = self.registry('ir.model.data')
|
||||
|
||||
ids = ModelData.search(
|
||||
self.cr, openerp.SUPERUSER_ID,
|
||||
[('model', '=', record._table_name), ('res_id', '=', record.id)])
|
||||
if ids:
|
||||
d = ModelData.read(
|
||||
self.cr, openerp.SUPERUSER_ID, ids, ['name', 'module'])[0]
|
||||
if d['module']:
|
||||
return '%s.%s' % (d['module'], d['name'])
|
||||
return d['name']
|
||||
|
||||
name = dict(record.name_get())[record.id]
|
||||
# fix dotted name_get results, otherwise xid lookups blow up
|
||||
name = name.replace('.', '-')
|
||||
ModelData.create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'name': name,
|
||||
'model': record._table_name,
|
||||
'res_id': record.id,
|
||||
'module': '__test__'
|
||||
})
|
||||
return '__test__.' + name
|
||||
|
||||
class test_ids_stuff(ImporterCase):
|
||||
model_name = 'export.integer'
|
||||
|
||||
def test_create_with_id(self):
|
||||
self.assertRaises(
|
||||
Exception, # dammit
|
||||
self.import_, ['.id', 'value'], [['42', '36']])
|
||||
def test_create_with_xid(self):
|
||||
self.assertEqual(
|
||||
self.import_(['id', 'value'], [['somexmlid', '42']]),
|
||||
ok(1))
|
||||
self.assertEqual(
|
||||
'somexmlid',
|
||||
self.xid(self.browse()[0]))
|
||||
|
||||
def test_update_with_id(self):
|
||||
id = self.model.create(self.cr, openerp.SUPERUSER_ID, {'value': 36})
|
||||
self.assertEqual(
|
||||
36,
|
||||
self.model.browse(self.cr, openerp.SUPERUSER_ID, id).value)
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['.id', 'value'], [[str(id), '42']]),
|
||||
ok(1))
|
||||
self.assertEqual(
|
||||
[42], # updated value to imported
|
||||
values(self.read()))
|
||||
|
||||
def test_update_with_xid(self):
|
||||
self.import_(['id', 'value'], [['somexmlid', '36']])
|
||||
self.assertEqual([36], values(self.read()))
|
||||
|
||||
self.import_(['id', 'value'], [['somexmlid', '1234567']])
|
||||
self.assertEqual([1234567], values(self.read()))
|
||||
|
||||
class test_boolean_field(ImporterCase):
|
||||
model_name = 'export.boolean'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], []),
|
||||
ok(0))
|
||||
|
||||
def test_exported(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['False'],
|
||||
['True'],
|
||||
]),
|
||||
ok(2))
|
||||
records = self.read()
|
||||
self.assertEqual([
|
||||
False,
|
||||
True,
|
||||
], values(records))
|
||||
|
||||
def test_falses(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
[u'0'],
|
||||
[u'off'],
|
||||
[u'false'],
|
||||
[u'FALSE'],
|
||||
[u'OFF'],
|
||||
[u''],
|
||||
]),
|
||||
ok(6))
|
||||
self.assertEqual([
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
],
|
||||
values(self.read()))
|
||||
|
||||
def test_trues(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['no'],
|
||||
['None'],
|
||||
['nil'],
|
||||
['()'],
|
||||
['f'],
|
||||
['#f'],
|
||||
# Problem: OpenOffice (and probably excel) output localized booleans
|
||||
['VRAI'],
|
||||
]),
|
||||
ok(7))
|
||||
self.assertEqual(
|
||||
[True] * 7,
|
||||
values(self.read()))
|
||||
|
||||
class test_integer_field(ImporterCase):
|
||||
model_name = 'export.integer'
|
||||
|
||||
def test_none(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], []),
|
||||
ok(0))
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['']]),
|
||||
ok(1))
|
||||
self.assertEqual(
|
||||
[False],
|
||||
values(self.read()))
|
||||
|
||||
def test_zero(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['0']]),
|
||||
ok(1))
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['-0']]),
|
||||
ok(1))
|
||||
self.assertEqual([False, False], values(self.read()))
|
||||
|
||||
def test_positives(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['1'],
|
||||
['42'],
|
||||
[str(2**31-1)],
|
||||
['12345678']
|
||||
]),
|
||||
ok(4))
|
||||
self.assertEqual([
|
||||
1, 42, 2**31-1, 12345678
|
||||
], values(self.read()))
|
||||
|
||||
def test_negatives(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['-1'],
|
||||
['-42'],
|
||||
[str(-(2**31 - 1))],
|
||||
[str(-(2**31))],
|
||||
['-12345678']
|
||||
]),
|
||||
ok(5))
|
||||
self.assertEqual([
|
||||
-1, -42, -(2**31 - 1), -(2**31), -12345678
|
||||
], values(self.read()))
|
||||
|
||||
def test_out_of_range(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [[str(2**31)]]),
|
||||
error(1, "integer out of range\n", value=2**31))
|
||||
# auto-rollbacks if error is in process_liness, but not during
|
||||
# ir.model.data write. Can differentiate because former ends lines
|
||||
# error lines with "!"
|
||||
self.cr.rollback()
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [[str(-2**32)]]),
|
||||
error(1, "integer out of range\n", value=-2**32))
|
||||
|
||||
|
||||
def test_nonsense(self):
|
||||
# FIXME: shit error reporting, exceptions half the time, messages the other half
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.import_, ['value'], [['zorglub']])
|
||||
|
||||
class test_float_field(ImporterCase):
|
||||
model_name = 'export.float'
|
||||
def test_none(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], []),
|
||||
ok(0))
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['']]),
|
||||
ok(1))
|
||||
self.assertEqual(
|
||||
[False],
|
||||
values(self.read()))
|
||||
|
||||
def test_zero(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['0']]),
|
||||
ok(1))
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['-0']]),
|
||||
ok(1))
|
||||
self.assertEqual([False, False], values(self.read()))
|
||||
|
||||
def test_positives(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['1'],
|
||||
['42'],
|
||||
[str(2**31-1)],
|
||||
['12345678'],
|
||||
[str(2**33)],
|
||||
['0.000001'],
|
||||
]),
|
||||
ok(6))
|
||||
self.assertEqual([
|
||||
1, 42, 2**31-1, 12345678, 2.0**33, .000001
|
||||
], values(self.read()))
|
||||
|
||||
def test_negatives(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['-1'],
|
||||
['-42'],
|
||||
[str(-2**31 + 1)],
|
||||
[str(-2**31)],
|
||||
['-12345678'],
|
||||
[str(-2**33)],
|
||||
['-0.000001'],
|
||||
]),
|
||||
ok(7))
|
||||
self.assertEqual([
|
||||
-1, -42, -(2**31 - 1), -(2**31), -12345678, -2.0**33, -.000001
|
||||
], values(self.read()))
|
||||
|
||||
def test_nonsense(self):
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.import_, ['value'], [['foobar']])
|
||||
|
||||
class test_string_field(ImporterCase):
|
||||
model_name = 'export.string.bounded'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['']]),
|
||||
ok(1))
|
||||
self.assertEqual([False], values(self.read()))
|
||||
|
||||
def test_imported(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
[u'foobar'],
|
||||
[u'foobarbaz'],
|
||||
[u'Með suð í eyrum við spilum endalaust'],
|
||||
[u"People 'get' types. They use them all the time. Telling "
|
||||
u"someone he can't pound a nail with a banana doesn't much "
|
||||
u"surprise him."]
|
||||
]),
|
||||
ok(4))
|
||||
self.assertEqual([
|
||||
u"foobar",
|
||||
u"foobarbaz",
|
||||
u"Með suð í eyrum ",
|
||||
u"People 'get' typ",
|
||||
], values(self.read()))
|
||||
|
||||
class test_unbound_string_field(ImporterCase):
|
||||
model_name = 'export.string'
|
||||
|
||||
def test_imported(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
[u'í dag viðrar vel til loftárása'],
|
||||
# ackbar.jpg
|
||||
[u"If they ask you about fun, you tell them – fun is a filthy"
|
||||
u" parasite"]
|
||||
]),
|
||||
ok(2))
|
||||
self.assertEqual([
|
||||
u"í dag viðrar vel til loftárása",
|
||||
u"If they ask you about fun, you tell them – fun is a filthy parasite"
|
||||
], values(self.read()))
|
||||
|
||||
class test_text(ImporterCase):
|
||||
model_name = 'export.text'
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['']]),
|
||||
ok(1))
|
||||
self.assertEqual([False], values(self.read()))
|
||||
|
||||
def test_imported(self):
|
||||
s = (u"Breiðskífa er notað um útgefna hljómplötu sem inniheldur "
|
||||
u"stúdíóupptökur frá einum flytjanda. Breiðskífur eru oftast "
|
||||
u"milli 25-80 mínútur og er lengd þeirra oft miðuð við 33⅓ "
|
||||
u"snúninga 12 tommu vínylplötur (sem geta verið allt að 30 mín "
|
||||
u"hvor hlið).\n\nBreiðskífur eru stundum tvöfaldar og eru þær þá"
|
||||
u" gefnar út á tveimur geisladiskum eða tveimur vínylplötum.")
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [[s]]),
|
||||
ok(1))
|
||||
self.assertEqual([s], values(self.read()))
|
||||
|
||||
class test_selection(ImporterCase):
|
||||
model_name = 'export.selection'
|
||||
translations_fr = [
|
||||
("Qux", "toto"),
|
||||
("Bar", "titi"),
|
||||
("Foo", "tete"),
|
||||
]
|
||||
|
||||
def test_imported(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['Qux'],
|
||||
['Bar'],
|
||||
['Foo'],
|
||||
['2'],
|
||||
]),
|
||||
ok(4))
|
||||
self.assertEqual([3, 2, 1, 2], values(self.read()))
|
||||
|
||||
def test_imported_translated(self):
|
||||
self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'name': u'Français',
|
||||
'code': 'fr_FR',
|
||||
'translatable': True,
|
||||
'date_format': '%d.%m.%Y',
|
||||
'decimal_point': ',',
|
||||
'thousand_sep': ' ',
|
||||
})
|
||||
Translations = self.registry('ir.translation')
|
||||
for source, value in self.translations_fr:
|
||||
Translations.create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'name': 'export.selection,value',
|
||||
'lang': 'fr_FR',
|
||||
'type': 'selection',
|
||||
'src': source,
|
||||
'value': value
|
||||
})
|
||||
|
||||
# FIXME: can't import an exported selection field label if lang != en_US
|
||||
# (see test_export.test_selection.test_localized_export)
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['toto'],
|
||||
['tete'],
|
||||
['titi'],
|
||||
], context={'lang': 'fr_FR'}),
|
||||
ok(3))
|
||||
self.assertEqual([3, 1, 2], values(self.read()))
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['Foo']], context={'lang': 'fr_FR'}),
|
||||
error(1, "Key/value 'Foo' not found in selection field 'value'",
|
||||
value=False))
|
||||
|
||||
def test_invalid(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['Baz']]),
|
||||
error(1, "Key/value 'Baz' not found in selection field 'value'",
|
||||
# what the fuck?
|
||||
value=False))
|
||||
self.cr.rollback()
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [[42]]),
|
||||
error(1, "Key/value '42' not found in selection field 'value'",
|
||||
value=False))
|
||||
|
||||
class test_selection_function(ImporterCase):
|
||||
model_name = 'export.selection.function'
|
||||
translations_fr = [
|
||||
("Corge", "toto"),
|
||||
("Grault", "titi"),
|
||||
("Whee", "tete"),
|
||||
("Moog", "tutu"),
|
||||
]
|
||||
|
||||
def test_imported(self):
|
||||
""" import uses fields_get, so translates import label (may or may not
|
||||
be good news) *and* serializes the selection function to reverse it:
|
||||
import does not actually know that the selection field uses a function
|
||||
"""
|
||||
# NOTE: conflict between a value and a label => ?
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['3'],
|
||||
["Grault"],
|
||||
]),
|
||||
ok(2))
|
||||
self.assertEqual(
|
||||
['3', '1'],
|
||||
values(self.read()))
|
||||
|
||||
def test_translated(self):
|
||||
""" Expects output of selection function returns translated labels
|
||||
"""
|
||||
self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'name': u'Français',
|
||||
'code': 'fr_FR',
|
||||
'translatable': True,
|
||||
'date_format': '%d.%m.%Y',
|
||||
'decimal_point': ',',
|
||||
'thousand_sep': ' ',
|
||||
})
|
||||
Translations = self.registry('ir.translation')
|
||||
for source, value in self.translations_fr:
|
||||
Translations.create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'name': 'export.selection,value',
|
||||
'lang': 'fr_FR',
|
||||
'type': 'selection',
|
||||
'src': source,
|
||||
'value': value
|
||||
})
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['toto'],
|
||||
['tete'],
|
||||
], context={'lang': 'fr_FR'}),
|
||||
error(1, "Key/value 'toto' not found in selection field 'value'",
|
||||
value=False))
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}),
|
||||
ok(1))
|
||||
|
||||
class test_m2o(ImporterCase):
|
||||
model_name = 'export.many2one'
|
||||
|
||||
def test_by_name(self):
|
||||
# create integer objects
|
||||
integer_id1 = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
integer_id2 = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 36})
|
||||
# get its name
|
||||
name1 = dict(self.registry('export.integer').name_get(
|
||||
self.cr, openerp.SUPERUSER_ID,[integer_id1]))[integer_id1]
|
||||
name2 = dict(self.registry('export.integer').name_get(
|
||||
self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2]
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
# import by name_get
|
||||
[name1],
|
||||
[name1],
|
||||
[name2],
|
||||
]),
|
||||
ok(3))
|
||||
# correct ids assigned to corresponding records
|
||||
self.assertEqual([
|
||||
(integer_id1, name1),
|
||||
(integer_id1, name1),
|
||||
(integer_id2, name2),],
|
||||
values(self.read()))
|
||||
|
||||
def test_by_xid(self):
|
||||
ExportInteger = self.registry('export.integer')
|
||||
integer_id = ExportInteger.create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
xid = self.xid(ExportInteger.browse(
|
||||
self.cr, openerp.SUPERUSER_ID, [integer_id])[0])
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['value/id'], [[xid]]),
|
||||
ok(1))
|
||||
b = self.browse()
|
||||
self.assertEqual(42, b[0].value.value)
|
||||
|
||||
def test_by_id(self):
|
||||
integer_id = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
self.assertEqual(
|
||||
self.import_(['value/.id'], [[integer_id]]),
|
||||
ok(1))
|
||||
b = self.browse()
|
||||
self.assertEqual(42, b[0].value.value)
|
||||
|
||||
def test_by_names(self):
|
||||
integer_id1 = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
integer_id2 = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
name1 = dict(self.registry('export.integer').name_get(
|
||||
self.cr, openerp.SUPERUSER_ID,[integer_id1]))[integer_id1]
|
||||
name2 = dict(self.registry('export.integer').name_get(
|
||||
self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2]
|
||||
# names should be the same
|
||||
self.assertEqual(name1, name2)
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [[name2]]),
|
||||
ok(1))
|
||||
# FIXME: is it really normal import does not care for name_search collisions?
|
||||
self.assertEqual([
|
||||
(integer_id1, name1)
|
||||
], values(self.read()))
|
||||
|
||||
def test_fail_by_implicit_id(self):
|
||||
""" Can't implicitly import records by id
|
||||
"""
|
||||
# create integer objects
|
||||
integer_id1 = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 42})
|
||||
integer_id2 = self.registry('export.integer').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 36})
|
||||
|
||||
self.assertRaises(
|
||||
ValueError, # Because name_search all the things. Fallback schmallback
|
||||
self.import_, ['value'], [
|
||||
# import by id, without specifying it
|
||||
[integer_id1],
|
||||
[integer_id2],
|
||||
[integer_id1],
|
||||
])
|
||||
|
||||
def test_sub_field(self):
|
||||
""" Does not implicitly create the record, does not warn that you can't
|
||||
import m2o subfields (at all)...
|
||||
"""
|
||||
self.assertRaises(
|
||||
ValueError, # No record found for 42, name_searches the bloody thing
|
||||
self.import_, ['value/value'], [['42']])
|
||||
|
||||
def test_fail_noids(self):
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.import_, ['value'], [['nameisnoexist:3']])
|
||||
self.cr.rollback()
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.import_, ['value/id'], [['noxidhere']]),
|
||||
self.cr.rollback()
|
||||
self.assertRaises(
|
||||
Exception, # FIXME: Why can't you be a ValueError like everybody else?
|
||||
self.import_, ['value/.id'], [[66]])
|
||||
|
||||
class test_m2m(ImporterCase):
|
||||
model_name = 'export.many2many'
|
||||
|
||||
# apparently, one and only thing which works is a
|
||||
# csv_internal_sep-separated list of ids, xids, or names (depending if
|
||||
# m2m/.id, m2m/id or m2m[/anythingelse]
|
||||
def test_ids(self):
|
||||
id1 = self.registry('export.many2many.other').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'})
|
||||
id2 = self.registry('export.many2many.other').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'})
|
||||
id3 = self.registry('export.many2many.other').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'})
|
||||
id4 = self.registry('export.many2many.other').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'})
|
||||
id5 = self.registry('export.many2many.other').create(
|
||||
self.cr, openerp.SUPERUSER_ID, {'value': 99, 'str': 'record4'})
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['value/.id'], [
|
||||
['%d,%d' % (id1, id2)],
|
||||
['%d,%d,%d' % (id1, id3, id4)],
|
||||
['%d,%d,%d' % (id1, id2, id3)],
|
||||
['%d' % id5]
|
||||
]),
|
||||
ok(4))
|
||||
ids = lambda records: [record.id for record in records]
|
||||
|
||||
b = self.browse()
|
||||
self.assertEqual(ids(b[0].value), [id1, id2])
|
||||
self.assertEqual(values(b[0].value), [3, 44])
|
||||
|
||||
self.assertEqual(ids(b[2].value), [id1, id2, id3])
|
||||
self.assertEqual(values(b[2].value), [3, 44, 84])
|
||||
|
||||
def test_noids(self):
|
||||
try:
|
||||
self.import_(['value/.id'], [['42']])
|
||||
self.fail("Should have raised an exception")
|
||||
except Exception, e:
|
||||
self.assertIs(type(e), Exception,
|
||||
"test should be fixed on exception subclass")
|
||||
|
||||
def test_xids(self):
|
||||
M2O_o = self.registry('export.many2many.other')
|
||||
id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'})
|
||||
id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'})
|
||||
id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'})
|
||||
id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'})
|
||||
records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4])
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['value/id'], [
|
||||
['%s,%s' % (self.xid(records[0]), self.xid(records[1]))],
|
||||
['%s' % self.xid(records[3])],
|
||||
['%s,%s' % (self.xid(records[2]), self.xid(records[1]))],
|
||||
]),
|
||||
ok(3))
|
||||
|
||||
b = self.browse()
|
||||
self.assertEqual(values(b[0].value), [3, 44])
|
||||
self.assertEqual(values(b[2].value), [44, 84])
|
||||
def test_noxids(self):
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.import_, ['value/id'], [['noxidforthat']])
|
||||
|
||||
def test_names(self):
|
||||
M2O_o = self.registry('export.many2many.other')
|
||||
id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'})
|
||||
id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'})
|
||||
id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'})
|
||||
id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'})
|
||||
records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4])
|
||||
|
||||
name = lambda record: dict(record.name_get())[record.id]
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['value'], [
|
||||
['%s,%s' % (name(records[1]), name(records[2]))],
|
||||
['%s,%s,%s' % (name(records[0]), name(records[1]), name(records[2]))],
|
||||
['%s,%s' % (name(records[0]), name(records[3]))],
|
||||
]),
|
||||
ok(3))
|
||||
|
||||
b = self.browse()
|
||||
self.assertEqual(values(b[1].value), [3, 44, 84])
|
||||
self.assertEqual(values(b[2].value), [3, 9])
|
||||
|
||||
def test_nonames(self):
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self.import_, ['value'], [['wherethem2mhavenonames']])
|
||||
|
||||
def test_import_to_existing(self):
|
||||
M2O_o = self.registry('export.many2many.other')
|
||||
id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'})
|
||||
id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'})
|
||||
id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'})
|
||||
id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'})
|
||||
|
||||
xid = 'myxid'
|
||||
self.assertEqual(
|
||||
self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id1, id2)]]),
|
||||
ok(1))
|
||||
self.assertEqual(
|
||||
self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id3, id4)]]),
|
||||
ok(1))
|
||||
|
||||
b = self.browse()
|
||||
self.assertEqual(len(b), 1)
|
||||
# TODO: replacement of existing m2m values is correct?
|
||||
self.assertEqual(values(b[0].value), [84, 9])
|
||||
|
||||
class test_o2m(ImporterCase):
|
||||
model_name = 'export.one2many'
|
||||
|
||||
def test_single(self):
|
||||
self.assertEqual(
|
||||
self.import_(['const', 'value/value'], [
|
||||
['5', '63']
|
||||
]),
|
||||
ok(1))
|
||||
|
||||
(b,) = self.browse()
|
||||
self.assertEqual(b.const, 5)
|
||||
self.assertEqual(values(b.value), [63])
|
||||
|
||||
def test_multicore(self):
|
||||
self.assertEqual(
|
||||
self.import_(['const', 'value/value'], [
|
||||
['5', '63'],
|
||||
['6', '64'],
|
||||
]),
|
||||
ok(2))
|
||||
|
||||
b1, b2 = self.browse()
|
||||
self.assertEqual(b1.const, 5)
|
||||
self.assertEqual(values(b1.value), [63])
|
||||
self.assertEqual(b2.const, 6)
|
||||
self.assertEqual(values(b2.value), [64])
|
||||
|
||||
def test_multisub(self):
|
||||
self.assertEqual(
|
||||
self.import_(['const', 'value/value'], [
|
||||
['5', '63'],
|
||||
['', '64'],
|
||||
['', '65'],
|
||||
['', '66'],
|
||||
]),
|
||||
ok(4))
|
||||
|
||||
(b,) = self.browse()
|
||||
self.assertEqual(values(b.value), [63, 64, 65, 66])
|
||||
|
||||
def test_multi_subfields(self):
|
||||
self.assertEqual(
|
||||
self.import_(['value/str', 'const', 'value/value'], [
|
||||
['this', '5', '63'],
|
||||
['is', '', '64'],
|
||||
['the', '', '65'],
|
||||
['rhythm', '', '66'],
|
||||
]),
|
||||
ok(4))
|
||||
|
||||
(b,) = self.browse()
|
||||
self.assertEqual(values(b.value), [63, 64, 65, 66])
|
||||
self.assertEqual(
|
||||
values(b.value, 'str'),
|
||||
'this is the rhythm'.split())
|
||||
|
||||
def test_link_inline(self):
|
||||
id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'str': 'Bf', 'value': 109
|
||||
})
|
||||
id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'str': 'Me', 'value': 262
|
||||
})
|
||||
|
||||
try:
|
||||
self.import_(['const', 'value/.id'], [
|
||||
['42', '%d,%d' % (id1, id2)]
|
||||
])
|
||||
except ValueError, e:
|
||||
# should be Exception(Database ID doesn't exist: export.one2many.child : $id1,$id2)
|
||||
self.assertIs(type(e), ValueError)
|
||||
self.assertEqual(
|
||||
e.args[0],
|
||||
"invalid literal for int() with base 10: '%d,%d'" % (id1, id2))
|
||||
|
||||
def test_link(self):
|
||||
id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'str': 'Bf', 'value': 109
|
||||
})
|
||||
id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'str': 'Me', 'value': 262
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['const', 'value/.id'], [
|
||||
['42', str(id1)],
|
||||
['', str(id2)],
|
||||
]),
|
||||
ok(2))
|
||||
|
||||
# No record values alongside id => o2m resolution skipped altogether,
|
||||
# creates 2 records => remove/don't import columns sideshow columns,
|
||||
# get completely different semantics
|
||||
b, b1 = self.browse()
|
||||
self.assertEqual(b.const, 42)
|
||||
self.assertEqual(values(b.value), [])
|
||||
self.assertEqual(b1.const, 4)
|
||||
self.assertEqual(values(b1.value), [])
|
||||
|
||||
def test_link_2(self):
|
||||
O2M_c = self.registry('export.one2many.child')
|
||||
id1 = O2M_c.create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'str': 'Bf', 'value': 109
|
||||
})
|
||||
id2 = O2M_c.create(self.cr, openerp.SUPERUSER_ID, {
|
||||
'str': 'Me', 'value': 262
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
self.import_(['const', 'value/.id', 'value/value'], [
|
||||
['42', str(id1), '1'],
|
||||
['', str(id2), '2'],
|
||||
]),
|
||||
ok(2))
|
||||
|
||||
(b,) = self.browse()
|
||||
# if an id (db or xid) is provided, expectations that objects are
|
||||
# *already* linked and emits UPDATE (1, id, {}).
|
||||
# Noid => CREATE (0, ?, {})
|
||||
# TODO: xid ignored aside from getting corresponding db id?
|
||||
self.assertEqual(b.const, 42)
|
||||
self.assertEqual(values(b.value), [])
|
||||
|
||||
# FIXME: updates somebody else's records?
|
||||
self.assertEqual(
|
||||
O2M_c.read(self.cr, openerp.SUPERUSER_ID, id1),
|
||||
{'id': id1, 'str': 'Bf', 'value': 1, 'parent_id': False})
|
||||
self.assertEqual(
|
||||
O2M_c.read(self.cr, openerp.SUPERUSER_ID, id2),
|
||||
{'id': id2, 'str': 'Me', 'value': 2, 'parent_id': False})
|
||||
|
||||
class test_o2m_multiple(ImporterCase):
|
||||
model_name = 'export.one2many.multiple'
|
||||
|
||||
def test_multi_mixed(self):
|
||||
self.assertEqual(
|
||||
self.import_(['const', 'child1/value', 'child2/value'], [
|
||||
['5', '11', '21'],
|
||||
['', '12', '22'],
|
||||
['', '13', '23'],
|
||||
['', '14', ''],
|
||||
]),
|
||||
ok(4))
|
||||
# Oh yeah, that's the stuff
|
||||
(b, b1, b2) = self.browse()
|
||||
self.assertEqual(values(b.child1), [11])
|
||||
self.assertEqual(values(b.child2), [21])
|
||||
|
||||
self.assertEqual(values(b1.child1), [12])
|
||||
self.assertEqual(values(b1.child2), [22])
|
||||
|
||||
self.assertEqual(values(b2.child1), [13, 14])
|
||||
self.assertEqual(values(b2.child2), [23])
|
||||
|
||||
def test_multi(self):
|
||||
self.assertEqual(
|
||||
self.import_(['const', 'child1/value', 'child2/value'], [
|
||||
['5', '11', '21'],
|
||||
['', '12', ''],
|
||||
['', '13', ''],
|
||||
['', '14', ''],
|
||||
['', '', '22'],
|
||||
['', '', '23'],
|
||||
]),
|
||||
ok(6))
|
||||
# What the actual fuck?
|
||||
(b, b1) = self.browse()
|
||||
self.assertEqual(values(b.child1), [11, 12, 13, 14])
|
||||
self.assertEqual(values(b.child2), [21])
|
||||
self.assertEqual(values(b1.child2), [22, 23])
|
||||
|
||||
def test_multi_fullsplit(self):
|
||||
self.assertEqual(
|
||||
self.import_(['const', 'child1/value', 'child2/value'], [
|
||||
['5', '11', ''],
|
||||
['', '12', ''],
|
||||
['', '13', ''],
|
||||
['', '14', ''],
|
||||
['', '', '21'],
|
||||
['', '', '22'],
|
||||
['', '', '23'],
|
||||
]),
|
||||
ok(7))
|
||||
# oh wow
|
||||
(b, b1) = self.browse()
|
||||
self.assertEqual(b.const, 5)
|
||||
self.assertEqual(values(b.child1), [11, 12, 13, 14])
|
||||
self.assertEqual(b1.const, 36)
|
||||
self.assertEqual(values(b1.child2), [21, 22, 23])
|
||||
|
||||
# function, related, reference: written to db as-is...
|
||||
# => function uses @type for value coercion/conversion
|
||||
@@ -39,13 +39,16 @@ class TransactionCase(unittest2.TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.cr = openerp.modules.registry.RegistryManager.get(DB).db.cursor()
|
||||
self.cr = self.cursor()
|
||||
self.uid = openerp.SUPERUSER_ID
|
||||
|
||||
def tearDown(self):
|
||||
self.cr.rollback()
|
||||
self.cr.close()
|
||||
|
||||
def cursor(self):
|
||||
return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
|
||||
|
||||
def registry(self, model):
|
||||
return openerp.modules.registry.RegistryManager.get(DB)[model]
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from misc import *
|
||||
from convert import *
|
||||
from translate import *
|
||||
from graph import graph
|
||||
from image import *
|
||||
from amount_to_text import *
|
||||
from amount_to_text_en import *
|
||||
from pdf_utils import *
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2012-today OpenERP s.a. (<http://openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import io
|
||||
from PIL import Image
|
||||
import StringIO
|
||||
|
||||
# ----------------------------------------
|
||||
# Image resizing
|
||||
# ----------------------------------------
|
||||
|
||||
def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype='PNG', avoid_if_small=False):
|
||||
""" Function to resize an image. The image will be resized to the given
|
||||
size, while keeping the aspect ratios, and holes in the image will be
|
||||
filled with transparent background. The image will not be stretched if
|
||||
smaller than the expected size.
|
||||
Steps of the resizing:
|
||||
- if avoid_if_small: if both image sizes are smaller than the requested
|
||||
sizes, the original image is returned. This is used to avoid adding
|
||||
transparent content around images that we do not want to alter but
|
||||
just resize if too big. This is used for example when storing images
|
||||
in the 'image' field: we keep the original image, resized to a maximal
|
||||
size, without adding transparent content around it if smaller.
|
||||
- create a thumbnail of the source image through using the thumbnail
|
||||
function. Aspect ratios are preserved when using it. Note that if the
|
||||
source image is smaller than the expected size, it will not be
|
||||
extended, but filled to match the size.
|
||||
- create a transparent background that will hold the final
|
||||
image.
|
||||
- past the thumbnail on the transparent background and center
|
||||
it.
|
||||
|
||||
:param base64_source: base64-encoded version of the source
|
||||
image
|
||||
:param size: tuple(height, width)
|
||||
:param encoding: the output encoding
|
||||
:param filetype: the output filetype
|
||||
:param avoid_if_small: do not resize if image height and width
|
||||
are smaller than the expected size.
|
||||
"""
|
||||
image_stream = io.BytesIO(base64_source.decode(encoding))
|
||||
image = Image.open(image_stream)
|
||||
# check image size: do not create a thumbnail if avoiding smaller images
|
||||
if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]:
|
||||
return base64_source
|
||||
# create a thumbnail: will resize and keep ratios
|
||||
image.thumbnail(size, Image.ANTIALIAS)
|
||||
# create a transparent image for background
|
||||
background = Image.new('RGBA', size, (255, 255, 255, 0))
|
||||
# past the resized image on the background
|
||||
background.paste(image, ((size[0] - image.size[0]) / 2, (size[1] - image.size[1]) / 2))
|
||||
# return an encoded image
|
||||
background_stream = StringIO.StringIO()
|
||||
background.save(background_stream, filetype)
|
||||
return background_stream.getvalue().encode(encoding)
|
||||
|
||||
def image_resize_image_big(base64_source, size=(1204, 1204), encoding='base64', filetype='PNG'):
|
||||
""" Wrapper on image_resize_image, to resize images larger than the standard
|
||||
'big' image size: 1024x1024px.
|
||||
:param base64_source: base64 encoded source image. If False,
|
||||
the function returns False.
|
||||
"""
|
||||
if not base64_source:
|
||||
return False
|
||||
return image_resize_image(base64_source, size, encoding, filetype, True)
|
||||
|
||||
def image_resize_image_medium(base64_source, size=(180, 180), encoding='base64', filetype='PNG'):
|
||||
""" Wrapper on image_resize_image, to resize to the standard 'medium'
|
||||
image size: 180x180.
|
||||
:param base64_source: base64 encoded source image. If False,
|
||||
the function returns False.
|
||||
"""
|
||||
if not base64_source:
|
||||
return False
|
||||
return image_resize_image(base64_source, size, encoding, filetype)
|
||||
|
||||
def image_resize_image_small(base64_source, size=(50, 50), encoding='base64', filetype='PNG'):
|
||||
""" Wrapper on image_resize_image, to resize to the standard 'small' image
|
||||
size: 50x50.
|
||||
:param base64_source: base64 encoded source image. If False,
|
||||
the function returns False.
|
||||
"""
|
||||
if not base64_source:
|
||||
return False
|
||||
return image_resize_image(base64_source, size, encoding, filetype)
|
||||
|
||||
# ----------------------------------------
|
||||
# Misc image tools
|
||||
# ---------------------------------------
|
||||
|
||||
def image_get_resized_images(base64_source, return_big=False, return_medium=True, return_small=True,
|
||||
big_name='image', medium_name='image_medium', small_name='image_small'):
|
||||
""" Standard tool function that returns a dictionary containing the
|
||||
big, medium and small versions of the source image. This function
|
||||
is meant to be used for the methods of functional fields for
|
||||
models using images.
|
||||
|
||||
Default parameters are given to be used for the getter of functional
|
||||
image fields, for example with res.users or res.partner. It returns
|
||||
only image_medium and image_small values, to update those fields.
|
||||
|
||||
:param base64_source: if set to False, other values are set to False
|
||||
also. The purpose is to be linked to the fields that hold images in
|
||||
OpenERP and that are binary fields.
|
||||
:param return_big: if set, return_dict contains the 'big_name' entry
|
||||
:param return_medium: if set, return_dict contains the 'medium_name' entry
|
||||
:param return_small: if set, return_dict contains the 'small_name' entry
|
||||
:param big_name: name related to the big version of the image;
|
||||
'image' by default.
|
||||
:param medium_name: name related to the medium version of the
|
||||
image; 'image_medium' by default.
|
||||
:param small_name: name related to the small version of the
|
||||
image; 'image_small' by default.
|
||||
:return return_dict: dictionary with resized images, depending on
|
||||
previous parameters.
|
||||
"""
|
||||
return_dict = dict()
|
||||
if return_big: return_dict[big_name] = image_resize_image_big(base64_source)
|
||||
if return_medium: return_dict[medium_name] = image_resize_image_medium(base64_source)
|
||||
if return_small: return_dict[small_name] = image_resize_image_small(base64_source)
|
||||
return return_dict
|
||||
@@ -279,7 +279,11 @@ 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("<.*-open(?:object|erp)-(\\d+).*@(.*)>", re.UNICODE)
|
||||
|
||||
# Updated in 7.0 to match the model name as well
|
||||
# Typical form of references is <timestamp-openerp-record_id-model_name@domain>
|
||||
# group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain
|
||||
reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?.*@(.*)>", re.UNICODE)
|
||||
|
||||
def html2plaintext(html, body_id=None, encoding='utf-8'):
|
||||
""" From an HTML text, convert the HTML to plain text.
|
||||
|
||||
Reference in New Issue
Block a user