Merge lp:~openerp-dev/openobject-addons/trunk-wms-salestockwkf-jco into lp:~openerp-dev/openobject-addons/trunk-wms

Proposed by Josse Colpaert (OpenERP)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-addons/trunk-wms-salestockwkf-jco
Merge into: lp:~openerp-dev/openobject-addons/trunk-wms
Diff against target: 331 lines (+105/-85)
5 files modified
sale/sale.py (+13/-51)
sale/sale_view.xml (+1/-0)
sale/sale_workflow.xml (+10/-4)
sale_stock/sale_stock.py (+57/-12)
stock/stock.py (+24/-18)
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/trunk-wms-salestockwkf-jco
Reviewer Review Type Date Requested Status
qdp (OpenERP) Pending
Review via email: mp+217566@code.launchpad.net

Description of the change

Change sale workflow to work as purchase workflow. The problem is that in the current workflow the sale order will not go to done when we invoice first and do the delivery afterwards.

Made also changes in procurement of cancel / split and last 2 commits make sure that the procurements of the sale order get cancelled also.

To post a comment you must log in.

Unmerged revisions

9702. By Josse Colpaert (OpenERP)

[IMP] When cancelling a sale order, cancel its procurements

9701. By Josse Colpaert (OpenERP)

[IMP] In case of ignoring exception of sale order, cancel procurements

9700. By Josse Colpaert (OpenERP)

[IMP] Adjust workflow not to take into account procurements when having procurements for services and do not throw error trying to cancel or split moves

9699. By Josse Colpaert (OpenERP)

[IMP] Clean test_state

9698. By Josse Colpaert (OpenERP)

[IMP] Do with procurements in sale order the same thing as with moves and purchase orders

9697. By Quentin (OpenERP) <email address hidden>

[REF] stock_account: removal of TODO statements

9696. By Quentin (OpenERP) <email address hidden>

[REF] stock: removal of TODO statements

9695. By Quentin (OpenERP) <email address hidden>

[MERGE] branch of jco fixing shipping exception in mrp + cancellation in chained moves

9694. By Quentin (OpenERP) <email address hidden>

[MERGE] stock_landed_costs: better usability

9693. By Quentin (OpenERP) <email address hidden>

[REF] stock_account: refactoring of do_change_standard_price() + removal of TODO statements

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'sale/sale.py'
2--- sale/sale.py 2014-04-16 15:05:51 +0000
3+++ sale/sale.py 2014-04-29 09:32:43 +0000
4@@ -228,7 +228,7 @@
5 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
6 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
7 'company_id': fields.many2one('res.company', 'Company'),
8- 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group'),
9+ 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', readonly=True),
10 }
11 _defaults = {
12 'date_order': fields.datetime.now,
13@@ -465,10 +465,13 @@
14 result['res_id'] = inv_ids and inv_ids[0] or False
15 return result
16
17+ def test_no_products(self, cr, uid, ids, context=None):
18+ for order in self.browse(cr, uid, ids, context=context):
19+ if not self.test_no_product(cr, uid, order, context=context):
20+ return False
21+ return True
22+
23 def test_no_product(self, cr, uid, order, context):
24- for line in order.order_line:
25- if line.product_id and (line.product_id.type<>'service'):
26- return False
27 return True
28
29 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
30@@ -663,6 +666,7 @@
31 res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
32 return any(res)
33
34+
35 def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
36 for sale_order in self.browse(cr, uid, ids, context=context):
37 self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
38@@ -714,53 +718,11 @@
39 order.write(val)
40 return True
41
42- # if mode == 'finished':
43- # returns True if all lines are done, False otherwise
44- # if mode == 'canceled':
45- # returns True if there is at least one canceled line, False otherwise
46- def test_state(self, cr, uid, ids, mode, *args):
47- assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
48- finished = True
49- canceled = False
50- write_done_ids = []
51- write_cancel_ids = []
52- for order in self.browse(cr, uid, ids, context={}):
53-
54- #TODO: Need to rethink what happens when cancelling
55- for line in order.order_line:
56- states = [x.state for x in line.procurement_ids]
57- cancel = states and all([x == 'cancel' for x in states])
58- doneorcancel = all([x in ('done', 'cancel') for x in states])
59- if cancel:
60- canceled = True
61- if line.state != 'exception':
62- write_cancel_ids.append(line.id)
63- if not doneorcancel:
64- finished = False
65- if doneorcancel and not cancel:
66- write_done_ids.append(line.id)
67-
68- if write_done_ids:
69- self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
70- if write_cancel_ids:
71- self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
72-
73- if mode == 'finished':
74- return finished
75- elif mode == 'canceled':
76- return canceled
77-
78-
79- def procurement_lines_get(self, cr, uid, ids, *args):
80- res = []
81- for order in self.browse(cr, uid, ids, context={}):
82- for line in order.order_line:
83- res += [x.id for x in line.procurement_ids]
84- return res
85-
86-
87-
88-
89+
90+
91+
92+ def action_ship_end(self, cr, uid, ids, context=None):
93+ return True
94
95 # TODO add a field price_unit_uos
96 # - update it on change product and unit price
97
98=== modified file 'sale/sale_view.xml'
99--- sale/sale_view.xml 2014-04-22 09:28:15 +0000
100+++ sale/sale_view.xml 2014-04-29 09:32:43 +0000
101@@ -202,6 +202,7 @@
102 <group name="sales_person" groups="base.group_user">
103 <field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'account.group_account_invoice', 'base.group_sale_salesman_all_leads']}"/>
104 <field groups="base.group_no_one" name="origin"/>
105+ <field name="procurement_group_id"/>
106 </group>
107 <group name="sale_pay">
108 <field name="payment_term" options="{'no_create': True}"/>
109
110=== modified file 'sale/sale_workflow.xml'
111--- sale/sale_workflow.xml 2014-01-24 09:27:58 +0000
112+++ sale/sale_workflow.xml 2014-04-29 09:32:43 +0000
113@@ -198,9 +198,11 @@
114 <record id="act_ship_end" model="workflow.activity">
115 <field name="wkf_id" ref="sale.wkf_sale"/>
116 <field name="name">ship_end</field>
117- <field name="kind">dummy</field>
118+ <field name="kind">function</field>
119+ <field name="action">action_ship_end()</field>
120 </record>
121
122+
123 <record id="act_ship_cancel" model="workflow.activity">
124 <field name="wkf_id" ref="sale.wkf_sale"/>
125 <field name="name">ship_cancel</field>
126@@ -262,9 +264,13 @@
127 <record id="trans_ship_ship_end" model="workflow.transition">
128 <field name="act_from" ref="act_ship"/>
129 <field name="act_to" ref="act_ship_end"/>
130- <field name="trigger_model">procurement.order</field>
131- <field name="trigger_expr_id">procurement_lines_get()</field>
132- <field name="condition">test_state('finished')</field>
133+ <field name="signal">ship_end</field>
134+ </record>
135+
136+ <record id="trans_ship_ship_end2" model="workflow.transition">
137+ <field name="act_from" ref="act_ship"/>
138+ <field name="act_to" ref="act_ship_end"/>
139+ <field name="condition">test_no_products()</field>
140 </record>
141
142 <record id="trans_ship_ship_except" model="workflow.transition">
143
144=== modified file 'sale_stock/sale_stock.py'
145--- sale_stock/sale_stock.py 2014-04-25 12:18:10 +0000
146+++ sale_stock/sale_stock.py 2014-04-29 09:32:43 +0000
147@@ -159,7 +159,6 @@
148 def action_cancel(self, cr, uid, ids, context=None):
149 if context is None:
150 context = {}
151- sale_order_line_obj = self.pool.get('sale.order.line')
152 proc_obj = self.pool.get('procurement.order')
153 stock_obj = self.pool.get('stock.picking')
154 for sale in self.browse(cr, uid, ids, context=context):
155@@ -168,6 +167,10 @@
156 raise osv.except_osv(
157 _('Cannot cancel sales order!'),
158 _('You must first cancel all delivery order(s) attached to this sales order.'))
159+ procurements = []
160+ for line in sale.order_line:
161+ procurements += [x.id for x in line.procurement_ids if x.state not in ('cancel', 'done') and x.product_id.type != 'service']
162+ proc_obj.cancel(cr, uid, procurements, context=context)
163 stock_obj.signal_button_cancel(cr, uid, [p.id for p in sale.picking_ids])
164 return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
165
166@@ -189,10 +192,11 @@
167 res.update({'move_type': order.picking_policy})
168 return res
169
170+
171 def action_ship_end(self, cr, uid, ids, context=None):
172 super(sale_order, self).action_ship_end(cr, uid, ids, context=context)
173 for order in self.browse(cr, uid, ids, context=context):
174- val = {'shipped': True}
175+ val = {}
176 if order.state == 'shipping_except':
177 val['state'] = 'progress'
178 if (order.order_policy == 'manual'):
179@@ -203,8 +207,25 @@
180 res = self.write(cr, uid, [order.id], val)
181 return True
182
183-
184-
185+ def test_no_product(self, cr, uid, order, context):
186+ for line in order.order_line:
187+ if line.product_id and (line.product_id.type<>'service'):
188+ return False
189+ return True
190+
191+ def test_procurements_done(self, cr, uid, ids, context=None):
192+ for sale in self.browse(cr, uid, ids, context=context):
193+ for line in sale.order_line:
194+ if not all([x.state == 'done' for x in line.procurement_ids if x.product_id.type != 'service']):
195+ return False
196+ return True
197+
198+ def test_procurements_except(self, cr, uid, ids, context=None):
199+ for sale in self.browse(cr, uid, ids, context=context):
200+ for line in sale.order_line:
201+ if any([x.state in ('exception', 'cancel') and x.move_ids for x in line.procurement_ids]):
202+ return True
203+ return False
204
205 def has_stockable_products(self, cr, uid, ids, *args):
206 for order in self.browse(cr, uid, ids):
207@@ -213,6 +234,19 @@
208 return True
209 return False
210
211+ def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
212+ """
213+ In case of ignoring the exception it should cancel the procurements
214+ """
215+ proc_obj = self.pool.get("procurement.order")
216+ exceptions = []
217+ for order in self.browse(cr, uid, ids, context=context):
218+ for line in order.order_line:
219+ exceptions += [x.id for x in line.procurement_ids if x.state == "exception"]
220+ if exceptions:
221+ proc_obj.cancel(cr, uid, exceptions, context=context)
222+ return super(sale_order, self).action_ignore_delivery_exception(cr, uid, ids, context=context)
223+
224
225 class sale_order_line(osv.osv):
226 _inherit = 'sale.order.line'
227@@ -375,17 +409,28 @@
228 res.update({'warning': warning})
229 return res
230
231+
232+class procurement_order(osv.osv):
233+ _inherit = 'procurement.order'
234+
235+ def write(self, cr, uid, ids, vals, context=None):
236+ if isinstance(ids, (int, long)):
237+ ids = [ids]
238+ res = super(procurement_order, self).write(cr, uid, ids, vals, context=context)
239+ from openerp import workflow
240+ if vals.get('state') in ['done', 'cancel', 'exception']:
241+ for proc in self.browse(cr, uid, ids, context=context):
242+ if proc.sale_line_id and proc.sale_line_id.order_id and proc.move_ids:
243+ order_id = proc.sale_line_id.order_id.id
244+ if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context):
245+ workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr)
246+ if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context):
247+ workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr)
248+ return res
249+
250 class stock_move(osv.osv):
251 _inherit = 'stock.move'
252
253- def action_cancel(self, cr, uid, ids, context=None):
254- sale_ids = []
255- for move in self.browse(cr, uid, ids, context=context):
256- if move.procurement_id and move.procurement_id.sale_line_id:
257- sale_ids.append(move.procurement_id.sale_line_id.order_id.id)
258- if sale_ids:
259- self.pool.get('sale.order').signal_ship_except(cr, uid, sale_ids)
260- return super(stock_move, self).action_cancel(cr, uid, ids, context=context)
261
262 def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
263 invoice_line_id = self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context)
264
265=== modified file 'stock/stock.py'
266--- stock/stock.py 2014-04-28 14:55:36 +0000
267+++ stock/stock.py 2014-04-29 09:32:43 +0000
268@@ -2139,31 +2139,37 @@
269 """
270 procurement_obj = self.pool.get('procurement.order')
271 context = context or {}
272+ procurements = []
273+ cancel_ids = []
274 for move in self.browse(cr, uid, ids, context=context):
275 if move.state == 'done':
276- raise osv.except_osv(_('Operation Forbidden!'),
277- _('You cannot cancel a stock move that has been set to \'Done\'.'))
278+ continue #Could have been propagated from a forced move
279+ cancel_ids.append(move.id)
280 if move.reserved_quant_ids:
281 self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context)
282 if context.get('cancel_procurement'):
283 if move.propagate:
284 procurement_ids = procurement_obj.search(cr, uid, [('move_dest_id', '=', move.id)], context=context)
285 procurement_obj.cancel(cr, uid, procurement_ids, context=context)
286- elif move.move_dest_id:
287- #cancel chained moves
288- if move.propagate:
289- self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
290- # If we have a long chain of moves to be cancelled, it is easier for the user to handle
291- # only the last procurement which will go into exception, instead of all procurements
292- # along the chain going into exception. We need to check if there are no split moves not cancelled however
293- if move.procurement_id:
294- proc = move.procurement_id
295- if all([x.state == 'cancel' for x in proc.move_ids if x.id != move.id]):
296- procurement_obj.write(cr, uid, [proc.id], {'state': 'cancel'})
297-
298- elif move.move_dest_id.state == 'waiting':
299- self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context)
300- return self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}, context=context)
301+ elif move.move_dest_id and move.propagate:
302+ #cancel chained moves
303+ self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
304+ # If we have a long chain of moves to be cancelled, it is easier for the user to handle
305+ # only the last procurement which will go into exception, instead of all procurements
306+ # along the chain going into exception. We need to check if there are no split moves not cancelled however
307+ if move.procurement_id:
308+ proc = move.procurement_id
309+ if all([x.state == 'cancel' for x in proc.move_ids if x.id != move.id]):
310+ procurement_obj.write(cr, uid, [proc.id], {'state': 'cancel'})
311+ elif move.procurement_id:
312+ procurements.append(move.procurement_id.id)
313+ if cancel_ids:
314+ res = self.write(cr, uid, cancel_ids, {'state': 'cancel', 'move_dest_id': False}, context=context)
315+ else:
316+ res = False
317+ if procurements:
318+ procurement_obj.check(cr, uid, procurements, context=context)
319+ return res
320
321 def _check_package_from_moves(self, cr, uid, ids, context=None):
322 pack_obj = self.pool.get("stock.quant.package")
323@@ -2354,7 +2360,7 @@
324 returns the ID of the backorder move created
325 """
326 if move.state in ('done', 'cancel'):
327- raise osv.except_osv(_('Error'), _('You cannot split a move done'))
328+ return False #Could have been propagated to a forced move
329 if move.state == 'draft':
330 #we restrict the split of a draft move because if not confirmed yet, it may be replaced by several other moves in
331 #case of phantom bom (with mrp module). And we don't want to deal with this complexity by copying the product that will explode.