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.date('Scheduled date', required=True),
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'),
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'),
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 for procurement in self.browse(cr, uid, ids):
872 res_id = procurement.move_id.id
873 loc_id = procurement.location_id.id
874 newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d') - DateTime.RelativeDateTime(days=procurement.product_id.product_tmpl_id.produce_delay or 0.0)
875 produce_id = self.pool.get('mrp.production').create(cr, uid, {
876 'origin': procurement.origin,
877 'product_id': procurement.product_id.id,
878 'product_qty': procurement.product_qty,
879 'product_uom': procurement.product_uom.id,
880 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
881 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
882 'location_src_id': procurement.location_id.id,
883 'location_dest_id': procurement.location_id.id,
884 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
885 'date_planned': newdate,
886 'move_prod_id': res_id,
888 self.write(cr, uid, [procurement.id], {'state':'running'})
889 bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
890 [produce_id], properties=[x.id for x in procurement.property_ids])
891 wf_service = netsvc.LocalService("workflow")
892 wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
895 def action_po_assign(self, cr, uid, ids):
897 for procurement in self.browse(cr, uid, ids):
898 res_id = procurement.move_id.id
899 partner = procurement.product_id.seller_ids[0].name
900 partner_id = partner.id
901 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
902 pricelist_id = partner.property_product_pricelist_purchase.id
904 uom_id = procurement.product_id.uom_po_id.id
906 qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
907 if procurement.product_id.seller_ids[0].qty:
908 qty=max(qty,procurement.product_id.seller_ids[0].qty)
910 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
912 newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d') - DateTime.RelativeDateTime(days=procurement.product_id.product_tmpl_id.seller_delay or 0.0)
914 'name': procurement.product_id.name,
916 'product_id': procurement.product_id.id,
917 'product_uom': uom_id,
919 'date_planned': newdate.strftime('%Y-%m-%d'),
920 'taxes_id': [(6, 0, [x.id for x in procurement.product_id.product_tmpl_id.supplier_taxes_id])],
921 'move_dest_id': res_id,
924 purchase_id = self.pool.get('purchase.order').create(cr, uid, {
925 'origin': procurement.origin,
926 'partner_id': partner_id,
927 'partner_address_id': address_id,
928 'location_id': procurement.location_id.id,
929 'pricelist_id': pricelist_id,
930 'order_line': [(0,0,line)]
932 self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
935 def action_cancel(self, cr, uid, ids):
937 for proc in self.browse(cr, uid, ids):
939 todo.append(proc.move_id.id)
941 self.pool.get('stock.move').action_cancel(cr, uid, [proc.move_id.id])
942 self.write(cr, uid, ids, {'state':'cancel'})
944 wf_service = netsvc.LocalService("workflow")
946 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
950 def action_check_finnished(self, cr, uid, ids):
953 def action_check(self, cr, uid, ids):
955 for procurement in self.browse(cr, uid, ids):
956 if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
957 self.action_done(cr, uid, [procurement.id])
961 def action_done(self, cr, uid, ids):
962 for procurement in self.browse(cr, uid, ids):
963 if procurement.move_id:
964 if procurement.close_move and (procurement.move_id.state <> 'done'):
965 self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
966 res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
968 wf_service = netsvc.LocalService("workflow")
970 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
972 def run_scheduler(self, cr, uid, user_id=False, schedule_cycle=1.0,\
973 po_cycle=1.0, po_lead=1.0, security_lead=50.0, picking_lead=1.0,\
974 automatic=False, use_new_cursor=False, context=None):
976 use_new_cursor: False or the dbname
980 self.run_procure_confirm(cr, uid, schedule_cycle=schedule_cycle,\
981 po_cycle=po_cycle, po_lead=po_lead, security_lead=security_lead,\
982 picking_lead=picking_lead, user_id=user_id,\
983 use_new_cursor=use_new_cursor, context=context)
984 self.run_orderpoint_confirm(cr, uid, automatic=automatic,\
985 use_new_cursor=use_new_cursor, context=context, user_id=user_id)
987 def run_procure_confirm(self, cr, uid, user_id=False, schedule_cycle=1.0,\
988 po_cycle=1.0, po_lead=1.0, security_lead=50.0, picking_lead=1.0, \
989 use_new_cursor=False, context=None):
991 use_new_cursor: False or the dbname
993 from wizard.schedulers import _procure_confirm
996 _procure_confirm(self, cr, uid, schedule_cycle=schedule_cycle,\
997 po_cycle=po_cycle, po_lead=po_lead, security_lead=security_lead,\
998 picking_lead=picking_lead, user_id=user_id, use_new_cursor=use_new_cursor,\
1001 def run_orderpoint_confirm(self, cr, uid, automatic=False, use_new_cursor=False,\
1002 context=None, user_id=False):
1004 use_new_cursor: False or the dbname
1006 from wizard.schedulers import _procure_orderpoint_confirm
1009 _procure_orderpoint_confirm(self, cr, uid, automatic=automatic,\
1010 use_new_cursor=use_new_cursor, context=context, user_id=user_id)
1014 class stock_warehouse_orderpoint(osv.osv):
1015 _name = "stock.warehouse.orderpoint"
1016 _description = "Orderpoint minimum rule"
1018 'name': fields.char('Name', size=32, required=True),
1019 'active': fields.boolean('Active'),
1020 'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1021 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1022 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1023 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1024 'product_min_qty': fields.float('Min Quantity', required=True),
1025 'product_max_qty': fields.float('Max Quantity', required=True),
1026 'qty_multiple': fields.integer('Qty Multiple', required=True),
1027 'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order')
1030 'active': lambda *a: 1,
1031 'logic': lambda *a: 'max',
1032 'qty_multiple': lambda *a: 1,
1033 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1034 'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1036 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1038 prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
1039 v = {'product_uom':prod.uom_id.id}
1042 stock_warehouse_orderpoint()
1045 class StockMove(osv.osv):
1046 _inherit = 'stock.move'
1048 'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1050 def _action_explode(self, cr, uid, move, context={}):
1051 if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1052 bis = self.pool.get('mrp.bom').search(cr, uid, [
1053 ('product_id','=',move.product_id.id),
1054 ('bom_id','=',False),
1055 ('type','=','phantom')])
1057 factor = move.product_qty
1058 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1059 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1060 dest = move.product_id.product_tmpl_id.property_stock_production.id
1062 if move.state=='assigned':
1066 'picking_id': move.picking_id.id,
1067 'product_id': line['product_id'],
1068 'product_uom': line['product_uom'],
1069 'product_qty': line['product_qty'],
1070 'product_uos': line['product_uos'],
1071 'product_uos_qty': line['product_uos_qty'],
1072 'move_dest_id': move.id,
1074 'location_dest_id': dest,
1075 'move_history_ids': [(6,0,[move.id])],
1076 'move_history_ids2': [(6,0,[])],
1079 mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1080 prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1081 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1082 'name': (move.picking_id.origin or ''),
1083 'origin': (move.picking_id.origin or ''),
1084 'date_planned': move.date_planned,
1085 'product_id': line['product_id'],
1086 'product_qty': line['product_qty'],
1087 'product_uom': line['product_uom'],
1088 'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1089 'product_uos': line['product_uos'],
1090 'location_id': move.location_id.id,
1091 'procure_method': prodobj.procure_method,
1094 wf_service = netsvc.LocalService("workflow")
1095 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1096 self.pool.get('stock.move').write(cr, uid, [move.id], {
1097 'location_id': move.location_dest_id.id,
1098 'auto_validate': True,
1099 'picking_id': False,
1100 'location_id': dest,
1103 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1104 wf_service = netsvc.LocalService("workflow")
1105 wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1110 class StockPicking(osv.osv):
1111 _inherit = 'stock.picking'
1113 def test_finnished(self, cursor, user, ids):
1114 wf_service = netsvc.LocalService("workflow")
1115 res = super(StockPicking, self).test_finnished(cursor, user, ids)
1116 for picking in self.browse(cursor, user, ids):
1117 for move in picking.move_lines:
1118 if move.state == 'done' and move.procurements:
1119 for procurement in move.procurements:
1120 wf_service.trg_validate(user, 'mrp.procurement',
1121 procurement.id, 'button_check', cursor)
1125 # Explode picking by replacing phantom BoMs
1127 def action_explode(self, cr, uid, ids, *args):
1128 for pick in self.browse(cr, uid, ids):
1129 for move in pick.move_lines:
1130 self.pool.get('stock.move')._action_explode(cr, uid, move)
1135 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: