Merge lp:~camptocamp/openobject-addons/trunk-extra-sale-exceptions-2.0 into lp:openobject-addons/extra-trunk

Proposed by Guewen Baconnier @ Camptocamp
Status: Merged
Merge reported by: Guewen Baconnier @ Camptocamp
Merged at revision: not available
Proposed branch: lp:~camptocamp/openobject-addons/trunk-extra-sale-exceptions-2.0
Merge into: lp:openobject-addons/extra-trunk
Diff against target: 543 lines (+326/-72)
8 files modified
sale_exceptions/__init__.py (+1/-0)
sale_exceptions/__openerp__.py (+3/-2)
sale_exceptions/sale.py (+124/-51)
sale_exceptions/sale_exceptions_data.xml (+48/-13)
sale_exceptions/sale_view.xml (+46/-6)
sale_exceptions/wizard/__init__.py (+1/-0)
sale_exceptions/wizard/sale_exception_confirm.py (+58/-0)
sale_exceptions/wizard/sale_exception_confirm_view.xml (+45/-0)
To merge this branch: bzr merge lp:~camptocamp/openobject-addons/trunk-extra-sale-exceptions-2.0
Reviewer Review Type Date Requested Status
Sébastien BEAU - http://www.akretion.com Pending
Review via email: mp+94061@code.launchpad.net

Description of the change

Hi,

Here is my improvements on the module sale_exceptions.

I want to commit you my work before any merge, because I think it may have some impact on your clients.

In short :
 - Instead of a method, each exception has now a python code, evaluated (safe_eval) during the execution. Indeed, you need super rights (group_system) to modify the exceptions code. So it let add, modify or deactivate (active field) each sale exception rule. Each rule is applied on the order or the sale order.
 - The test_exceptions method (workflow condition) does not anymore raise an error, it returns only False when some exceptions have been found
 - The confirm button on the sale order now open a nice popup (osv_memory) with the list of exceptions to fix. The Sale Manager has the possibility to bypass the exceptions and confirm it anyway.

Now, one remark:
I guess that this module has been created for a specific need, and that it contains default rules which are not really generic ('unknown' in line.product_id.name.lower() as instance ?). I also commented out in my branch the rule excep_invalid_location which needs the (missing) dependency on the module delivery.

I propose to clean up all this base exceptions, do you agree? we should keep some good examples like No ZIP on Destination or Not Enough Virtual Stock.

Thanks
See you
Guewen

To post a comment you must log in.
5627. By Guewen Baconnier @ Camptocamp

[FIX] removed necessary import

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'sale_exceptions/__init__.py'
2--- sale_exceptions/__init__.py 2011-03-07 12:19:42 +0000
3+++ sale_exceptions/__init__.py 2012-02-21 21:55:24 +0000
4@@ -20,5 +20,6 @@
5 ##############################################################################
6
7 import sale
8+import wizard
9 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
10
11
12=== modified file 'sale_exceptions/__openerp__.py'
13--- sale_exceptions/__openerp__.py 2012-02-13 09:31:57 +0000
14+++ sale_exceptions/__openerp__.py 2012-02-21 21:55:24 +0000
15@@ -23,11 +23,11 @@
16
17 {
18 'name': 'Sale Exceptions',
19- 'version': '1.0',
20+ 'version': '2.0',
21 'category': 'Generic Modules/Sale',
22 'description': """
23 This module allows you attach several customizable exceptions to your sale order in a way that you can filter orders by exceptions type and fix them.
24-This is escpecially useful in an order importation scenario such as with the base_sale_multi_channels module, because it's likely a few orders have errors when you import them (like product not found in OpenERP, wrong line format etc...)
25+This is especially useful in an order importation scenario such as with the base_sale_multi_channels module, because it's likely a few orders have errors when you import them (like product not found in OpenERP, wrong line format etc...)
26 """,
27 'author': 'Akretion',
28 'website': 'http://www.akretion.com',
29@@ -36,6 +36,7 @@
30 'update_xml': ['sale_workflow.xml',
31 'sale_view.xml',
32 'sale_exceptions_data.xml',
33+ 'wizard/sale_exception_confirm_view.xml',
34 'security/ir.model.access.csv'],
35 'demo_xml': [],
36 'installable': True,
37
38=== modified file 'sale_exceptions/sale.py'
39--- sale_exceptions/sale.py 2012-02-13 09:33:17 +0000
40+++ sale_exceptions/sale.py 2012-02-21 21:55:24 +0000
41@@ -3,6 +3,7 @@
42 #
43 # OpenERP, Open Source Management Solution
44 # Copyright (C) 2011 Akretion LTDA.
45+# Copyright (C) 2012 Camptocamp SA (Guewen Baconnier)
46 #
47 # This program is free software: you can redistribute it and/or modify
48 # it under the terms of the GNU Affero General Public License as
49@@ -19,20 +20,40 @@
50 #
51 ##############################################################################
52
53-from datetime import datetime, timedelta
54-from dateutil.relativedelta import relativedelta
55 import time
56+import netsvc
57
58 from osv import fields, osv
59+from tools.safe_eval import safe_eval as eval
60 from tools.translate import _
61-import netsvc
62
63 class sale_exception(osv.osv):
64 _name = "sale.exception"
65 _description = "Sale Exceptions"
66 _columns = {
67 'name': fields.char('Exception Name', size=64, required=True, translate=True),
68- 'sale_order_ids': fields.many2many('sale.order', 'sale_order_exception_rel', 'exception_id', 'sale_order_id', 'Sale Orders'),
69+ 'note': fields.text('Note'),
70+ 'sale_order_ids': fields.many2many('sale.order', 'sale_order_exception_rel',
71+ 'exception_id', 'sale_order_id', 'Sale Orders'),
72+ 'model': fields.selection([('sale.order', 'Sale Order'), ('sale.order.line', 'Sale Order Line')],
73+ string='Apply on', required=True),
74+ 'active': fields.boolean('Active'),
75+ 'code': fields.text('Python Code',
76+ help="Python code executed to check if the exception apply or not. The code must apply block = True to apply the exception."),
77+ }
78+
79+ _defaults = {
80+ 'code': """# Python code. Use block = True to block the sale order.
81+# You can use the following variables :
82+# - self: ORM model of the record which is checked
83+# - order or line: browse_record of the sale order or sale order line
84+# - object: same as order or line, browse_record of the sale order or sale order line
85+# - pool: ORM model pool (i.e. self.pool)
86+# - time: Python time module
87+# - cr: database cursor
88+# - uid: current user id
89+# - context: current context
90+"""
91 }
92
93 sale_exception()
94@@ -41,58 +62,110 @@
95 _inherit = "sale.order"
96 _columns = {
97 'exceptions_ids': fields.many2many('sale.exception', 'sale_order_exception_rel', 'sale_order_id', 'exception_id', 'Exceptions'),
98+ 'ignore_exceptions': fields.boolean('Ignore Exceptions'),
99 }
100
101- def __add_exception(self, cr, uid, exceptions_list, exception_name):
102- ir_model_data_id = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', exception_name)])[0]
103- except_id = self.pool.get('ir.model.data').read(cr, uid, ir_model_data_id, ['res_id'])['res_id']
104- if except_id not in exceptions_list:
105- exceptions_list.append(except_id)
106-
107 def test_all_draft_orders(self, cr, uid, context=None):
108 ids = self.search(cr, uid, [('state', '=', 'draft')])
109- for id in ids:
110- try:
111- self.test_exceptions(cr, uid, [id])
112- except Exception:
113- pass
114- return True
115-
116- def test_exceptions(self, cr, uid, ids, *args):
117+ self.test_exceptions(cr, uid, ids)
118+ return True
119+
120+ def button_order_confirm(self, cr, uid, ids, context=None):
121+ exception_ids = self.detect_exceptions(cr, uid, ids, context=context)
122+ if exception_ids:
123+ model_data_obj = self.pool.get('ir.model.data')
124+ action = {
125+ 'type': 'ir.actions.act_window',
126+ 'view_type': 'form',
127+ 'view_mode': 'form',
128+ 'res_model': 'sale.exception.confirm',
129+ 'view_id': model_data_obj.get_object_reference(cr, uid, 'sale_exceptions', 'view_sale_exception_confirm')[1],
130+ 'target': 'new',
131+ }
132+ return action
133+ else:
134+ wf_service = netsvc.LocalService("workflow")
135+ wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
136+ return True
137+
138+ def test_exceptions(self, cr, uid, ids, context=None):
139+ """
140+ Condition method for the workflow from draft to confirm
141+ """
142+ exception_ids = self.detect_exceptions(cr, uid, ids, context=context)
143+ if exception_ids:
144+ return False
145+ return True
146+
147+ def detect_exceptions(self, cr, uid, ids, context=None):
148+ exception_obj = self.pool.get('sale.exception')
149+ order_exception_ids = exception_obj.search(cr, uid,
150+ [('model', '=', 'sale.order')], context=context)
151+ line_exception_ids = exception_obj.search(cr, uid,
152+ [('model', '=', 'sale.order.line')], context=context)
153+ order_exceptions = exception_obj.browse(cr, uid, order_exception_ids, context=context)
154+ line_exceptions = exception_obj.browse(cr, uid, line_exception_ids, context=context)
155+
156+ exception_ids = False
157 for order in self.browse(cr, uid, ids):
158- new_exceptions = []
159- self.add_custom_order_exception(cr, uid, ids, order, new_exceptions, *args)
160- self.write(cr, uid, [order.id], {'exceptions_ids': [(6, 0, new_exceptions)]})
161- cr.commit()
162- if len(new_exceptions) != 0:
163- raise osv.except_osv(_('Order has errors!'), "\n".join(ex.name for ex in self.pool.get('sale.exception').browse(cr, uid, new_exceptions)))
164- return True
165-
166- def add_custom_order_exception(self, cr, uid, ids, order, exceptions, *args):
167- self.detect_invalid_destination(cr, uid, order, exceptions)
168- self.detect_no_zip(cr, uid, order, exceptions)
169+ if order.ignore_exceptions:
170+ continue
171+ exception_ids = self._detect_exceptions(cr, uid, order,
172+ order_exceptions, line_exceptions, context=context)
173+
174+ self.write(cr, uid, [order.id], {'exceptions_ids': [(6, 0, exception_ids)]})
175+ return exception_ids
176+
177+ def _exception_rule_eval_context(self, cr, uid, obj_name, obj, context=None):
178+ if context is None:
179+ context = {}
180+
181+ return {obj_name: obj,
182+ 'self': self.pool.get(obj._name),
183+ 'object': obj,
184+ 'obj': obj,
185+ 'pool': self.pool,
186+ 'cr': cr,
187+ 'uid': uid,
188+ 'user': self.pool.get('res.users').browse(cr, uid, uid),
189+ 'time': time,
190+ # copy context to prevent side-effects of eval
191+ 'context': dict(context),}
192+
193+ def _rule_eval(self, cr, uid, rule, obj_name, obj, context):
194+ expr = rule.code
195+ space = self._exception_rule_eval_context(cr, uid, obj_name, obj,
196+ context=context)
197+ try:
198+ eval(expr, space,
199+ mode='exec', nocopy=True) # nocopy allows to return 'result'
200+ except Exception, e:
201+ raise osv.except_osv(_('Error'), _('Error when evaluating the sale exception rule :\n %s \n(%s)') %
202+ (rule.name, e))
203+ return space.get('block', False)
204+
205+ def _detect_exceptions(self, cr, uid, order, order_exceptions, line_exceptions, context=None):
206+ exception_ids = []
207+ for rule in order_exceptions:
208+ if self._rule_eval(cr, uid, rule, 'order', order, context):
209+ exception_ids.append(rule.id)
210+
211 for order_line in order.order_line:
212- self.detect_wrong_product(cr, uid, order_line, exceptions)
213- self.detect_not_enough_virtual_stock(cr, uid, order_line, exceptions)
214- return exceptions
215-
216- def detect_invalid_destination(self, cr, uid, order, exceptions):
217- no_delivery_carrier = self.pool.get('delivery.carrier').search(cr, uid, [('name', '=', 'No Delivery')])
218- if no_delivery_carrier:
219- no_delivery_carrier_grid = self.pool.get('delivery.carrier').grid_get(cr, uid, no_delivery_carrier, order.partner_shipping_id.id)
220- if no_delivery_carrier_grid:
221- self.__add_exception(cr, uid, exceptions, 'excep_invalid_location')
222-
223- def detect_no_zip(self, cr, uid, order, exceptions):
224- if not order.partner_shipping_id.zip:
225- self.__add_exception(cr, uid, exceptions, 'excep_no_zip')
226-
227- def detect_wrong_product(self, cr, uid, order_line, exceptions):
228- if order_line.product_id and 'unknown' in order_line.product_id.name.lower() or not order_line.product_id:
229- self.__add_exception(cr, uid, exceptions, 'excep_product')
230-
231- def detect_not_enough_virtual_stock(self, cr, uid, order_line, exceptions):
232- if order_line.product_id and order_line.product_id.type == 'product' and order_line.product_id.virtual_available < order_line.product_uom_qty:
233- self.__add_exception(cr, uid, exceptions, 'excep_no_stock')
234+ for rule in line_exceptions:
235+ if rule.id in exception_ids:
236+ continue # we do not matter if the exception as already been
237+ # found for an order line of this order
238+ if self._rule_eval(cr, uid, rule, 'line', order_line, context):
239+ exception_ids.append(rule.id)
240+
241+ return exception_ids
242+
243+ def copy(self, cr, uid, id, default=None, context=None):
244+ if default is None:
245+ default = {}
246+ default.update({
247+ 'ignore_exceptions': False,
248+ })
249+ return super(sale_order, self).copy(cr, uid, id, default=default, context=context)
250
251 sale_order()
252
253=== modified file 'sale_exceptions/sale_exceptions_data.xml'
254--- sale_exceptions/sale_exceptions_data.xml 2011-08-22 21:10:53 +0000
255+++ sale_exceptions/sale_exceptions_data.xml 2012-02-21 21:55:24 +0000
256@@ -1,30 +1,49 @@
257 <?xml version="1.0" encoding="utf-8"?>
258 <openerp>
259- <data>
260+ <data noupdate="1">
261 <record id="excep_product" model="sale.exception">
262- <field name="name">Error: Product Not Found</field>
263- </record>
264-
265- <record id="excep_invalid_location" model="sale.exception">
266- <field name="name">Error: Invalid Destination</field>
267+ <field name="name">Product Not Found</field>
268+ <field name="code">if line.product_id and 'unknown' in line.product_id.name.lower() or not line.product_id:
269+ block = True</field>
270+ <field name="model">sale.order.line</field>
271+ <field name="note"></field>
272+ <field name="active" eval="True"/>
273 </record>
274
275 <record id="excep_no_zip" model="sale.exception">
276- <field name="name">Error: No Destination zip</field>
277+ <field name="name">No ZIP on Destination</field>
278+ <field name="code">if not order.partner_shipping_id.zip:
279+ block = True</field>
280+ <field name="model">sale.order</field>
281+ <field name="note"></field>
282+ <field name="active" eval="True"/>
283 </record>
284
285 <record id="excep_wrong_line" model="sale.exception">
286- <field name="name">Error: Wrong Order Line Format</field>
287+ <field name="name">Wrong Order Line Format</field>
288+ <field name="code"></field>
289+ <field name="model">sale.order.line</field>
290+ <field name="note"></field>
291+ <field name="active" eval="False"/>
292 </record>
293
294 <record id="excep_no_stock" model="sale.exception">
295- <field name="name">Error: Not enough virtual Stock</field>
296+ <field name="name">Not Enough Virtual Stock</field>
297+ <field name="code">if line.product_id and line.product_id.type == 'product' and line.product_id.virtual_available &lt; line.product_uom_qty:
298+ block = True</field>
299+ <field name="model">sale.order.line</field>
300+ <field name="note"></field>
301+ <field name="active" eval="True"/>
302 </record>
303
304 <record id="excep_no_component_stock" model="sale.exception">
305- <field name="name">Error: Not enough virtual stock of components</field>
306+ <field name="name">Not enough virtual stock of components</field>
307+ <field name="code"></field>
308+ <field name="model">sale.order.line</field>
309+ <field name="active" eval="False"/>could
310+ <field name="note"></field>
311 </record>
312-
313+
314 <record id="no_delivery_partner" model="res.partner">
315 <field name="name">No Delivery</field>
316 </record>
317@@ -35,11 +54,27 @@
318 <field model="product.category" name="categ_id" search="[]"/>
319 </record>
320
321- <record id="no_delivery_carrier" model="delivery.carrier">
322+ <!--
323+ induce a dependency on delivery, is it really necessary ?
324+
325+ <record id="excep_invalid_location" model="sale.exception">
326+ <field name="name">Invalid Destination</field>
327+ <field name="code">carrier_obj = pool.get('delivery.carrier')
328+no_delivery_carrier = carrier_obj.search(cr, uid, [('name', '=', 'No Delivery')])
329+if no_delivery_carrier:
330+ no_delivery_carrier_grid = carrier_obj.grid_get(cr, uid, no_delivery_carrier, order.partner_shipping_id.id)
331+ if no_delivery_carrier_grid:
332+ block=True</field>
333+ <field name="model">sale.order</field>
334+ <field name="note"></field>
335+ <field name="active" eval="True"/>
336+ </record>
337+
338+ <record id="no_delivery_carrier" model="delivery.carrier">
339 <field name="name">No Delivery</field>
340 <field name="partner_id" ref="no_delivery_partner"/>
341 <field name="product_id" ref="no_delivery_product"/>
342- </record>
343+ </record>-->
344
345 <record forcecreate="True" id="ir_cron_test_orders" model="ir.cron">
346 <field name="name">Test Draft Orders</field>
347
348=== modified file 'sale_exceptions/sale_view.xml'
349--- sale_exceptions/sale_view.xml 2011-03-07 12:19:42 +0000
350+++ sale_exceptions/sale_view.xml 2012-02-21 21:55:24 +0000
351@@ -1,6 +1,39 @@
352 <?xml version="1.0" ?>
353 <openerp>
354 <data>
355+
356+ <record id="view_sale_exception_tree" model="ir.ui.view">
357+ <field name="name">sale.exception.tree</field>
358+ <field name="model">sale.exception</field>
359+ <field name="type">tree</field>
360+ <field name="arch" type="xml">
361+ <tree string="Sale Exception">
362+ <field name="name"/>
363+ <field name="note"/>
364+ </tree>
365+ </field>
366+ </record>
367+
368+ <record id="view_sale_exception_form" model="ir.ui.view">
369+ <field name="name">sale.exception.form</field>
370+ <field name="model">sale.exception</field>
371+ <field name="type">form</field>
372+ <field name="arch" type="xml">
373+ <form string="Sale Exception Setup">
374+ <field name="name" colspan="4"/>
375+ <field name="note" colspan="4"/>
376+ <newline/>
377+ <group colspan="4" col="2" groups="base.group_sale_manager">
378+ <field name="active"/>
379+ <group colspan="2" col="2" groups="base.group_system">
380+ <field name="model"/>
381+ <field name="code"/>
382+ </group>
383+ </group>
384+ </form>
385+ </field>
386+ </record>
387+
388 <record id="ir_actions_act_window_exceptions0" model="ir.actions.act_window">
389 <field eval="[(6,0,[])]" name="groups_id"/>
390 <field name="context">{}</field>
391@@ -14,6 +47,7 @@
392 <field eval="0" name="multi"/>
393 <field name="type">ir.actions.act_window</field>
394 <field name="name">Exceptions</field>
395+ <field name="view_id" ref="view_sale_exception_tree"/>
396 </record>
397
398 <menuitem action="ir_actions_act_window_exceptions0" id="ir_ui_menu_exceptions0"
399@@ -24,12 +58,18 @@
400 <field name="inherit_id" ref="sale.view_order_form"/>
401 <field name="name">sale.order.68956.inherit</field>
402 <field name="arch" type="xml">
403- <field name="note" position="replace">
404- <group colspan="6" col="6">
405- <field name="exceptions_ids" colspan="2"/>
406- <field name="note" colspan="4"/>
407- </group>
408- </field>
409+ <data>
410+ <field name="note" position="after">
411+ <group colspan="6" col="6">
412+ <field name="exceptions_ids" colspan="2"/>
413+ <field name="note" colspan="4"/>
414+ </group>
415+ </field>
416+ <button name="order_confirm" position="attributes">
417+ <attribute name="name">button_order_confirm</attribute>
418+ <attribute name="type">object</attribute>
419+ </button>
420+ </data>
421 </field>
422 <field name="model">sale.order</field>
423 <field name="type">form</field>
424
425=== added directory 'sale_exceptions/wizard'
426=== added file 'sale_exceptions/wizard/__init__.py'
427--- sale_exceptions/wizard/__init__.py 1970-01-01 00:00:00 +0000
428+++ sale_exceptions/wizard/__init__.py 2012-02-21 21:55:24 +0000
429@@ -0,0 +1,1 @@
430+import sale_exception_confirm
431
432=== added file 'sale_exceptions/wizard/sale_exception_confirm.py'
433--- sale_exceptions/wizard/sale_exception_confirm.py 1970-01-01 00:00:00 +0000
434+++ sale_exceptions/wizard/sale_exception_confirm.py 2012-02-21 21:55:24 +0000
435@@ -0,0 +1,58 @@
436+# -*- encoding: utf-8 -*-
437+##############################################################################
438+#
439+# Copyright Camptocamp SA
440+# @author: Guewen Baconnier
441+#
442+# This program is free software: you can redistribute it and/or modify
443+# it under the terms of the GNU General Public License as published by
444+# the Free Software Foundation, either version 3 of the License, or
445+# (at your option) any later version.
446+#
447+# This program is distributed in the hope that it will be useful,
448+# but WITHOUT ANY WARRANTY; without even the implied warranty of
449+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
450+# GNU General Public License for more details.
451+#
452+# You should have received a copy of the GNU General Public License
453+# along with this program. If not, see <http://www.gnu.org/licenses/>.
454+#
455+##############################################################################
456+
457+import netsvc
458+
459+from osv import osv, fields
460+
461+
462+class SaleExceptionConfirm(osv.osv_memory):
463+
464+ _name = 'sale.exception.confirm'
465+
466+ _columns = {
467+ 'sale_id': fields.many2one('sale.order', 'Sale'),
468+ 'exception_ids': fields.many2many('sale.exception', string='Exceptions to resolve', readonly=True),
469+ 'ignore': fields.boolean('Ignore Exceptions'),
470+ }
471+
472+ def default_get(self, cr, uid, fields, context=None):
473+ res = super(SaleExceptionConfirm, self).default_get(cr, uid, fields, context=context)
474+ order_obj = self.pool.get('sale.order')
475+ sale_id = context.get('active_id', False)
476+ if sale_id:
477+ sale = order_obj.browse(cr, uid, sale_id, context=context)
478+ exception_ids = [e.id for e in sale.exceptions_ids]
479+ res.update({'exception_ids': [(6, 0, exception_ids)]})
480+
481+ res.update({'sale_id': sale_id})
482+ return res
483+
484+ def action_confirm(self, cr, uid, ids, context=None):
485+ form = self.browse(cr, uid, ids[0], context=context)
486+ if form.ignore:
487+ self.pool.get('sale.order').write(cr, uid, form.sale_id.id,
488+ {'ignore_exceptions': True}, context=context)
489+ wf_service = netsvc.LocalService("workflow")
490+ wf_service.trg_validate(uid, 'sale.order', form.sale_id.id, 'order_confirm', cr)
491+ return {'type': 'ir.actions.act_window_close'}
492+
493+SaleExceptionConfirm()
494
495=== added file 'sale_exceptions/wizard/sale_exception_confirm_view.xml'
496--- sale_exceptions/wizard/sale_exception_confirm_view.xml 1970-01-01 00:00:00 +0000
497+++ sale_exceptions/wizard/sale_exception_confirm_view.xml 2012-02-21 21:55:24 +0000
498@@ -0,0 +1,45 @@
499+<?xml version="1.0" encoding="utf-8"?>
500+<openerp>
501+ <data>
502+
503+ <record id="view_sale_exception_confirm" model="ir.ui.view">
504+ <field name="name">Sale Exceptions</field>
505+ <field name="model">sale.exception.confirm</field>
506+ <field name="type">form</field>
507+ <field name="arch" type="xml">
508+ <form string="Sale Exceptions">
509+ <separator colspan="4" string="Exceptions on the sale order" />
510+ <field name="exception_ids" colspan="4">
511+ <form string="Sale Exception">
512+ <field name="name" colspan="4"/>
513+ <field name="note" colspan="4"/>
514+ </form>
515+ <tree string="Sale Exceptions">
516+ <field name="name"/>
517+ <field name="note"/>
518+ </tree>
519+ </field>
520+ <newline/>
521+ <field name="ignore" groups='base.group_sale_manager'/>
522+ <newline/>
523+ <separator colspan="4"/>
524+ <group col="2" colspan="4">
525+ <button name="action_confirm" string="_Ok"
526+ colspan="1" type="object" icon="gtk-ok" />
527+ </group>
528+ </form>
529+ </field>
530+ </record>
531+
532+ <record id="action_sale_exception_confirm" model="ir.actions.act_window">
533+ <field name="name">Sale Exceptions</field>
534+ <field name="type">ir.actions.act_window</field>
535+ <field name="res_model">sale.exception.confirm</field>
536+ <field name="view_type">form</field>
537+ <field name="view_mode">form</field>
538+ <field name="view_id" ref="view_sale_exception_confirm"/>
539+ <field name="target">new</field>
540+ </record>
541+
542+ </data>
543+</openerp>

Subscribers

People subscribed via source and target branches