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 'hour_nbr': fields.float('Number of Hours', required=True),
138 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True),
139 'note': fields.text('Description')
142 'cycle_nbr': lambda *a: 1.0,
143 'hour_nbr': lambda *a: 0.0,
145 mrp_routing_workcenter()
147 class mrp_bom(osv.osv):
149 _description = 'Bill of Material'
150 def _child_compute(self, cr, uid, ids, name, arg, context={}):
152 for bom in self.browse(cr, uid, ids, context=context):
153 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
154 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
155 if bom.type=='phantom' or ok:
156 sids = self.pool.get('mrp.bom').search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
158 bom2 = self.pool.get('mrp.bom').browse(cr, uid, sids[0], context=context)
159 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
161 def _compute_type(self, cr, uid, ids, field_name, arg, context):
162 res = dict(map(lambda x: (x,''), ids))
163 for line in self.browse(cr, uid, ids):
164 if line.type=='phantom' and not line.bom_id:
167 if line.bom_lines or line.type=='phantom':
169 if line.product_id.supply_method=='produce':
170 if line.product_id.procure_method=='make_to_stock':
171 res[line.id] = 'stock'
173 res[line.id] = 'order'
176 'name': fields.char('Name', size=64, required=True),
177 'code': fields.char('Code', size=16),
178 'active': fields.boolean('Active'),
179 '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 having two production orders."),
181 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
183 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
184 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
185 'sequence': fields.integer('Sequence'),
186 'position': fields.char('Internal Ref.', size=64, help="Reference to a position in an external plan."),
187 'product_id': fields.many2one('product.product', 'Product', required=True),
188 'product_uos_qty': fields.float('Product UOS Qty'),
189 'product_uos': fields.many2one('product.uom', 'Product UOS'),
190 'product_qty': fields.float('Product Qty', required=True),
191 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
192 'product_rounding': fields.float('Product Rounding'),
193 'product_efficiency': fields.float('Product Efficiency', required=True),
194 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
195 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
196 '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."),
197 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
198 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
199 'revision_type': fields.selection([('numeric','numeric indices'),('alpha','alphabetical indices')], 'indice type'),
200 'child_ids': fields.function(_child_compute,relation='mrp.bom', method=True, string="BoM Hyerarchy", type='many2many'),
201 'child_complete_ids': fields.function(_child_compute,relation='mrp.bom', method=True, string="BoM Hyerarchy", type='many2many')
204 'active': lambda *a: 1,
205 'product_efficiency': lambda *a: 1.0,
206 'product_qty': lambda *a: 1.0,
207 'product_rounding': lambda *a: 1.0,
208 'type': lambda *a: 'normal',
212 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0 !'),
215 def _check_recursion(self, cr, uid, ids):
218 cr.execute('select distinct bom_id from mrp_bom where id in ('+','.join(map(str,ids))+')')
219 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
225 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
229 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
231 prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
232 v = {'product_uom':prod.uom_id.id}
234 v['name'] = prod.name
238 def _bom_find(self, cr, uid, product_id, product_uom, properties = []):
240 # Why searching on BoM without parent ?
241 cr.execute('select id from mrp_bom where product_id=%d and bom_id is null order by sequence', (product_id,))
242 ids = map(lambda x: x[0], cr.fetchall())
245 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
247 for prop_id in bom.property_ids:
248 if prop_id.id in properties:
250 if (prop>max_prop) or ((max_prop==0) and not result):
254 def _bom_explode(self, cr, uid, bom, factor, properties, addthis=False, level=10):
255 factor = factor / (bom.product_efficiency or 1.0)
256 factor = rounding(factor, bom.product_rounding)
257 if factor<bom.product_rounding:
258 factor = bom.product_rounding
261 if bom.type=='phantom' and not bom.bom_lines:
262 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
264 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
265 result = result + res[0]
266 result2 = result2 + res[1]
270 if addthis and not bom.bom_lines:
273 'name': bom.product_id.name,
274 'product_id': bom.product_id.id,
275 'product_qty': bom.product_qty * factor,
276 'product_uom': bom.product_uom.id,
277 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
278 'product_uos': bom.product_uos and bom.product_uos.id or False,
281 for wc_use in bom.routing_id.workcenter_lines:
282 wc = wc_use.workcenter_id
283 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
284 cycle = (d + (m and 1.0 or 0.0)) * wc_use.cycle_nbr
286 'name': bom.routing_id.name,
287 'workcenter_id': wc.id,
290 'hour': wc_use.hour_nbr + (wc.time_start+wc.time_stop+cycle*wc.time_cycle) * (wc.time_efficiency or 1.0),
292 for bom2 in bom.bom_lines:
293 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
294 result = result + res[0]
295 result2 = result2 + res[1]
296 return result, result2
298 def set_indices(self, cr, uid, ids, context = {}):
299 if not ids or (ids and not ids[0]):
301 res = self.read(cr, uid, ids, ['revision_ids', 'revision_type'])
302 rev_ids = res[0]['revision_ids']
305 for rev_id in rev_ids:
306 if res[0]['revision_type'] == 'numeric':
307 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : idx})
309 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : "%c"%(idx+96,)})
315 class mrp_bom_revision(osv.osv):
316 _name = 'mrp.bom.revision'
317 _description = 'Bill of material revisions'
319 'name': fields.char('Modification name', size=64, required=True),
320 'description': fields.text('Description'),
321 'date': fields.date('Modification Date'),
322 'indice': fields.char('Revision', size=16),
323 'last_indice': fields.char('last indice', size=64),
324 'author_id': fields.many2one('res.users', 'Author'),
325 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
329 'author_id': lambda x,y,z,c: z,
330 'date': lambda *a: time.strftime('%Y-%m-%d'),
338 return round(f / r) * r
340 class mrp_production(osv.osv):
341 _name = 'mrp.production'
342 _description = 'Production'
343 _date_name = 'date_planned'
345 def _get_sale_order(self,cr,uid,ids,field_name=False):
346 move_obj=self.pool.get('stock.move')
347 def get_parent_move(move_id):
348 move = move_obj.browse(cr,uid,move_id)
349 if move.move_dest_id:
350 return get_parent_move(move.move_dest_id.id)
352 productions=self.read(cr,uid,ids,['id','move_prod_id'])
354 for production in productions:
355 res[production['id']]=False
356 if production.get('move_prod_id',False):
357 parent_move_line=get_parent_move(production['move_prod_id'][0])
359 move = move_obj.browse(cr,uid,parent_move_line)
360 if field_name=='name':
361 res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.name or False
362 if field_name=='client_order_ref':
363 res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.client_order_ref or False
366 def _sale_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
367 return self._get_sale_order(cr,uid,ids,field_name='name')
369 def _sale_ref_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
370 return self._get_sale_order(cr,uid,ids,field_name='client_order_ref')
373 'name': fields.char('Reference', size=64, required=True),
374 'origin': fields.char('Origin', size=64),
375 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
377 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
378 'product_qty': fields.float('Product Qty', required=True),
379 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
380 'product_uos_qty': fields.float('Product Qty'),
381 'product_uos': fields.many2one('product.uom', 'Product UOM'),
383 'location_src_id': fields.many2one('stock.location', 'Raw Products Location', required=True),
384 'location_dest_id': fields.many2one('stock.location', 'Finnished Products Location', required=True),
386 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
387 'date_start': fields.datetime('Start Date'),
388 'date_finnished': fields.datetime('End Date'),
390 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
392 'picking_id': fields.many2one('stock.picking', 'Packing list', readonly=True),
393 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
394 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
396 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
397 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
398 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Workcenters Utilisation'),
400 '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),
401 'sale_name': fields.function(_sale_name_calc, method=True, type='char', string='Sale Name'),
402 'sale_ref': fields.function(_sale_ref_calc, method=True, type='char', string='Sale Ref'),
405 'priority': lambda *a: '1',
406 'state': lambda *a: 'draft',
407 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
408 'product_qty': lambda *a: 1.0,
409 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
411 _order = 'date_planned asc, priority desc';
413 def location_id_change(self, cr, uid, ids, src, dest, context={}):
417 return {'value': {'location_dest_id': src}}
420 def product_id_change(self, cr, uid, ids, product):
423 res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
424 uom = res['uom_id'] and res['uom_id'][0]
425 result = {'product_uom':uom}
426 return {'value':result}
428 def action_picking_except(self, cr, uid, ids):
429 self.write(cr, uid, ids, {'state':'picking_except'})
432 def action_compute(self, cr, uid, ids, properties=[]):
434 for production in self.browse(cr, uid, ids):
435 cr.execute('delete from mrp_production_product_line where production_id=%d', (production.id,))
436 cr.execute('delete from mrp_production_workcenter_line where production_id=%d', (production.id,))
437 bom_point = production.bom_id
438 bom_id = production.bom_id.id
440 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
442 self.write(cr, uid, [production.id], {'bom_id': bom_id})
443 bom_point = self.pool.get('mrp.bom').browse(cr, uid, [bom_id])[0]
446 raise osv.except_osv('Error', "Couldn't find bill of material for product")
448 #if bom_point.routing_id and bom_point.routing_id.location_id:
449 # self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
451 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
452 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
456 line['production_id'] = production.id
457 self.pool.get('mrp.production.product.line').create(cr, uid, line)
458 for line in results2:
459 line['production_id'] = production.id
460 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
463 def action_cancel(self, cr, uid, ids):
464 for production in self.browse(cr, uid, ids):
465 if production.move_created_ids:
466 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
467 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
468 self.write(cr, uid, ids, {'state':'cancel','move_lines':[(6,0,[])]})
471 #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
472 # between the end of the picking list and the call to this function
473 def action_ready(self, cr, uid, ids):
474 self.write(cr, uid, ids, {'state':'ready'})
475 for production in self.browse(cr, uid, ids):
476 if production.move_prod_id:
477 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
478 {'location_id':production.location_dest_id.id})
481 #TODO Review materials in function in_prod and prod_end.
482 def action_production_end(self, cr, uid, ids):
484 for production in self.browse(cr, uid, ids):
485 for res in production.move_lines:
486 for move in production.move_created_ids:
487 #XXX must use the orm
488 cr.execute('INSERT INTO stock_move_history_ids \
489 (parent_id, child_id) VALUES (%d,%d)',
491 move_ids.append(res.id)
492 if production.move_created_ids:
493 #TODO There we should handle the residus move creation
494 vals= {'state':'confirmed'}
495 new_moves = [x.id for x in production.move_created_ids]
496 self.pool.get('stock.move').write(cr, uid, new_moves, vals)
498 #XXX Why is it there ? Aren't we suppose to already have a created_move ?
499 source = production.product_id.product_tmpl_id.property_stock_production.id
501 'name':'PROD:'+production.name,
502 'date_planned': production.date_planned,
503 'product_id': production.product_id.id,
504 'product_qty': production.product_qty,
505 'product_uom': production.product_uom.id,
506 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
507 'product_uos': production.product_uos and production.product_uos.id or False,
508 'location_id': source,
509 'location_dest_id': production.location_dest_id.id,
510 'move_dest_id': production.move_prod_id.id,
513 new_moves = [self.pool.get('stock.move').create(cr, uid, vals)]
514 self.write(cr, uid, [production.id],
515 {'move_created_ids': [(6, 'WTF', new_moves)]})
516 if not production.date_finnished:
517 self.write(cr, uid, [production.id],
518 {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
519 self.pool.get('stock.move').check_assign(cr, uid, new_moves)
520 self.pool.get('stock.move').action_done(cr, uid, new_moves)
521 self._costs_generate(cr, uid, production)
522 self.pool.get('stock.move').action_done(cr, uid, move_ids)
523 self.write(cr, uid, ids, {'state': 'done'})
526 def _costs_generate(self, cr, uid, production):
528 for wc_line in production.workcenter_lines:
529 wc = wc_line.workcenter_id
530 if wc.costs_journal_id and wc.costs_general_account_id:
531 value = wc_line.hour * wc.costs_hour
532 account = wc.costs_hour_account_id.id
533 if value and account:
535 self.pool.get('account.analytic.line').create(cr, uid, {
536 'name': wc_line.name+' (H)',
538 'account_id': account,
539 'general_account_id': wc.costs_general_account_id.id,
540 'journal_id': wc.costs_journal_id.id,
543 if wc.costs_journal_id and wc.costs_general_account_id:
544 value = wc_line.cycle * wc.costs_cycle
545 account = wc.costs_cycle_account_id.id
546 if value and account:
548 self.pool.get('account.analytic.line').create(cr, uid, {
549 'name': wc_line.name+' (C)',
551 'account_id': account,
552 'general_account_id': wc.costs_general_account_id.id,
553 'journal_id': wc.costs_journal_id.id,
558 def action_in_production(self, cr, uid, ids):
560 for production in self.browse(cr, uid, ids):
561 for res in production.move_lines:
562 move_ids.append(res.id)
563 if not production.date_start:
564 self.write(cr, uid, [production.id],
565 {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
566 self.pool.get('stock.move').action_done(cr, uid, move_ids)
567 self.write(cr, uid, ids, {'state': 'in_production'})
570 def test_if_product(self, cr, uid, ids):
572 for production in self.browse(cr, uid, ids):
573 if not production.product_lines:
574 if not self.action_compute(cr, uid, [production.id]):
578 def _get_auto_picking(self, cr, uid, production):
581 def action_confirm(self, cr, uid, ids):
583 for production in self.browse(cr, uid, ids):
584 if not production.product_lines:
585 self.action_compute(cr, uid, [production.id])
586 production = self.browse(cr, uid, [production.id])[0]
588 pick_type = 'internal'
590 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
591 routing_loc = production.bom_id.routing_id.location_id
592 if routing_loc.usage<>'internal':
594 address_id = routing_loc.address_id and routing_loc.address_id.id or False
595 routing_loc = routing_loc.id
596 picking_id = self.pool.get('stock.picking').create(cr, uid, {
597 'origin': (production.origin or '').split(':')[0] +':'+production.name,
601 'address_id': address_id,
602 'auto_picking': self._get_auto_picking(cr, uid, production),
606 source = production.product_id.product_tmpl_id.property_stock_production.id
608 'name':'PROD:'+production.name,
609 'date_planned': production.date_planned,
610 'product_id': production.product_id.id,
611 'product_qty': production.product_qty,
612 'product_uom': production.product_uom.id,
613 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
614 'product_uos': production.product_uos and production.product_uos.id or False,
615 'location_id': source,
616 'location_dest_id': production.location_dest_id.id,
617 'move_dest_id': production.move_prod_id.id,
620 res_final_id = self.pool.get('stock.move').create(cr, uid, data)
622 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 'WTF', [res_final_id])]})
624 for line in production.product_lines:
626 newdate = production.date_planned
627 if line.product_id.type in ('product', 'consu'):
628 res_dest_id = self.pool.get('stock.move').create(cr, uid, {
629 'name':'PROD:'+production.name,
630 'date_planned': production.date_planned,
631 'product_id': line.product_id.id,
632 'product_qty': line.product_qty,
633 'product_uom': line.product_uom.id,
634 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
635 'product_uos': line.product_uos and line.product_uos.id or False,
636 'location_id': routing_loc or production.location_src_id.id,
637 'location_dest_id': source,
638 'move_dest_id': res_final_id,
641 moves.append(res_dest_id)
642 move_id = self.pool.get('stock.move').create(cr, uid, {
643 'name':'PROD:'+production.name,
644 'picking_id':picking_id,
645 'product_id': line.product_id.id,
646 'product_qty': line.product_qty,
647 'product_uom': line.product_uom.id,
648 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
649 'product_uos': line.product_uos and line.product_uos.id or False,
650 'date_planned': newdate,
651 'move_dest_id': res_dest_id,
652 'location_id': production.location_src_id.id,
653 'location_dest_id': routing_loc or production.location_src_id.id,
656 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
657 'name': (production.origin or '').split(':')[0] + ':' + production.name,
658 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
659 'date_planned': newdate,
660 'product_id': line.product_id.id,
661 'product_qty': line.product_qty,
662 'product_uom': line.product_uom.id,
663 'product_uos_qty': line.product_uos and line.product_qty or False,
664 'product_uos': line.product_uos and line.product_uos.id or False,
665 'location_id': production.location_src_id.id,
666 'procure_method': line.product_id.procure_method,
669 wf_service = netsvc.LocalService("workflow")
670 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
672 wf_service = netsvc.LocalService("workflow")
673 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
674 self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
677 def force_production(self, cr, uid, ids, *args):
678 pick_obj = self.pool.get('stock.picking')
679 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
685 class stock_move(osv.osv):
687 _inherit = 'stock.move'
689 'production_id': fields.many2one('mrp.production', 'Production', select=True),
693 class mrp_production_workcenter_line(osv.osv):
694 _name = 'mrp.production.workcenter.line'
695 _description = 'Production workcenters used'
697 'name': fields.char('Name', size=64, required=True),
698 'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
699 'cycle': fields.float('Nbr of cycle'),
700 'hour': fields.float('Nbr of hour'),
701 'sequence': fields.integer('Sequence', required=True),
702 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
705 'sequence': lambda *a: 1,
706 'hour': lambda *a: 0,
707 'cycle': lambda *a: 0,
709 mrp_production_workcenter_line()
711 class mrp_production_product_line(osv.osv):
712 _name = 'mrp.production.product.line'
713 _description = 'Production scheduled products'
715 'name': fields.char('Name', size=64, required=True),
716 'product_id': fields.many2one('product.product', 'Product', required=True),
717 'product_qty': fields.float('Product Qty', required=True),
718 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
719 'product_uos_qty': fields.float('Product UOS Qty'),
720 'product_uos': fields.many2one('product.uom', 'Product UOS'),
721 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
723 mrp_production_product_line()
725 # ------------------------------------------------------------------
727 # ------------------------------------------------------------------
729 # Produce, Buy or Find products and place a move
730 # then wizard for picking lists & move
732 class mrp_procurement(osv.osv):
733 _name = "mrp.procurement"
734 _description = "Procurement"
736 'name': fields.char('Name', size=64, required=True),
737 'origin': fields.char('Origin', size=64),
738 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
739 'date_planned': fields.datetime('Scheduled date', required=True),
740 'date_close': fields.datetime('Date Closed'),
741 'product_id': fields.many2one('product.product', 'Product', required=True),
742 'product_qty': fields.float('Quantity', required=True),
743 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
744 'product_uos_qty': fields.float('UoS Quantity'),
745 'product_uos': fields.many2one('product.uom', 'Product UoS'),
746 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
748 'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
750 'close_move': fields.boolean('Close Move at end', required=True),
751 'location_id': fields.many2one('stock.location', 'Location', required=True),
752 '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),
754 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
755 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
757 'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
759 'message': fields.char('Latest error', size=64),
760 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('exception','Exception'),('running','Running'),('cancel','Cancel'),('done','Done'),('waiting','Waiting')], 'Status')
763 'state': lambda *a: 'draft',
764 'priority': lambda *a: '1',
765 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
766 'close_move': lambda *a: 0,
767 'procure_method': lambda *a: 'make_to_order',
769 def check_product(self, cr, uid, ids):
770 for procurement in self.browse(cr, uid, ids):
771 if procurement.product_id.type in ('product', 'consu'):
775 def check_move_cancel(self, cr, uid, ids, context={}):
777 for procurement in self.browse(cr, uid, ids, context):
778 if procurement.move_id:
779 if not procurement.move_id.state=='cancel':
783 def check_move_done(self, cr, uid, ids, context={}):
785 for proc in self.browse(cr, uid, ids, context):
787 if not proc.move_id.state=='done':
792 # This method may be overrided by objects that override mrp.procurment
793 # for computing their own purpose
795 def _quantity_compute_get(self, cr, uid, proc, context={}):
796 if proc.product_id.type=='product':
797 return proc.move_id.product_uos_qty
800 def _uom_compute_get(self, cr, uid, proc, context={}):
801 if proc.product_id.type=='product':
802 if proc.move_id.product_uos:
803 return proc.move_id.product_uos.id
807 # Return the quantity of product shipped/produced/served, wich may be
808 # different from the planned quantity
810 def quantity_get(self, cr, uid, id, context={}):
811 proc = self.browse(cr, uid, id, context)
812 result = self._quantity_compute_get(cr, uid, proc, context)
814 result = proc.product_qty
817 def uom_get(self, cr, uid, id, context=None):
818 proc = self.browse(cr, uid, id, context)
819 result = self._uom_compute_get(cr, uid, proc, context)
821 result = proc.product_uom.id
824 def check_waiting(self, cr, uid, ids, context=[]):
825 for procurement in self.browse(cr, uid, ids, context=context):
826 if procurement.move_id and procurement.move_id.state=='auto':
830 def check_produce_service(self, cr, uid, procurement, context=[]):
833 def check_produce_product(self, cr, uid, procurement, context=[]):
834 properties = [x.id for x in procurement.property_ids]
835 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
837 cr.execute('update mrp_procurement set message=%s where id=%d', ('No BoM defined for this product !', procurement.id))
841 def check_make_to_stock(self, cr, uid, ids, context={}):
843 for procurement in self.browse(cr, uid, ids, context=context):
844 if procurement.product_id.type=='service':
845 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
847 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
850 def check_produce(self, cr, uid, ids, context={}):
852 user = self.pool.get('res.users').browse(cr, uid, uid)
853 for procurement in self.browse(cr, uid, ids):
854 if procurement.product_id.product_tmpl_id.supply_method=='buy':
855 if procurement.product_id.seller_ids:
856 partner = procurement.product_id.seller_ids[0].name
857 if user.company_id and user.company_id.partner_id:
858 if partner.id == user.company_id.partner_id.id:
861 if procurement.product_id.product_tmpl_id.type=='service':
862 res = res and self.check_produce_service(cr, uid, procurement, context)
864 res = res and self.check_produce_product(cr, uid, procurement, context)
869 def check_buy(self, cr, uid, ids):
870 user = self.pool.get('res.users').browse(cr, uid, uid)
871 for procurement in self.browse(cr, uid, ids):
872 if procurement.product_id.product_tmpl_id.supply_method=='produce':
874 if not procurement.product_id.seller_ids:
875 cr.execute('update mrp_procurement set message=%s where id=%d', ('No supplier defined for this product !', procurement.id))
877 partner = procurement.product_id.seller_ids[0].name
878 if user.company_id and user.company_id.partner_id:
879 if partner.id == user.company_id.partner_id.id:
881 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
883 cr.execute('update mrp_procurement set message=%s where id=%d', ('No address defined for the supplier', procurement.id))
887 def test_cancel(self, cr, uid, ids):
888 for record in self.browse(cr, uid, ids):
889 if record.move_id and record.move_id.state=='cancel':
893 def action_confirm(self, cr, uid, ids, context={}):
894 for procurement in self.browse(cr, uid, ids):
895 if procurement.product_id.type in ('product', 'consu'):
896 if not procurement.move_id:
897 source = procurement.location_id.id
898 if procurement.procure_method=='make_to_order':
899 source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
900 id = self.pool.get('stock.move').create(cr, uid, {
901 'name': 'PROC:'+procurement.name,
902 'location_id': source,
903 'location_dest_id': procurement.location_id.id,
904 'product_id': procurement.product_id.id,
905 'product_qty':procurement.product_qty,
906 'product_uom': procurement.product_uom.id,
907 'date_planned': procurement.date_planned,
910 self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
913 if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('waiting',):
914 id = self.pool.get('stock.move').write(cr, uid, [procurement.move_id.id], {'state':'confirmed'})
915 self.write(cr, uid, ids, {'state':'confirmed','message':''})
918 def action_move_assigned(self, cr, uid, ids):
919 self.write(cr, uid, ids, {'state':'running','message':'from stock: products assigned.'})
922 def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
925 def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
927 if procurement.move_id:
928 id = procurement.move_id.id
929 if not (procurement.move_id.state in ('done','assigned','cancel')):
930 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
931 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%d', (procurement.product_id.id,))
932 if not cr.fetchone()[0]:
933 cr.execute('update mrp_procurement set message=%s where id=%d', ('from stock and no minimum orderpoint rule defined', procurement.id))
936 def action_produce_assign_service(self, cr, uid, ids, context={}):
937 for procurement in self.browse(cr, uid, ids):
938 self.write(cr, uid, [procurement.id], {'state':'running'})
941 def action_produce_assign_product(self, cr, uid, ids, context={}):
943 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
944 for procurement in self.browse(cr, uid, ids):
945 res_id = procurement.move_id.id
946 loc_id = procurement.location_id.id
947 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)
948 newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
949 produce_id = self.pool.get('mrp.production').create(cr, uid, {
950 'origin': procurement.origin,
951 'product_id': procurement.product_id.id,
952 'product_qty': procurement.product_qty,
953 'product_uom': procurement.product_uom.id,
954 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
955 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
956 'location_src_id': procurement.location_id.id,
957 'location_dest_id': procurement.location_id.id,
958 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
959 'date_planned': newdate,
960 'move_prod_id': res_id,
962 self.write(cr, uid, [procurement.id], {'state':'running'})
963 bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
964 [produce_id], properties=[x.id for x in procurement.property_ids])
965 wf_service = netsvc.LocalService("workflow")
966 wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
969 def action_po_assign(self, cr, uid, ids, context={}):
971 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
972 for procurement in self.browse(cr, uid, ids):
973 res_id = procurement.move_id.id
974 partner = procurement.product_id.seller_ids[0].name
975 partner_id = partner.id
976 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
977 pricelist_id = partner.property_product_pricelist_purchase.id
979 uom_id = procurement.product_id.uom_po_id.id
981 qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
982 if procurement.product_id.seller_ids[0].qty:
983 qty=max(qty,procurement.product_id.seller_ids[0].qty)
985 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
987 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)
988 newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
989 context.update({'lang':partner.lang})
990 product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
993 'name': product.name,
995 'product_id': procurement.product_id.id,
996 'product_uom': uom_id,
998 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
999 'move_dest_id': res_id,
1000 'notes':product.description_purchase,
1003 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1004 self.pool.get('account.fiscal.position').map_tax(cr, uid, partner, taxes_ids)
1006 'taxes_id':[(6,0,taxes_ids)]
1008 purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1009 'origin': procurement.origin,
1010 'partner_id': partner_id,
1011 'partner_address_id': address_id,
1012 'location_id': procurement.location_id.id,
1013 'pricelist_id': pricelist_id,
1014 'order_line': [(0,0,line)]
1016 self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1019 def action_cancel(self, cr, uid, ids):
1021 for proc in self.browse(cr, uid, ids):
1023 todo.append(proc.move_id.id)
1025 self.pool.get('stock.move').action_cancel(cr, uid, [proc.move_id.id])
1026 self.write(cr, uid, ids, {'state':'cancel'})
1028 wf_service = netsvc.LocalService("workflow")
1030 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1034 def action_check_finnished(self, cr, uid, ids):
1037 def action_check(self, cr, uid, ids):
1039 for procurement in self.browse(cr, uid, ids):
1040 if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1041 self.action_done(cr, uid, [procurement.id])
1045 def action_done(self, cr, uid, ids):
1046 for procurement in self.browse(cr, uid, ids):
1047 if procurement.move_id:
1048 if procurement.close_move and (procurement.move_id.state <> 'done'):
1049 self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1050 res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1052 wf_service = netsvc.LocalService("workflow")
1054 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1056 def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1058 use_new_cursor: False or the dbname
1062 self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1063 self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1064 use_new_cursor=use_new_cursor, context=context)
1068 class stock_warehouse_orderpoint(osv.osv):
1069 _name = "stock.warehouse.orderpoint"
1070 _description = "Orderpoint minimum rule"
1072 'name': fields.char('Name', size=32, required=True),
1073 'active': fields.boolean('Active'),
1074 'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1075 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1076 'location_id': fields.many2one('stock.location', 'Location', required=True),
1077 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1078 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1079 'product_min_qty': fields.float('Min Quantity', required=True),
1080 'product_max_qty': fields.float('Max Quantity', required=True),
1081 'qty_multiple': fields.integer('Qty Multiple', required=True),
1082 'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order')
1085 'active': lambda *a: 1,
1086 'logic': lambda *a: 'max',
1087 'qty_multiple': lambda *a: 1,
1088 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1089 'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1091 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1093 w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1094 v = {'location_id':w.lot_stock_id.id}
1097 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1099 prod=self.pool.get('product.product').browse(cr,uid,product_id)
1100 v = {'product_uom':prod.uom_id.id}
1103 stock_warehouse_orderpoint()
1106 class StockMove(osv.osv):
1107 _inherit = 'stock.move'
1109 'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1111 def _action_explode(self, cr, uid, move, context={}):
1112 if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1113 bis = self.pool.get('mrp.bom').search(cr, uid, [
1114 ('product_id','=',move.product_id.id),
1115 ('bom_id','=',False),
1116 ('type','=','phantom')])
1118 factor = move.product_qty
1119 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1120 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1121 dest = move.product_id.product_tmpl_id.property_stock_production.id
1123 if move.state=='assigned':
1127 'picking_id': move.picking_id.id,
1128 'product_id': line['product_id'],
1129 'product_uom': line['product_uom'],
1130 'product_qty': line['product_qty'],
1131 'product_uos': line['product_uos'],
1132 'product_uos_qty': line['product_uos_qty'],
1133 'move_dest_id': move.id,
1135 'location_dest_id': dest,
1136 'move_history_ids': [(6,0,[move.id])],
1137 'move_history_ids2': [(6,0,[])],
1140 mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1141 prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1142 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1143 'name': (move.picking_id.origin or ''),
1144 'origin': (move.picking_id.origin or ''),
1145 'date_planned': move.date_planned,
1146 'product_id': line['product_id'],
1147 'product_qty': line['product_qty'],
1148 'product_uom': line['product_uom'],
1149 'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1150 'product_uos': line['product_uos'],
1151 'location_id': move.location_id.id,
1152 'procure_method': prodobj.procure_method,
1155 wf_service = netsvc.LocalService("workflow")
1156 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1157 self.pool.get('stock.move').write(cr, uid, [move.id], {
1158 'location_id': move.location_dest_id.id,
1159 'auto_validate': True,
1160 'picking_id': False,
1161 'location_id': dest,
1164 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1165 wf_service = netsvc.LocalService("workflow")
1166 wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1171 class StockPicking(osv.osv):
1172 _inherit = 'stock.picking'
1174 def test_finnished(self, cursor, user, ids):
1175 wf_service = netsvc.LocalService("workflow")
1176 res = super(StockPicking, self).test_finnished(cursor, user, ids)
1177 for picking in self.browse(cursor, user, ids):
1178 for move in picking.move_lines:
1179 if move.state == 'done' and move.procurements:
1180 for procurement in move.procurements:
1181 wf_service.trg_validate(user, 'mrp.procurement',
1182 procurement.id, 'button_check', cursor)
1186 # Explode picking by replacing phantom BoMs
1188 def action_explode(self, cr, uid, picks, *args):
1190 self.pool.get('stock.move')._action_explode(cr, uid, move)
1195 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: