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('Name', size=64, required=True),
53 'active': fields.boolean('Active'),
54 'type': fields.selection([('machine','Machine'),('hr','Human Resource'),('tool','Tool')], 'Type', required=True),
55 'code': fields.char('Code', size=8),
56 'timesheet_id': fields.many2one('hr.timesheet.group', 'Timesheet'),
57 'note': fields.text('Description'),
59 'capacity_per_cycle': fields.float('Capacity per Cycle'),
61 'time_cycle': fields.float('Time for 1 cycle (hour)'),
62 'time_start': fields.float('Time before prod.'),
63 'time_stop': fields.float('Time after prod.'),
64 'time_efficiency': fields.float('Time Efficiency'),
66 'costs_hour': fields.float('Cost per hour'),
67 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','=','expense')]),
68 'costs_cycle': fields.float('Cost per cycle'),
69 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','=','expense')]),
70 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
71 'costs_general_account_id': fields.many2one('account.account', 'General Account'),
74 'active': lambda *a: 1,
75 'type': lambda *a: 'machine',
76 'time_efficiency': lambda *a: 1.0,
77 'capacity_per_cycle': lambda *a: 1.0,
82 class mrp_property_group(osv.osv):
83 _name = 'mrp.property.group'
84 _description = 'Property Group'
86 'name': fields.char('Property Group', size=64, required=True),
87 'description': fields.text('Description'),
91 class mrp_property(osv.osv):
92 _name = 'mrp.property'
93 _description = 'Property'
95 'name': fields.char('Name', size=64, required=True),
96 'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True),
97 'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
98 'description': fields.text('Description'),
101 'composition': lambda *a: 'min',
105 class mrp_routing(osv.osv):
106 _name = 'mrp.routing'
107 _description = 'Routing'
109 'name': fields.char('Name', size=64, required=True),
110 'active': fields.boolean('Active'),
111 'code': fields.char('Code', size=8),
113 'note': fields.text('Description'),
114 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Workcenters'),
116 'location_id': fields.many2one('stock.location', 'Production Location'),
119 'active': lambda *a: 1,
123 class mrp_routing_workcenter(osv.osv):
124 _name = 'mrp.routing.workcenter'
125 _description = 'Routing workcenter usage'
127 'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
128 'name': fields.char('Name', size=64, required=True),
129 'sequence': fields.integer('Sequence'),
130 'cycle_nbr': fields.float('Number of cycle', required=True),
131 'hour_nbr': fields.float('Number of hours', required=True),
132 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True),
133 'note': fields.text('Description')
136 'cycle_nbr': lambda *a: 1.0,
137 'hour_nbr': lambda *a: 0.0,
139 mrp_routing_workcenter()
141 class mrp_bom(osv.osv):
143 _description = 'Bill of Material'
145 'name': fields.char('Name', size=64, required=True),
146 'code': fields.char('Code', size=16),
147 'active': fields.boolean('Active'),
148 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True, help="Use a phantom bill of material in lines that have a sub-bom and that have to be automatically computed in one line, without habing two production orders."),
149 'date_start': fields.date('Valid from'),
150 'date_stop': fields.date('Valid until'),
151 'sequence': fields.integer('Sequence'),
152 'position': fields.char('Internal Ref.', size=64),
153 'product_id': fields.many2one('product.product', 'Product', required=True),
154 'product_uos_qty': fields.float('Product UOS Qty'),
155 'product_uos': fields.many2one('product.uom', 'Product UOS'),
156 'product_qty': fields.float('Product Qty', required=True),
157 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
158 'product_rounding': fields.float('Product Rounding'),
159 'product_efficiency': fields.float('Product Efficiency', required=True),
160 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
161 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
162 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of workcenters) to produce the finnished product. The routing is mainly used to compute workcenter costs during operations and to plan futur loads on workcenters based on production plannification."),
163 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
164 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
165 'revision_type': fields.selection([('numeric','numeric indices'),('alpha','alphabetical indices')], 'indice type')
168 'active': lambda *a: 1,
169 'product_efficiency': lambda *a: 1.0,
170 'product_qty': lambda *a: 1.0,
171 'product_rounding': lambda *a: 1.0,
172 'type': lambda *a: 'normal',
176 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0 !'),
178 def _check_recursion(self, cr, uid, ids):
181 cr.execute('select distinct bom_id from mrp_bom where id in ('+','.join(map(str,ids))+')')
182 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
188 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
192 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
194 prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
195 v = {'product_uom':prod.uom_id.id}
197 v['name'] = prod.name
201 def _bom_find(self, cr, uid, product_id, product_uom, properties = []):
203 # Why searching on BoM without parent ?
204 cr.execute('select id from mrp_bom where product_id=%d and bom_id is null order by sequence', (product_id,))
205 ids = map(lambda x: x[0], cr.fetchall())
208 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
210 for prop_id in bom.property_ids:
211 if prop_id.id in properties:
213 if (prop>max_prop) or ((max_prop==0) and not result):
217 def _bom_explode(self, cr, uid, bom, factor, properties, addthis=False, level=10):
218 factor = factor / (bom.product_efficiency or 1.0)
219 factor = rounding(factor, bom.product_rounding)
220 if factor<bom.product_rounding:
221 factor = bom.product_rounding
224 if bom.type=='phantom' and not bom.bom_lines:
225 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
227 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
228 result = result + res[0]
229 result2 = result2 + res[1]
233 if addthis and not bom.bom_lines:
236 'name': bom.product_id.name,
237 'product_id': bom.product_id.id,
238 'product_qty': bom.product_qty * factor,
239 'product_uom': bom.product_uom.id,
240 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
241 'product_uos': bom.product_uos and bom.product_uos.id or False,
244 for wc_use in bom.routing_id.workcenter_lines:
245 wc = wc_use.workcenter_id
246 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
247 cycle = (d + (m and 1.0 or 0.0)) * wc_use.cycle_nbr
249 'name': bom.routing_id.name,
250 'workcenter_id': wc.id,
253 'hour': wc_use.hour_nbr + (wc.time_start+wc.time_stop+cycle*wc.time_cycle) * (wc.time_efficiency or 1.0),
255 for bom2 in bom.bom_lines:
256 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
257 result = result + res[0]
258 result2 = result2 + res[1]
259 return result, result2
261 def set_indices(self, cr, uid, ids, context = {}):
262 if not ids or (ids and not ids[0]):
264 res = self.read(cr, uid, ids, ['revision_ids', 'revision_type'])
265 rev_ids = res[0]['revision_ids']
268 for rev_id in rev_ids:
269 if res[0]['revision_type'] == 'numeric':
270 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : idx})
272 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : "%c"%(idx+96,)})
278 class mrp_bom_revision(osv.osv):
279 _name = 'mrp.bom.revision'
280 _description = 'Bill of material revisions'
282 'name': fields.char('Modification name', size=64, required=True),
283 'description': fields.text('Description'),
284 'date': fields.date('Modification Date'),
285 'indice': fields.char('Revision', size=16),
286 'last_indice': fields.char('last indice', size=64),
287 'author_id': fields.many2one('res.users', 'Author'),
288 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
292 'author_id': lambda x,y,z,c: z,
293 'date': lambda *a: time.strftime('%Y-%m-%d'),
301 return round(f / r) * r
303 class mrp_production(osv.osv):
304 _name = 'mrp.production'
305 _description = 'Production'
306 _date_name = 'date_planned'
308 'name': fields.char('Reference', size=64, required=True),
309 'origin': fields.char('Origin', size=64),
310 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
312 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
313 'product_qty': fields.float('Product Qty', required=True),
314 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
315 'product_uos_qty': fields.float('Product Qty'),
316 'product_uos': fields.many2one('product.uom', 'Product UOM'),
318 'location_src_id': fields.many2one('stock.location', 'Raw Products Location', required=True),
319 'location_dest_id': fields.many2one('stock.location', 'Finnished Products Location', required=True),
321 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
322 'date_start': fields.datetime('Start Date'),
323 'date_finnished': fields.datetime('End Date'),
325 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
327 'picking_id': fields.many2one('stock.picking', 'Packing list', readonly=True),
328 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
329 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
331 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
332 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
333 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Workcenters Utilisation'),
335 'state': fields.selection([('draft','Draft'),('picking_except', 'Packing Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','In Production'),('cancel','Canceled'),('done','Done')],'Status', readonly=True)
338 'priority': lambda *a: '1',
339 'state': lambda *a: 'draft',
340 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
341 'product_qty': lambda *a: 1.0,
342 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
344 _order = 'date_planned asc, priority desc';
346 def product_id_change(self, cr, uid, ids, product):
349 res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
350 uom = res['uom_id'] and res['uom_id'][0]
351 result = {'product_uom':uom}
352 return {'value':result}
354 def action_picking_except(self, cr, uid, ids):
355 self.write(cr, uid, ids, {'state':'picking_except'})
358 def action_compute(self, cr, uid, ids, properties=[]):
360 for production in self.browse(cr, uid, ids):
361 cr.execute('delete from mrp_production_product_line where production_id=%d', (production.id,))
362 cr.execute('delete from mrp_production_workcenter_line where production_id=%d', (production.id,))
363 bom_point = production.bom_id
364 bom_id = production.bom_id.id
366 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
368 self.write(cr, uid, [production.id], {'bom_id': bom_id})
369 bom_point = self.pool.get('mrp.bom').browse(cr, uid, [bom_id])[0]
372 raise osv.except_osv('Error', "Couldn't find bill of material for product")
374 #if bom_point.routing_id and bom_point.routing_id.location_id:
375 # self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
377 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
378 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
382 line['production_id'] = production.id
383 self.pool.get('mrp.production.product.line').create(cr, uid, line)
384 for line in results2:
385 line['production_id'] = production.id
386 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
389 def action_cancel(self, cr, uid, ids):
390 for production in self.browse(cr, uid, ids):
391 if production.move_created_ids:
392 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
393 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
394 self.write(cr, uid, ids, {'state':'cancel','move_lines':[(6,0,[])]})
397 #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
398 # between the end of the picking list and the call to this function
399 def action_ready(self, cr, uid, ids):
400 self.write(cr, uid, ids, {'state':'ready'})
401 for production in self.browse(cr, uid, ids):
402 if production.move_prod_id:
403 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
404 {'location_id':production.location_dest_id.id})
407 #TODO Review materials in function in_prod and prod_end.
408 def action_production_end(self, cr, uid, ids):
410 for production in self.browse(cr, uid, ids):
411 for res in production.move_lines:
412 for move in production.move_created_ids:
413 #XXX must use the orm
414 cr.execute('INSERT INTO stock_move_history_ids \
415 (parent_id, child_id) VALUES (%d,%d)',
417 move_ids.append(res.id)
418 if production.move_created_ids:
419 #TODO There we should handle the residus move creation
420 vals= {'state':'confirmed'}
421 new_moves = [x.id for x in production.move_created_ids]
422 self.pool.get('stock.move').write(cr, uid, new_moves, vals)
424 #XXX Why is it there ? Aren't we suppose to already have a created_move ?
425 source = production.product_id.product_tmpl_id.property_stock_production.id
427 'name':'PROD:'+production.name,
428 'date_planned': production.date_planned,
429 'product_id': production.product_id.id,
430 'product_qty': production.product_qty,
431 'product_uom': production.product_uom.id,
432 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
433 'product_uos': production.product_uos and production.product_uos.id or False,
434 'location_id': source,
435 'location_dest_id': production.location_dest_id.id,
436 'move_dest_id': production.move_prod_id.id,
439 new_moves = [self.pool.get('stock.move').create(cr, uid, vals)]
440 self.write(cr, uid, [production.id],
441 {'move_created_ids': [(6, 'WTF', new_moves)]})
442 if not production.date_finnished:
443 self.write(cr, uid, [production.id],
444 {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
445 self.pool.get('stock.move').check_assign(cr, uid, new_moves)
446 self.pool.get('stock.move').action_done(cr, uid, new_moves)
447 self._costs_generate(cr, uid, production)
448 self.pool.get('stock.move').action_done(cr, uid, move_ids)
449 self.write(cr, uid, ids, {'state': 'done'})
452 def _costs_generate(self, cr, uid, production):
454 for wc_line in production.workcenter_lines:
455 wc = wc_line.workcenter_id
456 if wc.costs_journal_id and wc.costs_general_account_id:
457 value = wc_line.hour * wc.costs_hour
458 account = wc.costs_hour_account_id.id
459 if value and account:
461 self.pool.get('account.analytic.line').create(cr, uid, {
462 'name': wc_line.name+' (H)',
464 'account_id': account,
465 'general_account_id': wc.costs_general_account_id.id,
466 'journal_id': wc.costs_journal_id.id,
469 if wc.costs_journal_id and wc.costs_general_account_id:
470 value = wc_line.cycle * wc.costs_cycle
471 account = wc.costs_cycle_account_id.id
472 if value and account:
474 self.pool.get('account.analytic.line').create(cr, uid, {
475 'name': wc_line.name+' (C)',
477 'account_id': account,
478 'general_account_id': wc.costs_general_account_id.id,
479 'journal_id': wc.costs_journal_id.id,
484 def action_in_production(self, cr, uid, ids):
486 for production in self.browse(cr, uid, ids):
487 for res in production.move_lines:
488 move_ids.append(res.id)
489 if not production.date_start:
490 self.write(cr, uid, [production.id],
491 {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
492 self.pool.get('stock.move').action_done(cr, uid, move_ids)
493 self.write(cr, uid, ids, {'state': 'in_production'})
496 def test_if_product(self, cr, uid, ids):
498 for production in self.browse(cr, uid, ids):
499 if not production.product_lines:
500 if not self.action_compute(cr, uid, [production.id]):
504 def _get_auto_picking(self, cr, uid, production):
507 def action_confirm(self, cr, uid, ids):
509 for production in self.browse(cr, uid, ids):
510 if not production.product_lines:
511 self.action_compute(cr, uid, [production.id])
512 production = self.browse(cr, uid, [production.id])[0]
514 pick_type = 'internal'
516 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
517 routing_loc = production.bom_id.routing_id.location_id
518 if routing_loc.usage<>'internal':
520 address_id = routing_loc.address_id and routing_loc.address_id.id or False
521 routing_loc = routing_loc.id
522 picking_id = self.pool.get('stock.picking').create(cr, uid, {
523 'origin': (production.origin or '').split(':')[0] +':'+production.name,
527 'address_id': address_id,
528 'auto_picking': self._get_auto_picking(cr, uid, production),
532 source = production.product_id.product_tmpl_id.property_stock_production.id
534 'name':'PROD:'+production.name,
535 'date_planned': production.date_planned,
536 'product_id': production.product_id.id,
537 'product_qty': production.product_qty,
538 'product_uom': production.product_uom.id,
539 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
540 'product_uos': production.product_uos and production.product_uos.id or False,
541 'location_id': source,
542 'location_dest_id': production.location_dest_id.id,
543 'move_dest_id': production.move_prod_id.id,
546 res_final_id = self.pool.get('stock.move').create(cr, uid, data)
548 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 'WTF', [res_final_id])]})
550 for line in production.product_lines:
552 newdate = production.date_planned
553 if line.product_id.type in ('product', 'consu'):
554 res_dest_id = self.pool.get('stock.move').create(cr, uid, {
555 'name':'PROD:'+production.name,
556 'date_planned': production.date_planned,
557 'product_id': line.product_id.id,
558 'product_qty': line.product_qty,
559 'product_uom': line.product_uom.id,
560 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
561 'product_uos': line.product_uos and line.product_uos.id or False,
562 'location_id': routing_loc or production.location_src_id.id,
563 'location_dest_id': source,
564 'move_dest_id': res_final_id,
567 moves.append(res_dest_id)
568 move_id = self.pool.get('stock.move').create(cr, uid, {
569 'name':'PROD:'+production.name,
570 'picking_id':picking_id,
571 'product_id': line.product_id.id,
572 'product_qty': line.product_qty,
573 'product_uom': line.product_uom.id,
574 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
575 'product_uos': line.product_uos and line.product_uos.id or False,
576 'date_planned': newdate,
577 'move_dest_id': res_dest_id,
578 'location_id': production.location_src_id.id,
579 'location_dest_id': routing_loc or production.location_src_id.id,
582 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
583 'name': (production.origin or '').split(':')[0] + ':' + production.name,
584 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
585 'date_planned': newdate,
586 'product_id': line.product_id.id,
587 'product_qty': line.product_qty,
588 'product_uom': line.product_uom.id,
589 'product_uos_qty': line.product_uos and line.product_qty or False,
590 'product_uos': line.product_uos and line.product_uos.id or False,
591 'location_id': production.location_src_id.id,
592 'procure_method': line.product_id.procure_method,
595 wf_service = netsvc.LocalService("workflow")
596 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
598 wf_service = netsvc.LocalService("workflow")
599 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
600 self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
603 def force_production(self, cr, uid, ids, *args):
604 pick_obj = self.pool.get('stock.picking')
605 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
611 class stock_move(osv.osv):
613 _inherit = 'stock.move'
616 'production_id': fields.many2one('mrp.production', 'Production', select=True),
621 class mrp_production_workcenter_line(osv.osv):
622 _name = 'mrp.production.workcenter.line'
623 _description = 'Production workcenters used'
625 'name': fields.char('Name', size=64, required=True),
626 'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
627 'cycle': fields.float('Nbr of cycle'),
628 'hour': fields.float('Nbr of hour'),
629 'sequence': fields.integer('Sequence', required=True),
630 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
633 'sequence': lambda *a: 1,
634 'hour': lambda *a: 0,
635 'cycle': lambda *a: 0,
637 mrp_production_workcenter_line()
639 class mrp_production_product_line(osv.osv):
640 _name = 'mrp.production.product.line'
641 _description = 'Production scheduled products'
643 'name': fields.char('Name', size=64, required=True),
644 'product_id': fields.many2one('product.product', 'Product', required=True),
645 'product_qty': fields.float('Product Qty', required=True),
646 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
647 'product_uos_qty': fields.float('Product UOS Qty'),
648 'product_uos': fields.many2one('product.uom', 'Product UOS'),
649 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
651 mrp_production_product_line()
653 # ------------------------------------------------------------------
655 # ------------------------------------------------------------------
657 # Produce, Buy or Find products and place a move
658 # then wizard for picking lists & move
660 class mrp_procurement(osv.osv):
661 _name = "mrp.procurement"
662 _description = "Procurement"
664 'name': fields.char('Name', size=64, required=True),
665 'origin': fields.char('Origin', size=64),
666 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
667 'date_planned': fields.date('Scheduled date', required=True),
668 'date_close': fields.date('Date Closed'),
669 'product_id': fields.many2one('product.product', 'Product', required=True),
670 'product_qty': fields.float('Quantity', required=True),
671 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
672 'product_uos_qty': fields.float('UoS Quantity'),
673 'product_uos': fields.many2one('product.uom', 'Product UoS'),
674 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
676 'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
678 'close_move': fields.boolean('Close Move at end', required=True),
679 'location_id': fields.many2one('stock.location', 'Location', required=True),
680 'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]}, readonly=True, required=True),
682 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
683 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'),
685 'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
687 'message': fields.char('Latest error', size=64),
688 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('exception','Exception'),('running','Running'),('cancel','Cancel'),('done','Done'),('waiting','Waiting')], 'Status')
691 'state': lambda *a: 'draft',
692 'priority': lambda *a: '1',
693 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
694 'close_move': lambda *a: 0,
695 'procure_method': lambda *a: 'make_to_order',
697 def check_product(self, cr, uid, ids):
698 for procurement in self.browse(cr, uid, ids):
699 if procurement.product_id.type in ('product', 'consu'):
703 def check_move_cancel(self, cr, uid, ids, context={}):
705 for procurement in self.browse(cr, uid, ids, context):
706 if procurement.move_id:
707 if not procurement.move_id.state=='cancel':
711 def check_move_done(self, cr, uid, ids, context={}):
713 for proc in self.browse(cr, uid, ids, context):
715 if not proc.move_id.state=='done':
720 # This method may be overrided by objects that override mrp.procurment
721 # for computing their own purpose
723 def _quantity_compute_get(self, cr, uid, proc, context={}):
724 if proc.product_id.type=='product':
725 return proc.move_id.product_uos_qty
728 def _uom_compute_get(self, cr, uid, proc, context={}):
729 if proc.product_id.type=='product':
730 if proc.move_id.product_uos:
731 return proc.move_id.product_uos.id
735 # Return the quantity of product shipped/produced/served, wich may be
736 # different from the planned quantity
738 def quantity_get(self, cr, uid, id, context={}):
739 proc = self.browse(cr, uid, id, context)
740 result = self._quantity_compute_get(cr, uid, proc, context)
742 result = proc.product_qty
745 def uom_get(self, cr, uid, id, context=None):
746 proc = self.browse(cr, uid, id, context)
747 result = self._uom_compute_get(cr, uid, proc, context)
749 result = proc.product_uom.id
752 def check_waiting(self, cr, uid, ids, context=[]):
753 for procurement in self.browse(cr, uid, ids, context=context):
754 if procurement.move_id and procurement.move_id.state=='auto':
758 def check_produce_service(self, cr, uid, procurement, context=[]):
761 def check_produce_product(self, cr, uid, procurement, context=[]):
762 properties = [x.id for x in procurement.property_ids]
763 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
765 cr.execute('update mrp_procurement set message=%s where id=%d', ('No BoM defined for this product !', procurement.id))
769 def check_make_to_stock(self, cr, uid, ids, context={}):
771 for procurement in self.browse(cr, uid, ids, context=context):
772 if procurement.product_id.type=='service':
773 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
775 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
778 def check_produce(self, cr, uid, ids, context={}):
780 user = self.pool.get('res.users').browse(cr, uid, uid)
781 for procurement in self.browse(cr, uid, ids):
782 if procurement.product_id.product_tmpl_id.supply_method=='buy':
783 if procurement.product_id.seller_ids:
784 partner = procurement.product_id.seller_ids[0].name
785 if user.company_id and user.company_id.partner_id:
786 if partner.id == user.company_id.partner_id.id:
789 if procurement.product_id.product_tmpl_id.type=='service':
790 res = res and self.check_produce_service(cr, uid, procurement, context)
792 res = res and self.check_produce_product(cr, uid, procurement, context)
797 def check_buy(self, cr, uid, ids):
798 user = self.pool.get('res.users').browse(cr, uid, uid)
799 for procurement in self.browse(cr, uid, ids):
800 if procurement.product_id.product_tmpl_id.supply_method=='produce':
802 if not procurement.product_id.seller_ids:
803 cr.execute('update mrp_procurement set message=%s where id=%d', ('No supplier defined for this product !', procurement.id))
805 partner = procurement.product_id.seller_ids[0].name
806 if user.company_id and user.company_id.partner_id:
807 if partner.id == user.company_id.partner_id.id:
809 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
811 cr.execute('update mrp_procurement set message=%s where id=%d', ('No address defined for the supplier', procurement.id))
815 def test_cancel(self, cr, uid, ids):
816 for record in self.browse(cr, uid, ids):
817 if record.move_id and record.move_id.state=='cancel':
821 def action_confirm(self, cr, uid, ids):
822 for procurement in self.browse(cr, uid, ids):
823 if procurement.product_id.type in ('product', 'consu'):
824 if not procurement.move_id:
825 source = procurement.location_id.id
826 if procurement.procure_method=='make_to_order':
827 source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
828 id = self.pool.get('stock.move').create(cr, uid, {
829 'name': 'PROC:'+procurement.name,
830 'location_id': source,
831 'location_dest_id': procurement.location_id.id,
832 'product_id': procurement.product_id.id,
833 'product_qty':procurement.product_qty,
834 'product_uom': procurement.product_uom.id,
835 'date_planned': procurement.date_planned,
838 self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
841 if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('waiting',):
842 id = self.pool.get('stock.move').write(cr, uid, [procurement.move_id.id], {'state':'confirmed'})
843 self.write(cr, uid, ids, {'state':'confirmed','message':''})
846 def action_move_assigned(self, cr, uid, ids):
847 self.write(cr, uid, ids, {'state':'running','message':'from stock: products assigned.'})
850 def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
853 def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
855 if procurement.move_id:
856 id = procurement.move_id.id
857 if not (procurement.move_id.state in ('done','assigned','cancel')):
858 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
859 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%d', (procurement.product_id.id,))
860 if not cr.fetchone()[0]:
861 cr.execute('update mrp_procurement set message=%s where id=%d', ('from stock and no minimum orderpoint rule defined', procurement.id))
864 def action_produce_assign_service(self, cr, uid, ids, context={}):
865 for procurement in self.browse(cr, uid, ids):
866 self.write(cr, uid, [procurement.id], {'state':'running'})
869 def action_produce_assign_product(self, cr, uid, ids, context={}):
871 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
872 for procurement in self.browse(cr, uid, ids):
873 res_id = procurement.move_id.id
874 loc_id = procurement.location_id.id
875 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)
876 newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
877 produce_id = self.pool.get('mrp.production').create(cr, uid, {
878 'origin': procurement.origin,
879 'product_id': procurement.product_id.id,
880 'product_qty': procurement.product_qty,
881 'product_uom': procurement.product_uom.id,
882 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
883 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
884 'location_src_id': procurement.location_id.id,
885 'location_dest_id': procurement.location_id.id,
886 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
887 'date_planned': newdate,
888 'move_prod_id': res_id,
890 self.write(cr, uid, [procurement.id], {'state':'running'})
891 bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
892 [produce_id], properties=[x.id for x in procurement.property_ids])
893 wf_service = netsvc.LocalService("workflow")
894 wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
897 def action_po_assign(self, cr, uid, ids, context={}):
899 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
900 for procurement in self.browse(cr, uid, ids):
901 res_id = procurement.move_id.id
902 partner = procurement.product_id.seller_ids[0].name
903 partner_id = partner.id
904 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
905 pricelist_id = partner.property_product_pricelist_purchase.id
907 uom_id = procurement.product_id.uom_po_id.id
909 qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
910 if procurement.product_id.seller_ids[0].qty:
911 qty=max(qty,procurement.product_id.seller_ids[0].qty)
913 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
915 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)
916 newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
918 'name': procurement.product_id.name,
920 'product_id': procurement.product_id.id,
921 'product_uom': uom_id,
923 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
924 'taxes_id': [(6, 0, [x.id for x in procurement.product_id.product_tmpl_id.supplier_taxes_id])],
925 'move_dest_id': res_id,
928 purchase_id = self.pool.get('purchase.order').create(cr, uid, {
929 'origin': procurement.origin,
930 'partner_id': partner_id,
931 'partner_address_id': address_id,
932 'location_id': procurement.location_id.id,
933 'pricelist_id': pricelist_id,
934 'order_line': [(0,0,line)]
936 self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
939 def action_cancel(self, cr, uid, ids):
941 for proc in self.browse(cr, uid, ids):
943 todo.append(proc.move_id.id)
945 self.pool.get('stock.move').action_cancel(cr, uid, [proc.move_id.id])
946 self.write(cr, uid, ids, {'state':'cancel'})
948 wf_service = netsvc.LocalService("workflow")
950 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
954 def action_check_finnished(self, cr, uid, ids):
957 def action_check(self, cr, uid, ids):
959 for procurement in self.browse(cr, uid, ids):
960 if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
961 self.action_done(cr, uid, [procurement.id])
965 def action_done(self, cr, uid, ids):
966 for procurement in self.browse(cr, uid, ids):
967 if procurement.move_id:
968 if procurement.close_move and (procurement.move_id.state <> 'done'):
969 self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
970 res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
972 wf_service = netsvc.LocalService("workflow")
974 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
976 def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
978 use_new_cursor: False or the dbname
982 self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
983 self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
984 use_new_cursor=use_new_cursor, context=context)
988 class stock_warehouse_orderpoint(osv.osv):
989 _name = "stock.warehouse.orderpoint"
990 _description = "Orderpoint minimum rule"
992 'name': fields.char('Name', size=32, required=True),
993 'active': fields.boolean('Active'),
994 'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
995 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
996 'location_id': fields.many2one('stock.location', 'Location', required=True),
997 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
998 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
999 'product_min_qty': fields.float('Min Quantity', required=True),
1000 'product_max_qty': fields.float('Max Quantity', required=True),
1001 'qty_multiple': fields.integer('Qty Multiple', required=True),
1002 'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order')
1005 'active': lambda *a: 1,
1006 'logic': lambda *a: 'max',
1007 'qty_multiple': lambda *a: 1,
1008 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1009 'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1011 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1013 w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1014 v = {'location_id':w.lot_stock_id.id}
1017 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1019 prod=self.pool.get('product.product').browse(cr,uid,product_id)
1020 v = {'product_uom':prod.uom_id.id}
1023 stock_warehouse_orderpoint()
1026 class StockMove(osv.osv):
1027 _inherit = 'stock.move'
1029 'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1031 def _action_explode(self, cr, uid, move, context={}):
1032 if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1033 bis = self.pool.get('mrp.bom').search(cr, uid, [
1034 ('product_id','=',move.product_id.id),
1035 ('bom_id','=',False),
1036 ('type','=','phantom')])
1038 factor = move.product_qty
1039 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1040 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1041 dest = move.product_id.product_tmpl_id.property_stock_production.id
1043 if move.state=='assigned':
1047 'picking_id': move.picking_id.id,
1048 'product_id': line['product_id'],
1049 'product_uom': line['product_uom'],
1050 'product_qty': line['product_qty'],
1051 'product_uos': line['product_uos'],
1052 'product_uos_qty': line['product_uos_qty'],
1053 'move_dest_id': move.id,
1055 'location_dest_id': dest,
1056 'move_history_ids': [(6,0,[move.id])],
1057 'move_history_ids2': [(6,0,[])],
1060 mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1061 prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1062 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1063 'name': (move.picking_id.origin or ''),
1064 'origin': (move.picking_id.origin or ''),
1065 'date_planned': move.date_planned,
1066 'product_id': line['product_id'],
1067 'product_qty': line['product_qty'],
1068 'product_uom': line['product_uom'],
1069 'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1070 'product_uos': line['product_uos'],
1071 'location_id': move.location_id.id,
1072 'procure_method': prodobj.procure_method,
1075 wf_service = netsvc.LocalService("workflow")
1076 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1077 self.pool.get('stock.move').write(cr, uid, [move.id], {
1078 'location_id': move.location_dest_id.id,
1079 'auto_validate': True,
1080 'picking_id': False,
1081 'location_id': dest,
1084 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1085 wf_service = netsvc.LocalService("workflow")
1086 wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1091 class StockPicking(osv.osv):
1092 _inherit = 'stock.picking'
1094 def test_finnished(self, cursor, user, ids):
1095 wf_service = netsvc.LocalService("workflow")
1096 res = super(StockPicking, self).test_finnished(cursor, user, ids)
1097 for picking in self.browse(cursor, user, ids):
1098 for move in picking.move_lines:
1099 if move.state == 'done' and move.procurements:
1100 for procurement in move.procurements:
1101 wf_service.trg_validate(user, 'mrp.procurement',
1102 procurement.id, 'button_check', cursor)
1106 # Explode picking by replacing phantom BoMs
1108 def action_explode(self, cr, uid, picks, *args):
1110 for move in pick.move_lines:
1111 self.pool.get('stock.move')._action_explode(cr, uid, move)
1116 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: