1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
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
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.
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.
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.
29 ##############################################################################
31 from osv import fields
37 from mx import DateTime
39 #----------------------------------------------------------
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
46 # TODO: Work Center may be recursive ?
48 class mrp_workcenter(osv.osv):
49 _name = 'mrp.workcenter'
50 _description = 'Workcenter'
52 'name': fields.char('Workcenter 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=16),
56 'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="The normal working time of the workcenter."),
57 'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
59 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operation this workcenter can do in parallel. If this workcenter represent a team of 5 workers, the capacity per cycle is 5."),
61 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
62 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
63 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
64 'time_efficiency': fields.float('Time Efficiency', help="Factor that multiplies all times expressed in the workcenter."),
66 'costs_hour': fields.float('Cost per hour'),
67 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
68 help="Complete this only if you want automatic analytic accounting entries on production orders."),
69 'costs_cycle': fields.float('Cost per cycle'),
70 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
71 help="Complete this only if you want automatic analytic accounting entries on production orders."),
72 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
73 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
76 'active': lambda *a: 1,
77 'type': lambda *a: 'machine',
78 'time_efficiency': lambda *a: 1.0,
79 'capacity_per_cycle': lambda *a: 1.0,
84 class mrp_property_group(osv.osv):
85 _name = 'mrp.property.group'
86 _description = 'Property Group'
88 'name': fields.char('Property Group', size=64, required=True),
89 'description': fields.text('Description'),
93 class mrp_property(osv.osv):
94 _name = 'mrp.property'
95 _description = 'Property'
97 'name': fields.char('Name', size=64, required=True),
98 'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
99 'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
100 'description': fields.text('Description'),
103 'composition': lambda *a: 'min',
107 class mrp_routing(osv.osv):
108 _name = 'mrp.routing'
109 _description = 'Routing'
111 'name': fields.char('Name', size=64, required=True),
112 'active': fields.boolean('Active'),
113 'code': fields.char('Code', size=8),
115 'note': fields.text('Description'),
116 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Workcenters'),
118 'location_id': fields.many2one('stock.location', 'Production Location',
119 help="Keep empty if you produce at the location where the finnished products are needed." \
120 "Put a location if you produce at a fixed location. This can be a partner location " \
121 "if you subcontract the manufacturing operations."
125 'active': lambda *a: 1,
129 class mrp_routing_workcenter(osv.osv):
130 _name = 'mrp.routing.workcenter'
131 _description = 'Routing workcenter usage'
133 'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
134 'name': fields.char('Name', size=64, required=True),
135 'sequence': fields.integer('Sequence'),
136 'cycle_nbr': fields.float('Number of Cycle', required=True,
137 help="A cycle is defined in the workcenter definition."),
138 'hour_nbr': fields.float('Number of Hours', required=True),
139 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True),
140 'note': fields.text('Description')
143 'cycle_nbr': lambda *a: 1.0,
144 'hour_nbr': lambda *a: 0.0,
146 mrp_routing_workcenter()
148 class mrp_bom(osv.osv):
150 _description = 'Bill of Material'
151 def _child_compute(self, cr, uid, ids, name, arg, context={}):
153 for bom in self.browse(cr, uid, ids, context=context):
154 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
155 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
156 if bom.type=='phantom' or ok:
157 sids = self.pool.get('mrp.bom').search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
159 bom2 = self.pool.get('mrp.bom').browse(cr, uid, sids[0], context=context)
160 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
162 def _compute_type(self, cr, uid, ids, field_name, arg, context):
163 res = dict(map(lambda x: (x,''), ids))
164 for line in self.browse(cr, uid, ids):
165 if line.type=='phantom' and not line.bom_id:
168 if line.bom_lines or line.type=='phantom':
170 if line.product_id.supply_method=='produce':
171 if line.product_id.procure_method=='make_to_stock':
172 res[line.id] = 'stock'
174 res[line.id] = 'order'
177 'name': fields.char('Name', size=64, required=True),
178 'code': fields.char('Code', size=16),
179 'active': fields.boolean('Active'),
180 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True, help=
181 "Use a phantom bill of material in raw materials lines that have to be " \
182 "automatically computed in on eproduction order and not one per level." \
183 "If you put \"Phantom/Set\" at the root level of a bill of material " \
184 "it is considered as a set or pack: the products are replaced by the components " \
185 "between the sale order to the picking without going through the production order." \
186 "The normal BoM will generate one production order per BoM level."),
187 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
188 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
189 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
190 'sequence': fields.integer('Sequence'),
191 'position': fields.char('Internal Ref.', size=64, help="Reference to a position in an external plan."),
192 'product_id': fields.many2one('product.product', 'Product', required=True),
193 'product_uos_qty': fields.float('Product UOS Qty'),
194 'product_uos': fields.many2one('product.uom', 'Product UOS'),
195 'product_qty': fields.float('Product Qty', required=True),
196 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
197 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity. For integer only values, put 1.0"),
198 'product_efficiency': fields.float('Product Efficiency', required=True, help="Efficiency on the production. A factor of 0.9 means a loss of 10% in the production."),
199 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
200 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
201 '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."),
202 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
203 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
204 'revision_type': fields.selection([('numeric','numeric indices'),('alpha','alphabetical indices')], 'indice type'),
205 'child_ids': fields.function(_child_compute,relation='mrp.bom', method=True, string="BoM Hyerarchy", type='many2many'),
206 'child_complete_ids': fields.function(_child_compute,relation='mrp.bom', method=True, string="BoM Hyerarchy", type='many2many')
209 'active': lambda *a: 1,
210 'product_efficiency': lambda *a: 1.0,
211 'product_qty': lambda *a: 1.0,
212 'product_rounding': lambda *a: 1.0,
213 'type': lambda *a: 'normal',
217 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
218 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
221 def _check_recursion(self, cr, uid, ids):
224 cr.execute('select distinct bom_id from mrp_bom where id in ('+','.join(map(str,ids))+')')
225 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
231 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
235 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
237 prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
238 v = {'product_uom':prod.uom_id.id}
240 v['name'] = prod.name
244 def _bom_find(self, cr, uid, product_id, product_uom, properties = []):
246 # Why searching on BoM without parent ?
247 cr.execute('select id from mrp_bom where product_id=%d and bom_id is null order by sequence', (product_id,))
248 ids = map(lambda x: x[0], cr.fetchall())
251 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
253 for prop_id in bom.property_ids:
254 if prop_id.id in properties:
256 if (prop>max_prop) or ((max_prop==0) and not result):
260 def _bom_explode(self, cr, uid, bom, factor, properties, addthis=False, level=10):
261 factor = factor / (bom.product_efficiency or 1.0)
262 factor = rounding(factor, bom.product_rounding)
263 if factor<bom.product_rounding:
264 factor = bom.product_rounding
267 if bom.type=='phantom' and not bom.bom_lines:
268 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
270 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
271 result = result + res[0]
272 result2 = result2 + res[1]
276 if addthis and not bom.bom_lines:
279 'name': bom.product_id.name,
280 'product_id': bom.product_id.id,
281 'product_qty': bom.product_qty * factor,
282 'product_uom': bom.product_uom.id,
283 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
284 'product_uos': bom.product_uos and bom.product_uos.id or False,
287 for wc_use in bom.routing_id.workcenter_lines:
288 wc = wc_use.workcenter_id
289 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
290 cycle = (d + (m and 1.0 or 0.0)) * wc_use.cycle_nbr
292 'name': bom.routing_id.name,
293 'workcenter_id': wc.id,
296 'hour': wc_use.hour_nbr + (wc.time_start+wc.time_stop+cycle*wc.time_cycle) * (wc.time_efficiency or 1.0),
298 for bom2 in bom.bom_lines:
299 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
300 result = result + res[0]
301 result2 = result2 + res[1]
302 return result, result2
304 def set_indices(self, cr, uid, ids, context = {}):
305 if not ids or (ids and not ids[0]):
307 res = self.read(cr, uid, ids, ['revision_ids', 'revision_type'])
308 rev_ids = res[0]['revision_ids']
311 for rev_id in rev_ids:
312 if res[0]['revision_type'] == 'numeric':
313 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : idx})
315 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : "%c"%(idx+96,)})
321 class mrp_bom_revision(osv.osv):
322 _name = 'mrp.bom.revision'
323 _description = 'Bill of material revisions'
325 'name': fields.char('Modification name', size=64, required=True),
326 'description': fields.text('Description'),
327 'date': fields.date('Modification Date'),
328 'indice': fields.char('Revision', size=16),
329 'last_indice': fields.char('last indice', size=64),
330 'author_id': fields.many2one('res.users', 'Author'),
331 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
335 'author_id': lambda x,y,z,c: z,
336 'date': lambda *a: time.strftime('%Y-%m-%d'),
344 return round(f / r) * r
346 class mrp_production(osv.osv):
347 _name = 'mrp.production'
348 _description = 'Production'
349 _date_name = 'date_planned'
351 def _get_sale_order(self,cr,uid,ids,field_name=False):
352 move_obj=self.pool.get('stock.move')
353 def get_parent_move(move_id):
354 move = move_obj.browse(cr,uid,move_id)
355 if move.move_dest_id:
356 return get_parent_move(move.move_dest_id.id)
358 productions=self.read(cr,uid,ids,['id','move_prod_id'])
360 for production in productions:
361 res[production['id']]=False
362 if production.get('move_prod_id',False):
363 parent_move_line=get_parent_move(production['move_prod_id'][0])
365 move = move_obj.browse(cr,uid,parent_move_line)
366 if field_name=='name':
367 res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.name or False
368 if field_name=='client_order_ref':
369 res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.client_order_ref or False
372 def _sale_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
373 return self._get_sale_order(cr,uid,ids,field_name='name')
375 def _sale_ref_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
376 return self._get_sale_order(cr,uid,ids,field_name='client_order_ref')
379 'name': fields.char('Reference', size=64, required=True),
380 'origin': fields.char('Origin', size=64),
381 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
383 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
384 'product_qty': fields.float('Product Qty', required=True),
385 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
386 'product_uos_qty': fields.float('Product Qty'),
387 'product_uos': fields.many2one('product.uom', 'Product UOM'),
389 'location_src_id': fields.many2one('stock.location', 'Raw Products Location', required=True,
390 help="Location where the system will look for products used in raw materials."),
391 'location_dest_id': fields.many2one('stock.location', 'Finnished Products Location', required=True,
392 help="Location where the system will stock the finnished products."),
394 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
395 'date_start': fields.datetime('Start Date'),
396 'date_finnished': fields.datetime('End Date'),
398 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
400 'picking_id': fields.many2one('stock.picking', 'Packing list', readonly=True,
401 help="This is the internal picking list take bring the raw materials to the production plan."),
402 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
403 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
405 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
406 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
407 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Workcenters Utilisation'),
409 '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),
410 'sale_name': fields.function(_sale_name_calc, method=True, type='char', string='Sale Name'),
411 'sale_ref': fields.function(_sale_ref_calc, method=True, type='char', string='Sale Ref'),
414 'priority': lambda *a: '1',
415 'state': lambda *a: 'draft',
416 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
417 'product_qty': lambda *a: 1.0,
418 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
420 _order = 'date_planned asc, priority desc';
422 def location_id_change(self, cr, uid, ids, src, dest, context={}):
426 return {'value': {'location_dest_id': src}}
429 def product_id_change(self, cr, uid, ids, product):
432 res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
433 uom = res['uom_id'] and res['uom_id'][0]
434 result = {'product_uom':uom}
435 return {'value':result}
437 def action_picking_except(self, cr, uid, ids):
438 self.write(cr, uid, ids, {'state':'picking_except'})
441 def action_compute(self, cr, uid, ids, properties=[]):
443 for production in self.browse(cr, uid, ids):
444 cr.execute('delete from mrp_production_product_line where production_id=%d', (production.id,))
445 cr.execute('delete from mrp_production_workcenter_line where production_id=%d', (production.id,))
446 bom_point = production.bom_id
447 bom_id = production.bom_id.id
449 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
451 self.write(cr, uid, [production.id], {'bom_id': bom_id})
452 bom_point = self.pool.get('mrp.bom').browse(cr, uid, [bom_id])[0]
455 raise osv.except_osv('Error', "Couldn't find bill of material for product")
457 #if bom_point.routing_id and bom_point.routing_id.location_id:
458 # self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
460 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
461 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
465 line['production_id'] = production.id
466 self.pool.get('mrp.production.product.line').create(cr, uid, line)
467 for line in results2:
468 line['production_id'] = production.id
469 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
472 def action_cancel(self, cr, uid, ids):
473 for production in self.browse(cr, uid, ids):
474 if production.move_created_ids:
475 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
476 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
477 self.write(cr, uid, ids, {'state':'cancel','move_lines':[(6,0,[])]})
480 #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
481 # between the end of the picking list and the call to this function
482 def action_ready(self, cr, uid, ids):
483 self.write(cr, uid, ids, {'state':'ready'})
484 for production in self.browse(cr, uid, ids):
485 if production.move_prod_id:
486 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
487 {'location_id':production.location_dest_id.id})
490 #TODO Review materials in function in_prod and prod_end.
491 def action_production_end(self, cr, uid, ids):
493 for production in self.browse(cr, uid, ids):
494 for res in production.move_lines:
495 for move in production.move_created_ids:
496 #XXX must use the orm
497 cr.execute('INSERT INTO stock_move_history_ids \
498 (parent_id, child_id) VALUES (%d,%d)',
500 move_ids.append(res.id)
501 if production.move_created_ids:
502 #TODO There we should handle the residus move creation
503 vals= {'state':'confirmed'}
504 new_moves = [x.id for x in production.move_created_ids]
505 self.pool.get('stock.move').write(cr, uid, new_moves, vals)
507 #XXX Why is it there ? Aren't we suppose to already have a created_move ?
508 source = production.product_id.product_tmpl_id.property_stock_production.id
510 'name':'PROD:'+production.name,
511 'date_planned': production.date_planned,
512 'product_id': production.product_id.id,
513 'product_qty': production.product_qty,
514 'product_uom': production.product_uom.id,
515 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
516 'product_uos': production.product_uos and production.product_uos.id or False,
517 'location_id': source,
518 'location_dest_id': production.location_dest_id.id,
519 'move_dest_id': production.move_prod_id.id,
522 new_moves = [self.pool.get('stock.move').create(cr, uid, vals)]
523 self.write(cr, uid, [production.id],
524 {'move_created_ids': [(6, 'WTF', new_moves)]})
525 if not production.date_finnished:
526 self.write(cr, uid, [production.id],
527 {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
528 self.pool.get('stock.move').check_assign(cr, uid, new_moves)
529 self.pool.get('stock.move').action_done(cr, uid, new_moves)
530 self._costs_generate(cr, uid, production)
531 self.pool.get('stock.move').action_done(cr, uid, move_ids)
532 self.write(cr, uid, ids, {'state': 'done'})
535 def _costs_generate(self, cr, uid, production):
537 for wc_line in production.workcenter_lines:
538 wc = wc_line.workcenter_id
539 if wc.costs_journal_id and wc.costs_general_account_id:
540 value = wc_line.hour * wc.costs_hour
541 account = wc.costs_hour_account_id.id
542 if value and account:
544 self.pool.get('account.analytic.line').create(cr, uid, {
545 'name': wc_line.name+' (H)',
547 'account_id': account,
548 'general_account_id': wc.costs_general_account_id.id,
549 'journal_id': wc.costs_journal_id.id,
552 if wc.costs_journal_id and wc.costs_general_account_id:
553 value = wc_line.cycle * wc.costs_cycle
554 account = wc.costs_cycle_account_id.id
555 if value and account:
557 self.pool.get('account.analytic.line').create(cr, uid, {
558 'name': wc_line.name+' (C)',
560 'account_id': account,
561 'general_account_id': wc.costs_general_account_id.id,
562 'journal_id': wc.costs_journal_id.id,
567 def action_in_production(self, cr, uid, ids):
569 for production in self.browse(cr, uid, ids):
570 for res in production.move_lines:
571 move_ids.append(res.id)
572 if not production.date_start:
573 self.write(cr, uid, [production.id],
574 {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
575 self.pool.get('stock.move').action_done(cr, uid, move_ids)
576 self.write(cr, uid, ids, {'state': 'in_production'})
579 def test_if_product(self, cr, uid, ids):
581 for production in self.browse(cr, uid, ids):
582 if not production.product_lines:
583 if not self.action_compute(cr, uid, [production.id]):
587 def _get_auto_picking(self, cr, uid, production):
590 def action_confirm(self, cr, uid, ids):
592 for production in self.browse(cr, uid, ids):
593 if not production.product_lines:
594 self.action_compute(cr, uid, [production.id])
595 production = self.browse(cr, uid, [production.id])[0]
597 pick_type = 'internal'
599 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
600 routing_loc = production.bom_id.routing_id.location_id
601 if routing_loc.usage<>'internal':
603 address_id = routing_loc.address_id and routing_loc.address_id.id or False
604 routing_loc = routing_loc.id
605 picking_id = self.pool.get('stock.picking').create(cr, uid, {
606 'origin': (production.origin or '').split(':')[0] +':'+production.name,
610 'address_id': address_id,
611 'auto_picking': self._get_auto_picking(cr, uid, production),
615 source = production.product_id.product_tmpl_id.property_stock_production.id
617 'name':'PROD:'+production.name,
618 'date_planned': production.date_planned,
619 'product_id': production.product_id.id,
620 'product_qty': production.product_qty,
621 'product_uom': production.product_uom.id,
622 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
623 'product_uos': production.product_uos and production.product_uos.id or False,
624 'location_id': source,
625 'location_dest_id': production.location_dest_id.id,
626 'move_dest_id': production.move_prod_id.id,
629 res_final_id = self.pool.get('stock.move').create(cr, uid, data)
631 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 'WTF', [res_final_id])]})
633 for line in production.product_lines:
635 newdate = production.date_planned
636 if line.product_id.type in ('product', 'consu'):
637 res_dest_id = self.pool.get('stock.move').create(cr, uid, {
638 'name':'PROD:'+production.name,
639 'date_planned': production.date_planned,
640 'product_id': line.product_id.id,
641 'product_qty': line.product_qty,
642 'product_uom': line.product_uom.id,
643 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
644 'product_uos': line.product_uos and line.product_uos.id or False,
645 'location_id': routing_loc or production.location_src_id.id,
646 'location_dest_id': source,
647 'move_dest_id': res_final_id,
650 moves.append(res_dest_id)
651 move_id = self.pool.get('stock.move').create(cr, uid, {
652 'name':'PROD:'+production.name,
653 'picking_id':picking_id,
654 'product_id': line.product_id.id,
655 'product_qty': line.product_qty,
656 'product_uom': line.product_uom.id,
657 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
658 'product_uos': line.product_uos and line.product_uos.id or False,
659 'date_planned': newdate,
660 'move_dest_id': res_dest_id,
661 'location_id': production.location_src_id.id,
662 'location_dest_id': routing_loc or production.location_src_id.id,
665 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
666 'name': (production.origin or '').split(':')[0] + ':' + production.name,
667 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
668 'date_planned': newdate,
669 'product_id': line.product_id.id,
670 'product_qty': line.product_qty,
671 'product_uom': line.product_uom.id,
672 'product_uos_qty': line.product_uos and line.product_qty or False,
673 'product_uos': line.product_uos and line.product_uos.id or False,
674 'location_id': production.location_src_id.id,
675 'procure_method': line.product_id.procure_method,
678 wf_service = netsvc.LocalService("workflow")
679 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
681 wf_service = netsvc.LocalService("workflow")
682 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
683 self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
686 def force_production(self, cr, uid, ids, *args):
687 pick_obj = self.pool.get('stock.picking')
688 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
694 class stock_move(osv.osv):
696 _inherit = 'stock.move'
698 'production_id': fields.many2one('mrp.production', 'Production', select=True),
702 class mrp_production_workcenter_line(osv.osv):
703 _name = 'mrp.production.workcenter.line'
704 _description = 'Production workcenters used'
706 'name': fields.char('Name', size=64, required=True),
707 'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
708 'cycle': fields.float('Nbr of cycle'),
709 'hour': fields.float('Nbr of hour'),
710 'sequence': fields.integer('Sequence', required=True),
711 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
714 'sequence': lambda *a: 1,
715 'hour': lambda *a: 0,
716 'cycle': lambda *a: 0,
718 mrp_production_workcenter_line()
720 class mrp_production_product_line(osv.osv):
721 _name = 'mrp.production.product.line'
722 _description = 'Production scheduled products'
724 'name': fields.char('Name', size=64, required=True),
725 'product_id': fields.many2one('product.product', 'Product', required=True),
726 'product_qty': fields.float('Product Qty', required=True),
727 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
728 'product_uos_qty': fields.float('Product UOS Qty'),
729 'product_uos': fields.many2one('product.uom', 'Product UOS'),
730 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
732 mrp_production_product_line()
734 # ------------------------------------------------------------------
736 # ------------------------------------------------------------------
738 # Produce, Buy or Find products and place a move
739 # then wizard for picking lists & move
741 class mrp_procurement(osv.osv):
742 _name = "mrp.procurement"
743 _description = "Procurement"
745 'name': fields.char('Name', size=64, required=True),
746 'origin': fields.char('Origin', size=64,
747 help="Reference of the document that created this procurement.\n"
748 "This is automatically completed by Open ERP."),
749 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
750 'date_planned': fields.datetime('Scheduled date', required=True),
751 'date_close': fields.datetime('Date Closed'),
752 'product_id': fields.many2one('product.product', 'Product', required=True),
753 'product_qty': fields.float('Quantity', required=True),
754 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
755 'product_uos_qty': fields.float('UoS Quantity'),
756 'product_uos': fields.many2one('product.uom', 'Product UoS'),
757 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
759 'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
761 'close_move': fields.boolean('Close Move at end', required=True),
762 'location_id': fields.many2one('stock.location', 'Location', required=True),
763 'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
764 readonly=True, required=True, help="If you encode manually a procurement, you probably want to use" \
765 " a make to order method."),
767 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
768 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
770 'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
772 'message': fields.char('Latest error', size=64),
773 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('exception','Exception'),('running','Running'),('cancel','Cancel'),('done','Done'),('waiting','Waiting')], 'Status')
776 'state': lambda *a: 'draft',
777 'priority': lambda *a: '1',
778 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
779 'close_move': lambda *a: 0,
780 'procure_method': lambda *a: 'make_to_order',
782 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
784 w=self.pool.get('product.product').browse(cr,uid,product_id, context)
786 'product_uom':w.uom_id.id,
787 'product_uos':w.uos_id and w.uos_id.id or w.uom_id.id
792 def unlink(self, cr, uid, ids):
793 procurements = self.read(cr, uid, ids, ['state'])
795 for s in procurements:
796 if s['state'] in ['draft','cancel']:
797 unlink_ids.append(s['id'])
799 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Procurement Order(s) which are in %s State!' % s['state']))
800 osv.osv.unlink(self, cr, uid, unlink_ids)
803 def check_product(self, cr, uid, ids):
804 for procurement in self.browse(cr, uid, ids):
805 if procurement.product_id.type in ('product', 'consu'):
809 def check_move_cancel(self, cr, uid, ids, context={}):
811 for procurement in self.browse(cr, uid, ids, context):
812 if procurement.move_id:
813 if not procurement.move_id.state=='cancel':
817 def check_move_done(self, cr, uid, ids, context={}):
819 for proc in self.browse(cr, uid, ids, context):
821 if not proc.move_id.state=='done':
826 # This method may be overrided by objects that override mrp.procurment
827 # for computing their own purpose
829 def _quantity_compute_get(self, cr, uid, proc, context={}):
830 if proc.product_id.type=='product':
831 return proc.move_id.product_uos_qty
834 def _uom_compute_get(self, cr, uid, proc, context={}):
835 if proc.product_id.type=='product':
836 if proc.move_id.product_uos:
837 return proc.move_id.product_uos.id
841 # Return the quantity of product shipped/produced/served, wich may be
842 # different from the planned quantity
844 def quantity_get(self, cr, uid, id, context={}):
845 proc = self.browse(cr, uid, id, context)
846 result = self._quantity_compute_get(cr, uid, proc, context)
848 result = proc.product_qty
851 def uom_get(self, cr, uid, id, context=None):
852 proc = self.browse(cr, uid, id, context)
853 result = self._uom_compute_get(cr, uid, proc, context)
855 result = proc.product_uom.id
858 def check_waiting(self, cr, uid, ids, context=[]):
859 for procurement in self.browse(cr, uid, ids, context=context):
860 if procurement.move_id and procurement.move_id.state=='auto':
864 def check_produce_service(self, cr, uid, procurement, context=[]):
867 def check_produce_product(self, cr, uid, procurement, context=[]):
868 properties = [x.id for x in procurement.property_ids]
869 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
871 cr.execute('update mrp_procurement set message=%s where id=%d', ('No BoM defined for this product !', procurement.id))
875 def check_make_to_stock(self, cr, uid, ids, context={}):
877 for procurement in self.browse(cr, uid, ids, context=context):
878 if procurement.product_id.type=='service':
879 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
881 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
884 def check_produce(self, cr, uid, ids, context={}):
886 user = self.pool.get('res.users').browse(cr, uid, uid)
887 for procurement in self.browse(cr, uid, ids):
888 if procurement.product_id.product_tmpl_id.supply_method=='buy':
889 if procurement.product_id.seller_ids:
890 partner = procurement.product_id.seller_ids[0].name
891 if user.company_id and user.company_id.partner_id:
892 if partner.id == user.company_id.partner_id.id:
895 if procurement.product_id.product_tmpl_id.type=='service':
896 res = res and self.check_produce_service(cr, uid, procurement, context)
898 res = res and self.check_produce_product(cr, uid, procurement, context)
903 def check_buy(self, cr, uid, ids):
904 user = self.pool.get('res.users').browse(cr, uid, uid)
905 for procurement in self.browse(cr, uid, ids):
906 if procurement.product_id.product_tmpl_id.supply_method=='produce':
908 if not procurement.product_id.seller_ids:
909 cr.execute('update mrp_procurement set message=%s where id=%d', ('No supplier defined for this product !', procurement.id))
911 partner = procurement.product_id.seller_ids[0].name
912 if user.company_id and user.company_id.partner_id:
913 if partner.id == user.company_id.partner_id.id:
915 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
917 cr.execute('update mrp_procurement set message=%s where id=%d', ('No address defined for the supplier', procurement.id))
921 def test_cancel(self, cr, uid, ids):
922 for record in self.browse(cr, uid, ids):
923 if record.move_id and record.move_id.state=='cancel':
927 def action_confirm(self, cr, uid, ids, context={}):
928 for procurement in self.browse(cr, uid, ids):
929 if procurement.product_id.type in ('product', 'consu'):
930 if not procurement.move_id:
931 source = procurement.location_id.id
932 if procurement.procure_method=='make_to_order':
933 source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
934 id = self.pool.get('stock.move').create(cr, uid, {
935 'name': 'PROC:'+procurement.name,
936 'location_id': source,
937 'location_dest_id': procurement.location_id.id,
938 'product_id': procurement.product_id.id,
939 'product_qty':procurement.product_qty,
940 'product_uom': procurement.product_uom.id,
941 'date_planned': procurement.date_planned,
944 self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
947 if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('waiting',):
948 id = self.pool.get('stock.move').write(cr, uid, [procurement.move_id.id], {'state':'confirmed'})
949 self.write(cr, uid, ids, {'state':'confirmed','message':''})
952 def action_move_assigned(self, cr, uid, ids):
953 self.write(cr, uid, ids, {'state':'running','message':'from stock: products assigned.'})
956 def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
959 def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
961 if procurement.move_id:
962 id = procurement.move_id.id
963 if not (procurement.move_id.state in ('done','assigned','cancel')):
964 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
965 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%d', (procurement.product_id.id,))
966 if not cr.fetchone()[0]:
967 cr.execute('update mrp_procurement set message=%s where id=%d', ('from stock and no minimum orderpoint rule defined', procurement.id))
970 def action_produce_assign_service(self, cr, uid, ids, context={}):
971 for procurement in self.browse(cr, uid, ids):
972 self.write(cr, uid, [procurement.id], {'state':'running'})
975 def action_produce_assign_product(self, cr, uid, ids, context={}):
977 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
978 for procurement in self.browse(cr, uid, ids):
979 res_id = procurement.move_id.id
980 loc_id = procurement.location_id.id
981 newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - DateTime.RelativeDateTime(days=procurement.product_id.product_tmpl_id.produce_delay or 0.0)
982 newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
983 produce_id = self.pool.get('mrp.production').create(cr, uid, {
984 'origin': procurement.origin,
985 'product_id': procurement.product_id.id,
986 'product_qty': procurement.product_qty,
987 'product_uom': procurement.product_uom.id,
988 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
989 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
990 'location_src_id': procurement.location_id.id,
991 'location_dest_id': procurement.location_id.id,
992 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
993 'date_planned': newdate,
994 'move_prod_id': res_id,
996 self.write(cr, uid, [procurement.id], {'state':'running'})
997 bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
998 [produce_id], properties=[x.id for x in procurement.property_ids])
999 wf_service = netsvc.LocalService("workflow")
1000 wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1003 def action_po_assign(self, cr, uid, ids, context={}):
1005 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1006 for procurement in self.browse(cr, uid, ids):
1007 res_id = procurement.move_id.id
1008 partner = procurement.product_id.seller_ids[0].name
1009 partner_id = partner.id
1010 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1011 pricelist_id = partner.property_product_pricelist_purchase.id
1013 uom_id = procurement.product_id.uom_po_id.id
1015 qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1016 if procurement.product_id.seller_ids[0].qty:
1017 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1019 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1021 newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - DateTime.RelativeDateTime(days=procurement.product_id.product_tmpl_id.seller_delay or 0.0)
1022 newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1023 context.update({'lang':partner.lang})
1024 product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1027 'name': product.name,
1029 'product_id': procurement.product_id.id,
1030 'product_uom': uom_id,
1031 'price_unit': price,
1032 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1033 'move_dest_id': res_id,
1034 'notes':product.description_purchase,
1037 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1038 self.pool.get('account.fiscal.position').map_tax(cr, uid, partner, taxes_ids)
1040 'taxes_id':[(6,0,taxes_ids)]
1042 purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1043 'origin': procurement.origin,
1044 'partner_id': partner_id,
1045 'partner_address_id': address_id,
1046 'location_id': procurement.location_id.id,
1047 'pricelist_id': pricelist_id,
1048 'order_line': [(0,0,line)]
1050 self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1053 def action_cancel(self, cr, uid, ids):
1055 for proc in self.browse(cr, uid, ids):
1057 todo.append(proc.move_id.id)
1059 self.pool.get('stock.move').action_cancel(cr, uid, [proc.move_id.id])
1060 self.write(cr, uid, ids, {'state':'cancel'})
1062 wf_service = netsvc.LocalService("workflow")
1064 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1068 def action_check_finnished(self, cr, uid, ids):
1071 def action_check(self, cr, uid, ids):
1073 for procurement in self.browse(cr, uid, ids):
1074 if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1075 self.action_done(cr, uid, [procurement.id])
1079 def action_done(self, cr, uid, ids):
1080 for procurement in self.browse(cr, uid, ids):
1081 if procurement.move_id:
1082 if procurement.close_move and (procurement.move_id.state <> 'done'):
1083 self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1084 res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1086 wf_service = netsvc.LocalService("workflow")
1088 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1090 def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1092 use_new_cursor: False or the dbname
1096 self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1097 self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1098 use_new_cursor=use_new_cursor, context=context)
1102 class stock_warehouse_orderpoint(osv.osv):
1103 _name = "stock.warehouse.orderpoint"
1104 _description = "Orderpoint minimum rule"
1106 'name': fields.char('Name', size=32, required=True),
1107 'active': fields.boolean('Active'),
1108 'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1109 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1110 'location_id': fields.many2one('stock.location', 'Location', required=True),
1111 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1112 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1113 'product_min_qty': fields.float('Min Quantity', required=True,
1114 help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1115 "a procurement to bring the virtual stock to the Max Quantity."),
1116 'product_max_qty': fields.float('Max Quantity', required=True,
1117 help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1118 "a procurement to bring the virtual stock to the Max Quantity."),
1119 'qty_multiple': fields.integer('Qty Multiple', required=True,
1120 help="The procurement quantity will by rounded up to this multiple."),
1121 'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order')
1124 'active': lambda *a: 1,
1125 'logic': lambda *a: 'max',
1126 'qty_multiple': lambda *a: 1,
1127 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1128 'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1130 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1132 w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1133 v = {'location_id':w.lot_stock_id.id}
1136 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1138 prod=self.pool.get('product.product').browse(cr,uid,product_id)
1139 v = {'product_uom':prod.uom_id.id}
1142 stock_warehouse_orderpoint()
1145 class StockMove(osv.osv):
1146 _inherit = 'stock.move'
1148 'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1150 def copy(self, cr, uid, id, default=None, context=None):
1151 default = default or {}
1152 default['procurements'] = []
1153 return super(StockMove, self).copy(cr, uid, id, default, context)
1155 def _action_explode(self, cr, uid, move, context={}):
1156 if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1157 bis = self.pool.get('mrp.bom').search(cr, uid, [
1158 ('product_id','=',move.product_id.id),
1159 ('bom_id','=',False),
1160 ('type','=','phantom')])
1162 factor = move.product_qty
1163 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1164 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1165 dest = move.product_id.product_tmpl_id.property_stock_production.id
1167 if move.state=='assigned':
1171 'picking_id': move.picking_id.id,
1172 'product_id': line['product_id'],
1173 'product_uom': line['product_uom'],
1174 'product_qty': line['product_qty'],
1175 'product_uos': line['product_uos'],
1176 'product_uos_qty': line['product_uos_qty'],
1177 'move_dest_id': move.id,
1179 'location_dest_id': dest,
1180 'move_history_ids': [(6,0,[move.id])],
1181 'move_history_ids2': [(6,0,[])],
1184 mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1185 prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1186 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1187 'name': (move.picking_id.origin or ''),
1188 'origin': (move.picking_id.origin or ''),
1189 'date_planned': move.date_planned,
1190 'product_id': line['product_id'],
1191 'product_qty': line['product_qty'],
1192 'product_uom': line['product_uom'],
1193 'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1194 'product_uos': line['product_uos'],
1195 'location_id': move.location_id.id,
1196 'procure_method': prodobj.procure_method,
1199 wf_service = netsvc.LocalService("workflow")
1200 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1201 self.pool.get('stock.move').write(cr, uid, [move.id], {
1202 'location_id': move.location_dest_id.id,
1203 'auto_validate': True,
1204 'picking_id': False,
1205 'location_id': dest,
1208 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1209 wf_service = netsvc.LocalService("workflow")
1210 wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1215 class StockPicking(osv.osv):
1216 _inherit = 'stock.picking'
1218 def test_finnished(self, cursor, user, ids):
1219 wf_service = netsvc.LocalService("workflow")
1220 res = super(StockPicking, self).test_finnished(cursor, user, ids)
1221 for picking in self.browse(cursor, user, ids):
1222 for move in picking.move_lines:
1223 if move.state == 'done' and move.procurements:
1224 for procurement in move.procurements:
1225 wf_service.trg_validate(user, 'mrp.procurement',
1226 procurement.id, 'button_check', cursor)
1230 # Explode picking by replacing phantom BoMs
1232 def action_explode(self, cr, uid, picks, *args):
1234 self.pool.get('stock.move')._action_explode(cr, uid, move)
1239 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: