Modifs
[odoo/odoo.git] / addons / mrp / mrp.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 #
6 # $Id$
7 #
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
13 # Service Company
14 #
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 #
29 ##############################################################################
30
31 from osv import fields
32 from osv import osv
33 import ir
34
35 import netsvc
36 import time
37 from mx import DateTime
38
39 #----------------------------------------------------------
40 # Workcenters
41 #----------------------------------------------------------
42 # capacity_hour : capacity per hour. default: 1.0. 
43 #          Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
44 # unit_per_cycle : how many units are produced for one cycle
45 #
46 # TODO: Work Center may be recursive ?
47 #
48 class mrp_workcenter(osv.osv):
49     _name = 'mrp.workcenter'
50     _description = 'Workcenter'
51     _columns = {
52         'name': fields.char('Name', size=64, required=True),
53         'active': fields.boolean('Active'),
54         'type': fields.selection([('machine','Machine'),('hr','Human Resource'),('tool','Tool')], 'Type', required=True),
55         'code': fields.char('Code', size=8),
56         'timesheet_id': fields.many2one('hr.timesheet.group', 'Timesheet'),
57         'note': fields.text('Description'),
58
59         'capacity_per_cycle': fields.float('Capacity per Cycle'),
60
61         'time_cycle': fields.float('Time for 1 cycle (hour)'),
62         'time_start': fields.float('Time before prod.'),
63         'time_stop': fields.float('Time after prod.'),
64         'time_efficiency': fields.float('Time Efficiency'),
65
66         'costs_hour': fields.float('Cost per hour'),
67         'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','=','expense')]),
68         'costs_cycle': fields.float('Cost per cycle'),
69         'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','=','expense')]),
70         'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
71         'costs_general_account_id': fields.many2one('account.account', 'General Account'),
72     }
73     _defaults = {
74         'active': lambda *a: 1,
75         'type': lambda *a: 'machine',
76         'time_efficiency': lambda *a: 1.0,
77         'capacity_per_cycle': lambda *a: 1.0,
78     }
79 mrp_workcenter()
80
81
82 class mrp_property_group(osv.osv):
83     _name = 'mrp.property.group'
84     _description = 'Property Group'
85     _columns = {
86         'name': fields.char('Property Group', size=64, required=True),
87         'description': fields.text('Description'),
88     }
89 mrp_property_group()
90
91 class mrp_property(osv.osv):
92     _name = 'mrp.property'
93     _description = 'Property'
94     _columns = {
95         'name': fields.char('Name', size=64, required=True),
96         'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True),
97         'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
98         'description': fields.text('Description'),
99     }
100     _defaults = {
101         'composition': lambda *a: 'min',
102     }
103 mrp_property()
104
105 class mrp_routing(osv.osv):
106     _name = 'mrp.routing'
107     _description = 'Routing'
108     _columns = {
109         'name': fields.char('Name', size=64, required=True),
110         'active': fields.boolean('Active'),
111         'code': fields.char('Code', size=8),
112
113         'note': fields.text('Description'),
114         'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Workcenters'),
115
116         'location_id': fields.many2one('stock.location', 'Production Location'),
117     }
118     _defaults = {
119         'active': lambda *a: 1,
120     }
121 mrp_routing()
122
123 class mrp_routing_workcenter(osv.osv):
124     _name = 'mrp.routing.workcenter'
125     _description = 'Routing workcenter usage'
126     _columns = {
127         'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
128         'name': fields.char('Name', size=64, required=True),
129         'sequence': fields.integer('Sequence'),
130         'cycle_nbr': fields.float('Number of cycle', required=True),
131         'hour_nbr': fields.float('Number of hours', required=True),
132         'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True),
133         'note': fields.text('Description')
134     }
135     _defaults = {
136         'cycle_nbr': lambda *a: 1.0,
137         'hour_nbr': lambda *a: 0.0,
138     }
139 mrp_routing_workcenter()
140
141 class mrp_bom(osv.osv):
142     _name = 'mrp.bom'
143     _description = 'Bill of Material'
144     _columns = {
145         'name': fields.char('Name', size=64, required=True),
146         'code': fields.char('Code', size=16),
147         'active': fields.boolean('Active'),
148         'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True, help="Use a phantom bill of material in lines that have a sub-bom and that have to be automatically computed in one line, without habing two production orders."),
149         'date_start': fields.date('Valid from'),
150         'date_stop': fields.date('Valid until'),
151         'sequence': fields.integer('Sequence'),
152         'position': fields.char('Internal Ref.', size=64),
153         'product_id': fields.many2one('product.product', 'Product', required=True),
154         'product_uos_qty': fields.float('Product UOS Qty'),
155         'product_uos': fields.many2one('product.uom', 'Product UOS'),
156         'product_qty': fields.float('Product Qty', required=True),
157         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
158         'product_rounding': fields.float('Product Rounding'),
159         'product_efficiency': fields.float('Product Efficiency', required=True),
160         'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
161         'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
162         'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of workcenters) to produce the finnished product. The routing is mainly used to compute workcenter costs during operations and to plan futur loads on workcenters based on production plannification."),
163         'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
164         'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
165         'revision_type': fields.selection([('numeric','numeric indices'),('alpha','alphabetical indices')], 'indice type')
166     }
167     _defaults = {
168         'active': lambda *a: 1,
169         'product_efficiency': lambda *a: 1.0,
170         'product_qty': lambda *a: 1.0,
171         'product_rounding': lambda *a: 1.0,
172         'type': lambda *a: 'normal',
173     }
174     _order = "sequence"
175     _sql_constraints = [
176         ('bom_qty_zero', 'CHECK (product_qty>0)',  'All product quantities must be greater than 0 !'),
177     ]
178     def _check_recursion(self, cr, uid, ids):
179         level = 500
180         while len(ids):
181             cr.execute('select distinct bom_id from mrp_bom where id in ('+','.join(map(str,ids))+')')
182             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
183             if not level:
184                 return False
185             level -= 1
186         return True
187     _constraints = [
188         (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
189     ]
190
191     
192     def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
193         if product_id:
194             prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
195             v = {'product_uom':prod.uom_id.id}
196             if not name:
197                 v['name'] = prod.name
198             return {'value': v}
199         return {}
200
201     def _bom_find(self, cr, uid, product_id, product_uom, properties = []):
202         bom_result = False
203         # Why searching on BoM without parent ?
204         cr.execute('select id from mrp_bom where product_id=%d and bom_id is null order by sequence', (product_id,))
205         ids = map(lambda x: x[0], cr.fetchall())
206         max_prop = 0
207         result = False
208         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
209             prop = 0
210             for prop_id in bom.property_ids:
211                 if prop_id.id in properties:
212                     prop+=1
213             if (prop>max_prop) or ((max_prop==0) and not result):
214                 result = bom.id
215         return result
216
217     def _bom_explode(self, cr, uid, bom, factor, properties, addthis=False, level=10):
218         factor = factor / (bom.product_efficiency or 1.0)
219         factor = rounding(factor, bom.product_rounding)
220         if factor<bom.product_rounding:
221             factor = bom.product_rounding
222         result = []
223         result2 = []
224         if bom.type=='phantom' and not bom.bom_lines:
225             newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
226             if newbom:
227                 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
228                 result = result + res[0]
229                 result2 = result2 + res[1]
230             else:
231                 return [],[]
232         else:
233             if addthis and not bom.bom_lines:
234                 result.append(
235                 {
236                     'name': bom.product_id.name,
237                     'product_id': bom.product_id.id,
238                     'product_qty': bom.product_qty * factor,
239                     'product_uom': bom.product_uom.id,
240                     'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
241                     'product_uos': bom.product_uos and bom.product_uos.id or False,
242                 })
243             if bom.routing_id:
244                 for wc_use in bom.routing_id.workcenter_lines:
245                     wc = wc_use.workcenter_id
246                     d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
247                     cycle = (d + (m and 1.0 or 0.0)) * wc_use.cycle_nbr
248                     result2.append({
249                         'name': bom.routing_id.name,
250                         'workcenter_id': wc.id,
251                         'sequence': level,
252                         'cycle': cycle,
253                         'hour': wc_use.hour_nbr + (wc.time_start+wc.time_stop+cycle*wc.time_cycle) * (wc.time_efficiency or 1.0),
254                     })
255             for bom2 in bom.bom_lines:
256                 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
257                 result = result + res[0]
258                 result2 = result2 + res[1]
259         return result, result2
260
261     def set_indices(self, cr, uid, ids, context = {}):
262         if not ids or (ids and not ids[0]):
263             return True 
264         res = self.read(cr, uid, ids, ['revision_ids', 'revision_type'])
265         rev_ids = res[0]['revision_ids']
266         idx = 1
267         new_idx = []
268         for rev_id in rev_ids:
269             if res[0]['revision_type'] == 'numeric':
270                 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : idx})
271             else:
272                 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : "%c"%(idx+96,)})
273             idx+=1
274         return True
275
276 mrp_bom()
277
278 class mrp_bom_revision(osv.osv):
279     _name = 'mrp.bom.revision'
280     _description = 'Bill of material revisions'
281     _columns = {
282         'name': fields.char('Modification name', size=64, required=True),
283         'description': fields.text('Description'),
284         'date': fields.date('Modification Date'),
285         'indice': fields.char('Revision', size=16),
286         'last_indice': fields.char('last indice', size=64),
287         'author_id': fields.many2one('res.users', 'Author'),
288         'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
289     }
290     
291     _defaults = {
292         'author_id': lambda x,y,z,c: z,
293         'date': lambda *a: time.strftime('%Y-%m-%d'),
294     }
295
296 mrp_bom_revision()
297
298 def rounding(f, r):
299     if not r:
300         return f
301     return round(f / r) * r
302
303 class mrp_production(osv.osv):
304     _name = 'mrp.production'
305     _description = 'Production'
306     _date_name  = 'date_planned'
307     _columns = {
308         'name': fields.char('Reference', size=64, required=True),
309         'origin': fields.char('Origin', size=64),
310         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
311
312         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
313         'product_qty': fields.float('Product Qty', required=True),
314         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
315         'product_uos_qty': fields.float('Product Qty'),
316         'product_uos': fields.many2one('product.uom', 'Product UOM'),
317
318         'location_src_id': fields.many2one('stock.location', 'Raw Products Location', required=True),
319         'location_dest_id': fields.many2one('stock.location', 'Finnished Products Location', required=True),
320
321         'date_planned': fields.date('Scheduled date', required=True),
322         'date_start': fields.datetime('Start Date'),
323         'date_finnished': fields.datetime('End Date'),
324
325         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
326
327         'picking_id': fields.many2one('stock.picking', 'Packing list', readonly=True),
328         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
329         'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
330
331         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
332         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
333         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Workcenters Utilisation'),
334
335         'state': fields.selection([('draft','Draft'),('picking_except', 'Packing Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','In Production'),('cancel','Canceled'),('done','Done')],'Status', readonly=True)
336     }
337     _defaults = {
338         'priority': lambda *a: '1',
339         'state': lambda *a: 'draft',
340         'date_planned': lambda *a: time.strftime('%Y-%m-%d'),
341         'product_qty':  lambda *a: 1.0,
342         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
343     }
344     _order = 'date_planned asc, priority desc';
345
346     def product_id_change(self, cr, uid, ids, product):
347         if not product:
348             return {}
349         res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
350         uom = res['uom_id'] and res['uom_id'][0]
351         result = {'product_uom':uom}
352         return {'value':result}
353
354     def action_picking_except(self, cr, uid, ids):
355         self.write(cr, uid, ids, {'state':'picking_except'})
356         return True
357
358     def action_compute(self, cr, uid, ids, properties=[]):
359         results = []
360         for production in self.browse(cr, uid, ids):
361             cr.execute('delete from mrp_production_product_line where production_id=%d', (production.id,))
362             cr.execute('delete from mrp_production_workcenter_line where production_id=%d', (production.id,))
363             bom_point = production.bom_id
364             bom_id = production.bom_id.id
365             if not bom_point:
366                 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
367                 if bom_id:
368                     self.write(cr, uid, [production.id], {'bom_id': bom_id})
369                     bom_point = self.pool.get('mrp.bom').browse(cr, uid, [bom_id])[0]
370
371             if not bom_id:
372                 raise osv.except_osv('Error', "Couldn't find bill of material for product")
373
374             #if bom_point.routing_id and bom_point.routing_id.location_id:
375             #   self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
376
377             factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
378             res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
379             results = res[0]
380             results2 = res[1]
381             for line in results:
382                 line['production_id'] = production.id
383                 self.pool.get('mrp.production.product.line').create(cr, uid, line)
384             for line in results2:
385                 line['production_id'] = production.id
386                 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
387         return len(results)
388
389     def action_cancel(self, cr, uid, ids):
390         for production in self.browse(cr, uid, ids):
391             if production.move_created_ids:
392                 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
393             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
394         self.write(cr, uid, ids, {'state':'cancel','move_lines':[(6,0,[])]})
395         return True
396
397     #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
398     #     between the end of the picking list and the call to this function
399     def action_ready(self, cr, uid, ids):
400         self.write(cr, uid, ids, {'state':'ready'})
401         for production in self.browse(cr, uid, ids):
402             if production.move_prod_id:
403                 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
404                         {'location_id':production.location_dest_id.id})
405         return True
406
407     #TODO Review materials in function in_prod and prod_end.
408     def action_production_end(self, cr, uid, ids):
409         move_ids = []
410         for production in self.browse(cr, uid, ids):
411             for res in production.move_lines:
412                 for move in production.move_created_ids:
413                     #XXX must use the orm
414                     cr.execute('INSERT INTO stock_move_history_ids \
415                             (parent_id, child_id) VALUES (%d,%d)',
416                             (res.id, move.id))
417                 move_ids.append(res.id)
418             if production.move_created_ids:
419                 #TODO There we should handle the residus move creation
420                 vals= {'state':'confirmed'}
421                 new_moves = [x.id for x in production.move_created_ids]
422                 self.pool.get('stock.move').write(cr, uid, new_moves, vals)
423             else:
424                 #XXX Why is it there ? Aren't we suppose to already have a created_move ?
425                 source = production.product_id.product_tmpl_id.property_stock_production.id
426                 vals = {
427                     'name':'PROD:'+production.name,
428                     'date_planned': production.date_planned,
429                     'product_id': production.product_id.id,
430                     'product_qty': production.product_qty,
431                     'product_uom': production.product_uom.id,
432                     'product_uos_qty': production.product_uos and production.product_uos_qty or False,
433                     'product_uos': production.product_uos and production.product_uos.id or False,
434                     'location_id': source,
435                     'location_dest_id': production.location_dest_id.id,
436                     'move_dest_id': production.move_prod_id.id,
437                     'state': 'confirmed'
438                 }
439                 new_moves = [self.pool.get('stock.move').create(cr, uid, vals)]
440                 self.write(cr, uid, [production.id],
441                         {'move_created_ids': [(6, 'WTF', new_moves)]})
442             if not production.date_finnished:
443                 self.write(cr, uid, [production.id],
444                         {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
445             self.pool.get('stock.move').check_assign(cr, uid, new_moves)
446             self.pool.get('stock.move').action_done(cr, uid, new_moves)
447             self._costs_generate(cr, uid, production)
448         self.pool.get('stock.move').action_done(cr, uid, move_ids)
449         self.write(cr,  uid, ids, {'state': 'done'})
450         return True
451
452     def _costs_generate(self, cr, uid, production):
453         amount = 0.0
454         for wc_line in production.workcenter_lines:
455             wc = wc_line.workcenter_id
456             if wc.costs_journal_id and wc.costs_general_account_id:
457                 value = wc_line.hour * wc.costs_hour
458                 account = wc.costs_hour_account_id.id
459                 if value and account:
460                     amount += value
461                     self.pool.get('account.analytic.line').create(cr, uid, {
462                         'name': wc_line.name+' (H)',
463                         'amount': value,
464                         'account_id': account,
465                         'general_account_id': wc.costs_general_account_id.id,
466                         'journal_id': wc.costs_journal_id.id,
467                         'code': wc.code
468                     } )
469             if wc.costs_journal_id and wc.costs_general_account_id:
470                 value = wc_line.cycle * wc.costs_cycle
471                 account = wc.costs_cycle_account_id.id
472                 if value and account:
473                     amount += value
474                     self.pool.get('account.analytic.line').create(cr, uid, {
475                         'name': wc_line.name+' (C)',
476                         'amount': value,
477                         'account_id': account,
478                         'general_account_id': wc.costs_general_account_id.id,
479                         'journal_id': wc.costs_journal_id.id,
480                         'code': wc.code
481                     } )
482         return amount
483
484     def action_in_production(self, cr, uid, ids):
485         move_ids = []
486         for production in self.browse(cr, uid, ids):
487             for res in production.move_lines:
488                 move_ids.append(res.id)
489             if not production.date_start:
490                 self.write(cr, uid, [production.id],
491                         {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
492         self.pool.get('stock.move').action_done(cr, uid, move_ids)
493         self.write(cr, uid, ids, {'state': 'in_production'})
494         return True
495
496     def test_if_product(self, cr, uid, ids):
497         res = True
498         for production in self.browse(cr, uid, ids):
499             if not production.product_lines:
500                 if not self.action_compute(cr, uid, [production.id]):
501                     res = False
502         return res
503
504     def _get_auto_picking(self, cr, uid, production):
505         return True
506
507     def action_confirm(self, cr, uid, ids):
508         picking_id=False
509         for production in self.browse(cr, uid, ids):
510             if not production.product_lines:
511                 self.action_compute(cr, uid, [production.id])
512                 production = self.browse(cr, uid, [production.id])[0]
513             routing_loc = None
514             pick_type = 'internal'
515             address_id = False
516             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
517                 routing_loc = production.bom_id.routing_id.location_id
518                 if routing_loc.usage<>'internal':
519                     pick_type = 'out'
520                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
521                 routing_loc = routing_loc.id
522             picking_id = self.pool.get('stock.picking').create(cr, uid, {
523                 'origin': (production.origin or '').split(':')[0] +':'+production.name,
524                 'type': pick_type,
525                 'move_type': 'one',
526                 'state': 'auto',
527                 'address_id': address_id,
528                 'auto_picking': self._get_auto_picking(cr, uid, production),
529             })
530             toconfirm = True
531
532             source = production.product_id.product_tmpl_id.property_stock_production.id
533             data = {
534                 'name':'PROD:'+production.name,
535                 'date_planned': production.date_planned,
536                 'product_id': production.product_id.id,
537                 'product_qty': production.product_qty,
538                 'product_uom': production.product_uom.id,
539                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
540                 'product_uos': production.product_uos and production.product_uos.id or False,
541                 'location_id': source,
542                 'location_dest_id': production.location_dest_id.id,
543                 'move_dest_id': production.move_prod_id.id,
544                 'state': 'waiting'
545             }
546             res_final_id = self.pool.get('stock.move').create(cr, uid, data)
547             
548             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 'WTF', [res_final_id])]})
549             moves = []
550             for line in production.product_lines:
551                 move_id=False
552                 newdate = production.date_planned
553                 if line.product_id.type in ('product', 'consu'):
554                     res_dest_id = self.pool.get('stock.move').create(cr, uid, {
555                         'name':'PROD:'+production.name,
556                         'date_planned': production.date_planned,
557                         'product_id': line.product_id.id,
558                         'product_qty': line.product_qty,
559                         'product_uom': line.product_uom.id,
560                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
561                         'product_uos': line.product_uos and line.product_uos.id or False,
562                         'location_id': routing_loc or production.location_src_id.id,
563                         'location_dest_id': source,
564                         'move_dest_id': res_final_id,
565                         'state': 'waiting',
566                     })
567                     moves.append(res_dest_id)
568                     move_id = self.pool.get('stock.move').create(cr, uid, {
569                         'name':'PROD:'+production.name,
570                         'picking_id':picking_id,
571                         'product_id': line.product_id.id,
572                         'product_qty': line.product_qty,
573                         'product_uom': line.product_uom.id,
574                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
575                         'product_uos': line.product_uos and line.product_uos.id or False,
576                         'date_planned': newdate,
577                         'move_dest_id': res_dest_id,
578                         'location_id': production.location_src_id.id,
579                         'location_dest_id': routing_loc or production.location_src_id.id,
580                         'state': 'waiting',
581                     })
582                 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
583                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
584                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
585                     'date_planned': newdate,
586                     'product_id': line.product_id.id,
587                     'product_qty': line.product_qty,
588                     'product_uom': line.product_uom.id,
589                     'product_uos_qty': line.product_uos and line.product_qty or False,
590                     'product_uos': line.product_uos and line.product_uos.id or False,
591                     'location_id': production.location_src_id.id,
592                     'procure_method': line.product_id.procure_method,
593                     'move_id': move_id,
594                 })
595                 wf_service = netsvc.LocalService("workflow")
596                 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
597             if toconfirm:
598                 wf_service = netsvc.LocalService("workflow")
599                 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
600             self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
601         return picking_id
602
603     def force_production(self, cr, uid, ids, *args):
604         pick_obj = self.pool.get('stock.picking')
605         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
606         return True
607
608 mrp_production()
609
610
611 class stock_move(osv.osv):
612     _name = 'stock.move'
613     _inherit = 'stock.move'
614
615     _columns = {
616             'production_id': fields.many2one('mrp.production', 'Production', select=True),
617             }
618
619 stock_move()
620
621 class mrp_production_workcenter_line(osv.osv):
622     _name = 'mrp.production.workcenter.line'
623     _description = 'Production workcenters used'
624     _columns = {
625         'name': fields.char('Name', size=64, required=True),
626         'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
627         'cycle': fields.float('Nbr of cycle'),
628         'hour': fields.float('Nbr of hour'),
629         'sequence': fields.integer('Sequence', required=True),
630         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
631     }
632     _defaults = {
633         'sequence': lambda *a: 1,
634         'hour': lambda *a: 0,
635         'cycle': lambda *a: 0,
636     }
637 mrp_production_workcenter_line()
638
639 class mrp_production_product_line(osv.osv):
640     _name = 'mrp.production.product.line'
641     _description = 'Production scheduled products'
642     _columns = {
643         'name': fields.char('Name', size=64, required=True),
644         'product_id': fields.many2one('product.product', 'Product', required=True),
645         'product_qty': fields.float('Product Qty', required=True),
646         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
647         'product_uos_qty': fields.float('Product UOS Qty'),
648         'product_uos': fields.many2one('product.uom', 'Product UOS'),
649         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
650     }
651 mrp_production_product_line()
652
653 # ------------------------------------------------------------------
654 # Procurement
655 # ------------------------------------------------------------------
656 #
657 # Produce, Buy or Find products and place a move
658 #     then wizard for picking lists & move
659 #
660 class mrp_procurement(osv.osv):
661     _name = "mrp.procurement"
662     _description = "Procurement"
663     _columns = {
664         'name': fields.char('Name', size=64, required=True),
665         'origin': fields.char('Origin', size=64),
666         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
667         'date_planned': fields.date('Scheduled date', required=True),
668         'date_close': fields.date('Date Closed'),
669         'product_id': fields.many2one('product.product', 'Product', required=True),
670         'product_qty': fields.float('Quantity', required=True),
671         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
672         'product_uos_qty': fields.float('UoS Quantity'),
673         'product_uos': fields.many2one('product.uom', 'Product UoS'),
674         'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
675
676         'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
677
678         'close_move': fields.boolean('Close Move at end', required=True),
679         'location_id': fields.many2one('stock.location', 'Location', required=True),
680         'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]}, readonly=True, required=True),
681
682         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
683         'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
684
685         'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
686
687         'message': fields.char('Latest error', size=64),
688         'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('exception','Exception'),('running','Running'),('cancel','Cancel'),('done','Done'),('waiting','Waiting')], 'Status')
689     }
690     _defaults = {
691         'state': lambda *a: 'draft',
692         'priority': lambda *a: '1',
693         'date_planned': lambda *a: time.strftime('%Y-%m-%d'),
694         'close_move': lambda *a: 0,
695         'procure_method': lambda *a: 'make_to_order',
696     }
697     def check_product(self, cr, uid, ids):
698         for procurement in self.browse(cr, uid, ids):
699             if procurement.product_id.type in ('product', 'consu'):
700                 return True
701         return False
702
703     def check_move_cancel(self, cr, uid, ids, context={}):
704         res = True
705         for procurement in self.browse(cr, uid, ids, context):
706             if procurement.move_id:
707                 if not procurement.move_id.state=='cancel':
708                     res = False
709         return res
710
711     def check_move_done(self, cr, uid, ids, context={}):
712         res = True
713         for proc in self.browse(cr, uid, ids, context):
714             if proc.move_id:
715                 if not proc.move_id.state=='done':
716                     res = False
717         return res
718
719     #
720     # This method may be overrided by objects that override mrp.procurment
721     # for computing their own purpose
722     #
723     def _quantity_compute_get(self, cr, uid, proc, context={}):
724         if proc.product_id.type=='product':
725             return proc.move_id.product_uos_qty
726         return False
727
728     def _uom_compute_get(self, cr, uid, proc, context={}):
729         if proc.product_id.type=='product':
730             if proc.move_id.product_uos:
731                 return proc.move_id.product_uos.id
732         return False
733
734     #
735     # Return the quantity of product shipped/produced/served, wich may be
736     # different from the planned quantity
737     #
738     def quantity_get(self, cr, uid, id, context={}):
739         proc = self.browse(cr, uid, id, context)
740         result = self._quantity_compute_get(cr, uid, proc, context)
741         if not result:
742             result = proc.product_qty
743         return result
744
745     def uom_get(self, cr, uid, id, context=None):
746         proc = self.browse(cr, uid, id, context)
747         result = self._uom_compute_get(cr, uid, proc, context)
748         if not result:
749             result = proc.product_uom.id
750         return result
751
752     def check_waiting(self, cr, uid, ids, context=[]):
753         for procurement in self.browse(cr, uid, ids, context=context):
754             if procurement.move_id and procurement.move_id.state=='auto':
755                 return True
756         return False
757
758     def check_produce_service(self, cr, uid, procurement, context=[]):
759         return True
760
761     def check_produce_product(self, cr, uid, procurement, context=[]):
762         properties = [x.id for x in procurement.property_ids]
763         bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
764         if not bom_id:
765             cr.execute('update mrp_procurement set message=%s where id=%d', ('No BoM defined for this product !', procurement.id))
766             return False
767         return True
768
769     def check_make_to_stock(self, cr, uid, ids, context={}):
770         ok = True
771         for procurement in self.browse(cr, uid, ids, context=context):
772             if procurement.product_id.type=='service':
773                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
774             else:
775                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
776         return ok
777
778     def check_produce(self, cr, uid, ids, context={}):
779         res = True
780         user = self.pool.get('res.users').browse(cr, uid, uid)
781         for procurement in self.browse(cr, uid, ids):
782             if procurement.product_id.product_tmpl_id.supply_method=='buy':
783                 if procurement.product_id.seller_ids:
784                     partner = procurement.product_id.seller_ids[0].name
785                     if user.company_id and user.company_id.partner_id:
786                         if partner.id == user.company_id.partner_id.id:
787                             return True
788                 return False
789             if procurement.product_id.product_tmpl_id.type=='service':
790                 res = res and self.check_produce_service(cr, uid, procurement, context)
791             else:
792                 res = res and self.check_produce_product(cr, uid, procurement, context)
793             if not res:
794                 return False
795         return res
796
797     def check_buy(self, cr, uid, ids):
798         user = self.pool.get('res.users').browse(cr, uid, uid)
799         for procurement in self.browse(cr, uid, ids):
800             if procurement.product_id.product_tmpl_id.supply_method=='produce':
801                 return False
802             if not procurement.product_id.seller_ids:
803                 cr.execute('update mrp_procurement set message=%s where id=%d', ('No supplier defined for this product !', procurement.id))
804                 return False
805             partner = procurement.product_id.seller_ids[0].name
806             if user.company_id and user.company_id.partner_id:
807                 if partner.id == user.company_id.partner_id.id:
808                     return False
809             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
810             if not address_id:
811                 cr.execute('update mrp_procurement set message=%s where id=%d', ('No address defined for the supplier', procurement.id))
812                 return False
813         return True
814
815     def test_cancel(self, cr, uid, ids):
816         for record in self.browse(cr, uid, ids):
817             if record.move_id and record.move_id.state=='cancel':
818                 return True
819         return False
820
821     def action_confirm(self, cr, uid, ids):
822         for procurement in self.browse(cr, uid, ids):
823             if procurement.product_id.type in ('product', 'consu'):
824                 if not procurement.move_id:
825                     source = procurement.location_id.id
826                     if procurement.procure_method=='make_to_order':
827                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
828                     id = self.pool.get('stock.move').create(cr, uid, {
829                         'name': 'PROC:'+procurement.name,
830                         'location_id': source,
831                         'location_dest_id': procurement.location_id.id,
832                         'product_id': procurement.product_id.id,
833                         'product_qty':procurement.product_qty,
834                         'product_uom': procurement.product_uom.id,
835                         'date_planned': procurement.date_planned,
836                         'state':'confirmed',
837                     })
838                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
839                 else:
840                     # TODO: check this
841                     if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('waiting',):
842                         id = self.pool.get('stock.move').write(cr, uid, [procurement.move_id.id], {'state':'confirmed'})
843         self.write(cr, uid, ids, {'state':'confirmed','message':''})
844         return True
845
846     def action_move_assigned(self, cr, uid, ids):
847         self.write(cr, uid, ids, {'state':'running','message':'from stock: products assigned.'})
848         return True
849
850     def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
851         return True
852
853     def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
854         ok = True
855         if procurement.move_id:
856             id = procurement.move_id.id
857             if not (procurement.move_id.state in ('done','assigned','cancel')):
858                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
859                 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%d', (procurement.product_id.id,))
860                 if not cr.fetchone()[0]:
861                     cr.execute('update mrp_procurement set message=%s where id=%d', ('from stock and no minimum orderpoint rule defined', procurement.id))
862         return ok
863
864     def action_produce_assign_service(self, cr, uid, ids, context={}):
865         for procurement in self.browse(cr, uid, ids):
866             self.write(cr, uid, [procurement.id], {'state':'running'})
867         return True
868
869     def action_produce_assign_product(self, cr, uid, ids, context={}):
870         produce_id = False
871         for procurement in self.browse(cr, uid, ids):
872             res_id = procurement.move_id.id
873             loc_id = procurement.location_id.id
874             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d') - DateTime.RelativeDateTime(days=procurement.product_id.product_tmpl_id.produce_delay or 0.0)
875             produce_id = self.pool.get('mrp.production').create(cr, uid, {
876                 'origin': procurement.origin,
877                 'product_id': procurement.product_id.id,
878                 'product_qty': procurement.product_qty,
879                 'product_uom': procurement.product_uom.id,
880                 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
881                 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
882                 'location_src_id': procurement.location_id.id,
883                 'location_dest_id': procurement.location_id.id,
884                 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
885                 'date_planned': newdate,
886                 'move_prod_id': res_id,
887             })
888             self.write(cr, uid, [procurement.id], {'state':'running'})
889             bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
890                     [produce_id], properties=[x.id for x in procurement.property_ids])
891             wf_service = netsvc.LocalService("workflow")
892             wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
893         return produce_id
894
895     def action_po_assign(self, cr, uid, ids):
896         purchase_id = False
897         for procurement in self.browse(cr, uid, ids):
898             res_id = procurement.move_id.id
899             partner = procurement.product_id.seller_ids[0].name
900             partner_id = partner.id
901             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
902             pricelist_id = partner.property_product_pricelist_purchase.id
903
904             uom_id = procurement.product_id.uom_po_id.id
905
906             qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
907             if procurement.product_id.seller_ids[0].qty:
908                 qty=max(qty,procurement.product_id.seller_ids[0].qty)
909
910             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
911
912             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d') - DateTime.RelativeDateTime(days=procurement.product_id.product_tmpl_id.seller_delay or 0.0)
913             line = {
914                 'name': procurement.product_id.name,
915                 'product_qty': qty,
916                 'product_id': procurement.product_id.id,
917                 'product_uom': uom_id,
918                 'price_unit': price,
919                 'date_planned': newdate.strftime('%Y-%m-%d'),
920                 'taxes_id': [(6, 0, [x.id for x in procurement.product_id.product_tmpl_id.supplier_taxes_id])],
921                 'move_dest_id': res_id,
922             }
923
924             purchase_id = self.pool.get('purchase.order').create(cr, uid, {
925                 'origin': procurement.origin,
926                 'partner_id': partner_id,
927                 'partner_address_id': address_id,
928                 'location_id': procurement.location_id.id,
929                 'pricelist_id': pricelist_id,
930                 'order_line': [(0,0,line)]
931             })
932             self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
933         return purchase_id
934
935     def action_cancel(self, cr, uid, ids):
936         todo = []
937         for proc in self.browse(cr, uid, ids):
938             if proc.move_id:
939                 todo.append(proc.move_id.id)
940         if len(todo):
941             self.pool.get('stock.move').action_cancel(cr, uid, [proc.move_id.id])
942         self.write(cr, uid, ids, {'state':'cancel'})
943
944         wf_service = netsvc.LocalService("workflow")
945         for id in ids:
946             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
947
948         return True
949
950     def action_check_finnished(self, cr, uid, ids):
951         return True
952
953     def action_check(self, cr, uid, ids):
954         ok = False
955         for procurement in self.browse(cr, uid, ids):
956             if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
957                 self.action_done(cr, uid, [procurement.id])
958                 ok = True
959         return ok
960
961     def action_done(self, cr, uid, ids):
962         for procurement in self.browse(cr, uid, ids):
963             if procurement.move_id:
964                 if procurement.close_move and (procurement.move_id.state <> 'done'):
965                     self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
966         res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
967
968         wf_service = netsvc.LocalService("workflow")
969         for id in ids:
970             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
971         return res
972     def run_scheduler(self, cr, uid, user_id=False, schedule_cycle=1.0,\
973             po_cycle=1.0, po_lead=1.0, security_lead=50.0, picking_lead=1.0,\
974             automatic=False, use_new_cursor=False, context=None):
975         '''
976         use_new_cursor: False or the dbname
977         '''
978         if not context:
979             context={}
980         self.run_procure_confirm(cr, uid, schedule_cycle=schedule_cycle,\
981                 po_cycle=po_cycle, po_lead=po_lead, security_lead=security_lead,\
982                 picking_lead=picking_lead, user_id=user_id,\
983                 use_new_cursor=use_new_cursor, context=context)
984         self.run_orderpoint_confirm(cr, uid, automatic=automatic,\
985                 use_new_cursor=use_new_cursor, context=context, user_id=user_id)
986
987     def run_procure_confirm(self, cr, uid, user_id=False, schedule_cycle=1.0,\
988             po_cycle=1.0, po_lead=1.0, security_lead=50.0, picking_lead=1.0, \
989             use_new_cursor=False, context=None):
990         '''
991         use_new_cursor: False or the dbname
992         '''
993         from wizard.schedulers import _procure_confirm
994         if not context:
995             context={}
996         _procure_confirm(self, cr, uid, schedule_cycle=schedule_cycle,\
997                 po_cycle=po_cycle, po_lead=po_lead, security_lead=security_lead,\
998                 picking_lead=picking_lead, user_id=user_id, use_new_cursor=use_new_cursor,\
999                 context=context)
1000
1001     def run_orderpoint_confirm(self, cr, uid, automatic=False, use_new_cursor=False,\
1002             context=None, user_id=False):
1003         '''
1004         use_new_cursor: False or the dbname
1005         '''
1006         from wizard.schedulers import _procure_orderpoint_confirm
1007         if not context:
1008             context={}
1009         _procure_orderpoint_confirm(self, cr, uid, automatic=automatic,\
1010                 use_new_cursor=use_new_cursor, context=context, user_id=user_id)
1011 mrp_procurement()
1012
1013
1014 class stock_warehouse_orderpoint(osv.osv):
1015     _name = "stock.warehouse.orderpoint"
1016     _description = "Orderpoint minimum rule"
1017     _columns = {
1018         'name': fields.char('Name', size=32, required=True),
1019         'active': fields.boolean('Active'),
1020         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1021         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1022         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1023         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1024         'product_min_qty': fields.float('Min Quantity', required=True),
1025         'product_max_qty': fields.float('Max Quantity', required=True),
1026         'qty_multiple': fields.integer('Qty Multiple', required=True),
1027         'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order')
1028     }
1029     _defaults = {
1030         'active': lambda *a: 1,
1031         'logic': lambda *a: 'max',
1032         'qty_multiple': lambda *a: 1,
1033         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1034         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1035     }
1036     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1037         if product_id:
1038             prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
1039             v = {'product_uom':prod.uom_id.id}
1040             return {'value': v}
1041         return {}
1042 stock_warehouse_orderpoint()
1043
1044
1045 class StockMove(osv.osv):
1046     _inherit = 'stock.move'
1047     _columns = {
1048         'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1049     }
1050     def _action_explode(self, cr, uid, move, context={}):
1051         if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1052             bis = self.pool.get('mrp.bom').search(cr, uid, [
1053                 ('product_id','=',move.product_id.id),
1054                 ('bom_id','=',False),
1055                 ('type','=','phantom')])
1056             if bis:
1057                 factor = move.product_qty
1058                 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1059                 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1060                 dest = move.product_id.product_tmpl_id.property_stock_production.id
1061                 state = 'confirmed'
1062                 if move.state=='assigned':
1063                     state='assigned'
1064                 for line in res[0]:
1065                     valdef = {
1066                         'picking_id': move.picking_id.id,
1067                         'product_id': line['product_id'],
1068                         'product_uom': line['product_uom'],
1069                         'product_qty': line['product_qty'],
1070                         'product_uos': line['product_uos'],
1071                         'product_uos_qty': line['product_uos_qty'],
1072                         'move_dest_id': move.id,
1073                         'state': state,
1074                         'location_dest_id': dest,
1075                         'move_history_ids': [(6,0,[move.id])],
1076                         'move_history_ids2': [(6,0,[])],
1077                         'procurements': []
1078                     }
1079                     mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1080                     prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1081                     proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1082                         'name': (move.picking_id.origin or ''),
1083                         'origin': (move.picking_id.origin or ''),
1084                         'date_planned': move.date_planned,
1085                         'product_id': line['product_id'],
1086                         'product_qty': line['product_qty'],
1087                         'product_uom': line['product_uom'],
1088                         'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1089                         'product_uos':  line['product_uos'],
1090                         'location_id': move.location_id.id,
1091                         'procure_method': prodobj.procure_method,
1092                         'move_id': mid,
1093                     })
1094                     wf_service = netsvc.LocalService("workflow")
1095                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1096                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1097                     'location_id': move.location_dest_id.id,
1098                     'auto_validate': True,
1099                     'picking_id': False,
1100                     'location_id': dest,
1101                     'state': 'waiting'
1102                 })
1103                 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1104                     wf_service = netsvc.LocalService("workflow")
1105                     wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1106         return True
1107 StockMove()
1108
1109
1110 class StockPicking(osv.osv):
1111     _inherit = 'stock.picking'
1112
1113     def test_finnished(self, cursor, user, ids):
1114         wf_service = netsvc.LocalService("workflow")
1115         res = super(StockPicking, self).test_finnished(cursor, user, ids)
1116         for picking in self.browse(cursor, user, ids):
1117             for move in picking.move_lines:
1118                 if move.state == 'done' and move.procurements:
1119                     for procurement in move.procurements:
1120                         wf_service.trg_validate(user, 'mrp.procurement',
1121                                 procurement.id, 'button_check', cursor)
1122         return res
1123
1124     #
1125     # Explode picking by replacing phantom BoMs
1126     #
1127     def action_explode(self, cr, uid, ids, *args):
1128         for pick in self.browse(cr, uid, ids):
1129             for move in pick.move_lines:
1130                 self.pool.get('stock.move')._action_explode(cr, uid, move)
1131         return True
1132
1133 StockPicking()
1134
1135 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1136