创建向导 - (The Process)

简介

向导是描述客户端和服务器之间的相互动作的序列.

以下是一个典型的向导进程:

  1. 向客户端发送一个窗口(待完成的表格)

  2. 客户端发送在此表格中所填的数据

  3. 服务器收到结果,执行一个函数并发送一个新的窗口到客户端

/doc_static/5.0/_images/Wizard.png

以下是一个向导的截图,该向导是用于处理业务的 (when you click on the gear icon in an account chart):

/doc_static/5.0/_images/Wizard_screenshot.png

向导 - 原则

向导具有一连串的步骤,每一个步骤又多个动作组成;

  1. 发送表单给客户端或按钮

  2. 客户端的按钮按下后,服务端获取表单的数据

  3. 执行动作

  4. 发送一个新动作给客户端(form,print, …)

To define a wizard, you have to create a class inheriting from wizard.interface and instantiate it. Each wizard must have a unique name, which can be chosen arbitrarily except for the fact it has to start with the module name (for example: account.move.line.reconcile). The wizard must define a dictionary named states which defines all its steps. A full example of a simple wizard can be found at http://www.openobject.com/forum/post43900.html#43900

以下为一个向导类的例子:

  1. class wiz_reconcile(wizard.interface):
  2. states = {
  3. 'init': {
  4. 'actions': [_trans_rec_get],
  5. 'result': {'type': 'form',
  6. 'arch': _transaction_form,
  7. 'fields': _transaction_fields,
  8. 'state':[('reconcile','Reconcile'),('end','Cancel')]}
  9. },
  10. 'reconcile': {
  11. 'actions': [_trans_rec_reconcile],
  12. 'result': {'type': 'state', 'state':'end'}
  13. }
  14. }
  15. wiz_reconcile('account.move.line.reconcile');

‘states’ 字典定义了向导的所有状态。在这个例子里; initreconcile. 还有一个隐藏的状态叫 end .

向导一般从从 init 状态开始,到 end 状态结束.

状态定义了以下两个东西:

  1. 动作列表

  2. 结果

动作列表(The list of actions)

向导的每一步骤/状态都定义了动作列表,到向导进入该状态后便执行这些动作。动作列表可以是空的.

函数(actions)的语法规范如下 :

  1. def _trans_rec_get(self, uid, data, res_get=False):

其中:

  • self 是指向向导当前对象的指针

  • uid 是执行此向导的用户ID

  • data 是包含以下数据的字典:

    • ids: 用户执行向导时,所关联的资源的id列表

    • id: 当用户执行向导时高亮的id

    • form: 一个字典,该字典包含了之前发送的表单里用户填写的数据,若你改变了字典里的值,则表单里的数据就会被提前填写.

每个动作函数必须返回一个字典。字典中的每一项将会与发送来的表单中所填写的数据合并.

结果(The result)

以下为一些result的例子:

Result: 下一步

  1. 'result': {'type': 'state',
  2. 'state':'end'}

表示向导得继续下一步的状态: ‘end’. 如果这一步是 ‘end’ 状态,则向导停止.

Result: 发给客户端的新对话

  1. 'result': {'type': 'form',
  2. 'arch': _form,
  3. 'fields': _fields,
  4. 'state':[('reconcile','Reconcile'),('end','Cancel')]}

type=form 表示这步骤是发送对话给客户端,对话由以下部分组成:

  1. a form : 带有字段的描述和表单的描述

  2. some buttons : 用户填写完数据后,按此按钮提交

表单的描述(arch)和视图一样,如下:

  1. _form = """<?xml version="1.0"?>
  2. <form title="Reconciliation">
  3. <separator string="Reconciliation transactions" colspan="4"/>
  4. <field name="trans_nbr"/>
  5. <newline/>
  6. <field name="credit"/>
  7. <field name="debit"/>
  8. <field name="state"/>
  9. <separator string="Write-Off" colspan="4"/>
  10. <field name="writeoff"/>
  11. <newline/>
  12. <field name="writeoff_acc_id" colspan="3"/>
  13. </form>
  14. """

字段的描述和python对象里字段的描述类似。如:

  1. _transaction_fields = {
  2. 'trans_nbr': {'string':'# of Transaction', 'type':'integer', 'readonly':True},
  3. 'credit': {'string':'Credit amount', 'type':'float', 'readonly':True},
  4. 'debit': {'string':'Debit amount', 'type':'float', 'readonly':True},
  5. 'state': {
  6. 'string':"Date/Period Filter",
  7. 'type':'selection',
  8. 'selection':[('bydate','By Date'),
  9. ('byperiod','By Period'),
  10. ('all','By Date and Period'),
  11. ('none','No Filter')],
  12. 'default': lambda *a:'none'
  13. },
  14. 'writeoff': {'string':'Write-Off amount', 'type':'float', 'readonly':True},
  15. 'writeoff_acc_id': {'string':'Write-Off account',
  16. 'type':'many2one',
  17. 'relation':'account.account'
  18. },
  19. }

向导中每一步/状态都有多个按钮,这些按钮都分布在对话框的右下方。向导的每一步所涉及的按钮列表在结果字典的状态键中声明.

For example:

  1. 'state':[('end', 'Cancel', 'gtk-cancel'), ('reconcile', 'Reconcile', '', True)]
  1. 下一步的名称(决定下一个状态)

  2. 按钮的名字 (用于在客户端上的展示)

  3. the gtk stock item without the stock prefix (自 4.2)

  4. a boolean, 如果为true,按钮被设置为默认的动作 (自 4.2)

以下为这种表单的截图:

/doc_static/5.0/_images/Wizard_screenshot1.png

Result: 调用方法决定下一个状态

  1. def _check_refund(self, cr, uid, data, context):
  2. ...
  3. return datas['form']['refund_id'] and 'wait_invoice' or 'end'
  4. ...
  5. 'result': {'type':'choice', 'next_state':_check_refund}

Result: 打印一个报表

  1. def _get_invoice_id(self, uid, datas):
  2. ...
  3. return {'ids': [...]}
  4. ...
  5. 'actions': [_get_invoice_id],
  6. 'result': {'type':'print',
  7. 'report':'account.invoice',
  8. 'get_id_from_action': True,
  9. 'state':'check_refund'}

Result: 客户端执行一个动作

  1. def _makeInvoices(self, cr, uid, data, context):
  2. ...
  3. return {
  4. 'domain': "[('id','in', ["+','.join(map(str,newinv))+"])]",
  5. 'name': 'Invoices',
  6. 'view_type': 'form',
  7. 'view_mode': 'tree,form',
  8. 'res_model': 'account.invoice',
  9. 'view_id': False,
  10. 'context': "{'type':'out_refund'}",
  11. 'type': 'ir.actions.act_window'
  12. }
  13. ...
  14. 'result': {'type': 'action',
  15. 'action': _makeInvoices,
  16. 'state': 'end'}

函数的返回的结果必须为 ir.actions.* 的所有字段. 这里为ir.action.act_window,所以客户端会打开一个新的标签页,新的标签页包含了account.invoicd的信息.

建议用一下方式读取 ir.actions 对象:

  1. def _account_chart_open_window(self, cr, uid, data, context):
  2. mod_obj = pooler.get_pool(cr.dbname).get('ir.model.data')
  3. act_obj = pooler.get_pool(cr.dbname).get('ir.actions.act_window')
  4. result = mod_obj._get_id(cr, uid, 'account', 'action_account_tree')
  5. id = mod_obj.read(cr, uid, [result], ['res_id'])[0]['res_id']
  6. result = act_obj.read(cr, uid, [id])[0]
  7. result['context'] = str({'fiscalyear': data['form']['fiscalyear']})
  8. return result
  9. ...
  10. 'result': {'type': 'action',
  11. 'action': _account_chart_open_window,
  12. 'state':'end'}

规范

表单(Form)

  1. _form = '''<?xml version="1.0"?>
  2. <form string="Your String">
  3. <field name="Field 1"/>
  4. <newline/>
  5. <field name="Field 2"/>
  6. </form>'''

字段(Fields)

标准(Standard)

  1. Field type: char, integer, boolean, float, date, datetime
  2. _fields = {
  3. 'str_field': {'string':'product name', 'type':'char', 'readonly':True},
  4. }
  • string: 字段标签 (必填)

  • type: (必填)

  • readonly: (可选)

关系(Relational)

  1. Field type: one2one,many2one,one2many,many2many
  2. _fields = {
  3. 'field_id': {'string':'Write-Off account', 'type':'many2one', 'relation':'account.account'}
  4. }
  • string: 字段标签 (必填)

  • type: (必填)

  • relation: 所关系的对象名称

选择(Selection)

  1. Field type: selection
  2. _fields = {
  3. 'field_id': {
  4. 'string':"Date/Period Filter",
  5. 'type':'selection',
  6. 'selection':[('bydate','By Date'),
  7. ('byperiod','By Period'),
  8. ('all','By Date and Period'),
  9. ('none','No Filter')],
  10. 'default': lambda *a:'none'
  11. },
  • string: 字段标签 (必填)

  • type: (必填)

  • selection: 选择字段中的键和值

添加一个新向导

创建一个新向导,你应该:

  • 在一个 .py 文件中创建向导定义

    • 向导一般都是定义在组件中的向导子文件夹中 server/bin/addons/module_name/wizard/your_wizard_name.py
  • 把向导添加到导入的声明列表,该列表位于组件向导子目录的 __init__.py 文件.

  • 在数据库中声明向导

声明需要映射向导和客户端键之间的关系,从而才能启动相应的客户端。声明一个新向导,需要把它加到 module_name_wizard.xml 文件里,该文件包含了此组件所有的向导声明。若该文件不存在,则需先创建.

这里以 account_wizard.xml 文件做一个例子;

  1. <?xml version="1.0"?>
  2. <openerp>
  3. <data>
  4. <delete model="ir.actions.wizard" search="[('wiz_name','like','account.')]" />
  5. <wizard string="Reconcile Transactions" model="account.move.line"
  6. name="account.move.line.reconcile" />
  7. <wizard string="Verify Transac steptions" model="account.move.line"
  8. name="account.move.line.check" keyword="tree_but_action" />
  9. <wizard string="Verify Transactions" model="account.move.line"
  10. name="account.move.line.check" />
  11. <wizard string="Print Journal" model="account.account"
  12. name="account.journal" />
  13. <wizard string="Split Invoice" model="account.invoice"
  14. name="account.invoice.split" />
  15. <wizard string="Refund Invoice" model="account.invoice"
  16. name="account.invoice.refund" />
  17. </data>
  18. </openerp>

向导的标签属性:

  • id: 此向导的唯一标识.

  • string: 如果一个资源关联多个向导,此字符串会显示).

  • model: 对象从该模型中获取所需数据.

  • name: 向导的名称,只可内部使用并且唯一.

  • replace (可选): 此向导是否要重写 all 所有已经存在的向导。缺省值: False.

  • menu (可选): 是否 (True|False) 把向导和 ‘gears’ 按钮 (i.e. show the button or not) 关联到一起。缺省值: True.

  • keyword (可选): 向导绑定另一动作 (print icon, gear icon, …). 关键字属性的可能值为:

    • client_print_multi: 表单中的打印图标

    • client_action_multi: 表单中的 ‘gears’ 图标

    • tree_but_action: 列表中的 ‘gears’ 图标 (在左侧的快捷方式)

    • tree_but_open: 在树的一个分支,双击 (在左侧的快捷方式). 例如,有这样的应用,在菜单中来绑定向导.

__terp__.py

If the wizard you created is the first one of its module, you probably had to create the modulename_wizard.xml file yourself. In that case, it should be added to the update_xml field of the __terp__.py file of the module.

Here is, for example, the __terp__.py file for the account module.

  1. {
  2. "name": OpenERP Accounting",
  3. "version": "0.1",
  4. "depends": ["base"],
  5. "init_xml": ["account_workflow.xml", "account_data.xml"],
  6. "update_xml": ["account_view.xml","account_report.xml", "account_wizard.xml"],
  7. }

osv_memory 向导系统

开发一个 osv_memory 向导, 只需创建一普通的对象,不是继承至 osv.osv, 而是继承至 osv.osv_memory. 向导 “wizard” 的方法是在对象中的,如果向导很复杂,可以在对象中定义工作流. osv_memory 对象是存储在内存中的,而不是存储在 postgresql.

That’s all, nothing more than just changing the inherit. These wizards can be defined at any location unlike addons/modulename/modulename_wizard.py. Historically, the _wizard prefix is for actual (old-style) wizards, so there might be a connotation there, the “new-style” osv_memory based “wizards” are perfectly normal objects (just used to emulate the old wizards, so they don’t really match the old separations. Furthermore, osv_memory based “wizards” tend to need more than one object (e.g. one osv_memory object for each state of the original wizard) so the correspondance is not exactly 1:1.

所以,为何他们看着想旧式的向导呢?

  • 在打开对象的动作中,你可以写入以下
  1. <field name="target">new</field>

这表示对象会在一个新的窗口中打开,而非当前这个.

  • 可以使用 来关闭窗口.

例如 : 在 project.py 文件中.

  1. class config_compute_remaining(osv.osv_memory):
  2. _name='config.compute.remaining'
  3. def _get_remaining(self,cr, uid, ctx):
  4. if 'active_id' in ctx:
  5. return self.pool.get('project.task').browse(cr,uid,ctx['active_id']).remaining_hours
  6. return False
  7. _columns = {
  8. 'remaining_hours' : fields.float('Remaining Hours', digits=(16,2),),
  9. }
  10. _defaults = {
  11. 'remaining_hours': _get_remaining
  12. }
  13. def compute_hours(self, cr, uid, ids, context=None):
  14. if 'active_id' in context:
  15. remaining_hrs=self.browse(cr,uid,ids)[0].remaining_hours
  16. self.pool.get('project.task').write(cr,uid,context['active_id'],
  17. {'remaining_hours' : remaining_hrs})
  18. return {
  19. 'type': 'ir.actions.act_window_close',
  20. }
  21. config_compute_remaining()
  • 视图也和普通的视图一样 (注意按钮).

例如 :

  1. <record id="view_config_compute_remaining" model="ir.ui.view">
  2. <field name="name">Compute Remaining Hours </field>
  3. <field name="model">config.compute.remaining</field>
  4. <field name="type">form</field>
  5. <field name="arch" type="xml">
  6. <form string="Remaining Hours">
  7. <separator colspan="4" string="Change Remaining Hours"/>
  8. <newline/>
  9. <field name="remaining_hours" widget="float_time"/>
  10. <group col="4" colspan="4">
  11. <button icon="gtk-cancel" special="cancel" string="Cancel"/>
  12. <button icon="gtk-ok" name="compute_hours" string="Update" type="object"/>
  13. </group>
  14. </form>
  15. </field>
  16. </record>
  • 动作也和普通的动作一样 (不要忘了添加一个target 属性)

例如 :

  1. <record id="action_config_compute_remaining" model="ir.actions.act_window">
  2. <field name="name">Compute Remaining Hours</field>
  3. <field name="type">ir.actions.act_window</field>
  4. <field name="res_model">config.compute.remaining</field>
  5. <field name="view_type">form</field>
  6. <field name="view_mode">form</field>
  7. <field name="target">new</field>
  8. </record>

osv_memory 配置项

有时,你的插件不希望用默认的配置,而需要进一步的配置从而工作的更好。在这种情况下, 你希望在安装之后能有一个配置向导,并在今后需要重新配置时能再次调用该向导.

5.0以上的openerp有这样的功能,但却无相应的文档,而且需要手工操作。为了这样的需求, 一个简单明了的新的解决方案已经出现.

基础概念

新的解决方案提供一个具有基本的行为 osv_memory 对象,你必须继承该对象。这行为 是用来控制配置项和扩展之间的流的,而且必须继承自此对象.

同时,还有一个可继承的视图,该视图提供一个基本的框架,通过这种机制从而达到很强的可定制性。所以强烈建议你继承该视图.

创建基本的配置项

你的配置模型

首先是创建配置项本身,这是以个普通的 osv_memory 对象,该对象有一些限制:

  • 必须继承至 res.config, 提供一个基本的配置行为和基本的事件控制器和扩展点

  • 必须提供一个 execute 方法.[#]_ 当验证配置表单和包含验证逻辑时,就会调用这个方法。方法没有返回值.

  1. class my_item_config(osv.osv_memory):
  2. _name = 'my.model.config'
  3. _inherit = 'res.config' # mandatory
  4. _columns = {
  5. 'my_field': fields.char('Field', size=64, required=True),
  6. }
  7. def execute(self, cr, uid, ids, context=None):
  8. 'do whatever configuration work you need here'
  9. my_item_config()

配置视图

接下来是配置表单。Openerp提供一个基础视图,你可以继承这个基础视图,所以你 不需要自己创建按钮和控制进度条。强烈推荐使用这个基本视图.

在 ir.ui.view 中加入一个 inherit_id 字段,把它的值设为 res_config_view_base:

  1. <record id="my_config_view_form" model="ir.ui.view">
  2. <field name="name">my.item.config.view</field>
  3. <!-- this is the model defined above -->
  4. <field name="model">my.model.config</field>
  5. <field name="type">form</field>
  6. <field name="inherit_id" ref="base.res_config_view_base"/>
  7. ...
  8. </record>

当不做任何改变时,会展示出一个对话框,该对话框中包含一个进度条和两个按钮, 毫无生趣. res_config_view_base 有一个特别的group hook,你可以用你自己 的代码代替它,如下:

  1. <field name="arch" type="xml">
  2. <group string="res_config_contents" position="replace">
  3. <!-- your content should be inserted within this, the string
  4. attribute of the previous group is used to easily find
  5. it for replacement -->
  6. <label colspan="4" align="0.0" string="
  7. Configure this item by defining its field"/>
  8. <field colspan="2" name="my_field"/>
  9. </group>
  10. </field>

打开你的窗口

下一步是创建 act_window ,用于连接模型和视图的配置:

  1. <record id="my_config_window" model="ir.actions.act_window">
  2. <field name="name">My config window</field>
  3. <field name="type">ir.actions.act_window</field>
  4. <field name="res_model">my.model.config</field>
  5. <field name="view_type">form</field>
  6. <field name="view_id" ref="my_config_view_form"/>
  7. <field name="view_mode">form</field>
  8. <field name="target">new</field>
  9. </record>

当在配置向导步骤的子菜单中列出多种配置选项时,注意到 act_window 的 name 字段会被显示出来 (在 Administration > Configuration > Configuration > Wizards).

注册你的动作

最后是在openerp中注册配置项。这是在 ir.actions.todo 对象中完成的, 需要一个 action_id 字段关联到之前创建的 act_window:

  1. <record id="my_config_step" model="ir.actions.todo">
  2. <field name="action_id" ref="my_config_window"/>
  3. </record>

ir.actions.todo 有3个可选字段:

sequence (default: 10)

执行次序,数值小的先执行.

active (default: True)

不活跃的步骤在下一轮配置时将不会被执行.

state (default: ‘open’)

配置步骤的当前状态,用于记录执行过程中所发生的时间,值包含有 ‘open’, ‘done’, ‘skip’ and ‘cancel’.

结果如下图:

/doc_static/5.0/_images/config_wizard_base.png

定制你的配置项

目前所具备的知识已经足够配置你的插件了,但做一些好的定制能得到更好的用户体验.

更进一步的视图的定制

也许你已经注意到之前的截图,在默认的情况下,窗口是没有标题的,虽然并无大碍但却影响美观.

在设置一个标题之前,需要在视图里做一些微小的改动: group 标签中需要填入 data ,这样就能修改父窗口中的多项配置:

  1. <record id="my_config_view_form" model="ir.ui.view">
  2. <field name="name">my.item.config.view</field>
  3. <!-- this is the model defined above -->
  4. <field name="model">my.model.config</field>
  5. <field name="type">form</field>
  6. <field name="inherit_id">res_config_view_base</field>
  7. <field name="arch" type="xml">
  8. <data>
  9. <group string="res_config_contents" position="replace">
  10. <!-- your content should be inserted within this, the
  11. string attribute of the previous group is used to
  12. easily find it for replacement
  13. -->
  14. <label colspan="4" align="0.0" string="
  15. Configure this item by defining its field
  16. ">
  17. <field colspan="2" name="my_field"/>
  18. </group>
  19. </data>
  20. </field>
  21. </record>

然后,就能通过增加以下代码 data 元件,从而转换原始 form 的 string 属性 (这个例子,或许在 group 前):

  1. <!-- position=attributes is new and is used to alter the
  2. element's attributes, instead of its content -->
  3. <form position="attributes">
  4. <!-- set the value of the 'string' attribute -->
  5. <attribute name="string">Set item field</attribute>
  6. </form>

警告

Comments in view overload

At this point (December 2009) OpenERP cannot handle comments at the toplevel of the view element overload. When testing or reusing these examples, remember to strip out the comments or you will get runtime errors when testing the addon.

完成这步后,配置的表单有用了一个标题:

/doc_static/5.0/_images/config_wizard_title.png

More interesting customizations might be to alter the buttons provided by res_config_view_base at the bottom of the dialog: remove a button (if the configuration action shouldn’t be skippable), change the button labels, …

由于这些改变无具体的与之关联的属性,需要使用xpath选择器(使用 xpath 元素).

删除Skip按钮,把Recond按钮的标签改成Set。例如,可以在 group 元素后加入以下代码,如下:

  1. <!-- select the button 'action_skip' of the original template
  2. and replace it by nothing, removing it -->
  3. <xpath expr="//button[@name='action_skip']"
  4. position="replace"/>
  1. <!-- select the button 'action_next' -->
  2. <xpath expr="//button[@name='action_next']"
  3. position="attributes">
  4. <!-- and change the attribute 'string' to 'Set' -->
  5. <attribute name="string">Set</attribute>
  6. </xpath>

and yield:

/doc_static/5.0/_images/config_wizard_buttons.png

还可以用这种方法改变按钮的名称, 这样方法在对象中被唤醒 (不推荐).

定制模型

使用 execute method hook, 可以很轻易的实现许多要求,但addon-specific要求会 更复杂点. res.config 必须提供所有的hooks用于复杂的行为.

忽略下一步

最后,通过调用 res.config 中的 self.next 方法,进入下一步的配置项。 action_next 和 action_skip 最后 做的事。但是在某些情况下,循环当前的视图或实现一个和工作流一样的行为是必要的。在这样的情况下, 你可以通过 execute 返回一个字典, res.config 会跳转到那个视图,而不是 self.next 返回的那个 .

这是创建项目所必须做的,例如,让用户在一行中创建多个新用户.

在 skipping 执行一个动作

和 action_next 对比( action_next 要求 execute 被子类实现), action_skip 是实现 res.config 的。 但是在子模型需要完成sipping discovery的动作的情况下,它需要提供一个方法,该方法名为 cancel , 你可以用和 execute 一样的方法重载此函数。这是和 execute 一致的:不仅在 cancel 结束时可以自动调用 next 方法, 而且能 忽略下一步

选取动作

既可以选择action重载 action_next 和 action_skip 方法,也可以实现更多的函数,如果此实例中的按钮多余两.

在这样的情况下,请记住,你需要提供一个方法,让用户能调用self.next函数,这样他才能配置他剩下的插件.

res.config 的公开接口

接口和标准的Openerp的变量一致: self, cr, uid, ids and context.

execute

Hook 方法会被 action_next 按钮(默认标签:Record)唤醒. 除非你想展示一个 新的视图而非下一步的配置项,否则不要返回任何内容 anything 。返回字典以为的东西将会导致未定义的行为.

重写它是必须的。如果不这么做,将会导致 NotImplementedError 错误.

默认的 res.config 在重载时不能被调用 (don’t use super).

cancel

action_skip 按钮 (default label: Skip) 会调用相关的方法,他和 execute‘ 是一样的,除了它不是必须被重载.

next

下一步配置项调用绑定的方法.若配置项需要定制,则它可以被重载.

如果被重载,默认的 res.config 的实现将会被调用,返回值是为了下一步的配置项.

action_next and action_skip

基础视图中的时间控制按钮,重载他们是不需要的,但是在默认的 res.config 实现被调用 (via super) 且有返回值的情况下是必须的.

[1]

This isn’t completely true, as you will see when 定制你的配置项

[2]

this method is part of the official API and you’re free to overload it if needed, but you should always call res.config‘s through super when your work is done. Overloading next is also probably overkill in most situations.