1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 from osv import fields
29 from mx import DateTime
30 from tools.translate import _
32 #----------------------------------------------------------
34 #----------------------------------------------------------
35 # capacity_hour : capacity per hour. default: 1.0.
36 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
37 # unit_per_cycle : how many units are produced for one cycle
39 # TODO: Work Center may be recursive ?
41 class mrp_workcenter(osv.osv):
42 _name = 'mrp.workcenter'
43 _description = 'Workcenter'
45 'name': fields.char('Workcenter Name', size=64, required=True),
46 'active': fields.boolean('Active'),
47 'type': fields.selection([('machine','Machine'),('hr','Human Resource'),('tool','Tool')], 'Type', required=True),
48 'code': fields.char('Code', size=16),
49 'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="The normal working time of the workcenter."),
50 'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
52 '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."),
54 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
55 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
56 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
57 'time_efficiency': fields.float('Time Efficiency', help="Factor that multiplies all times expressed in the workcenter."),
59 'costs_hour': fields.float('Cost per hour'),
60 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
61 help="Complete this only if you want automatic analytic accounting entries on production orders."),
62 'costs_cycle': fields.float('Cost per cycle'),
63 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
64 help="Complete this only if you want automatic analytic accounting entries on production orders."),
65 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
66 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
69 'active': lambda *a: 1,
70 'type': lambda *a: 'machine',
71 'time_efficiency': lambda *a: 1.0,
72 'capacity_per_cycle': lambda *a: 1.0,
77 class mrp_property_group(osv.osv):
78 _name = 'mrp.property.group'
79 _description = 'Property Group'
81 'name': fields.char('Property Group', size=64, required=True),
82 'description': fields.text('Description'),
86 class mrp_property(osv.osv):
87 _name = 'mrp.property'
88 _description = 'Property'
90 'name': fields.char('Name', size=64, required=True),
91 'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
92 'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
93 'description': fields.text('Description'),
96 'composition': lambda *a: 'min',
100 class mrp_routing(osv.osv):
101 _name = 'mrp.routing'
102 _description = 'Routing'
104 'name': fields.char('Name', size=64, required=True),
105 'active': fields.boolean('Active'),
106 'code': fields.char('Code', size=8),
108 'note': fields.text('Description'),
109 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Workcenters'),
111 'location_id': fields.many2one('stock.location', 'Production Location',
112 help="Keep empty if you produce at the location where the finished products are needed." \
113 "Set a location if you produce at a fixed location. This can be a partner location " \
114 "if you subcontract the manufacturing operations."
118 'active': lambda *a: 1,
122 class mrp_routing_workcenter(osv.osv):
123 _name = 'mrp.routing.workcenter'
124 _description = 'Routing workcenter usage'
126 'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
127 'name': fields.char('Name', size=64, required=True),
128 'sequence': fields.integer('Sequence'),
129 'cycle_nbr': fields.float('Number of Cycle', required=True,
130 help="A cycle is defined in the workcenter definition."),
131 'hour_nbr': fields.float('Number of Hours', required=True),
132 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade'),
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'
144 def _child_compute(self, cr, uid, ids, name, arg, context={}):
146 for bom in self.browse(cr, uid, ids, context=context):
147 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
150 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
151 if bom.type=='phantom' or ok:
152 sids = self.pool.get('mrp.bom').search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
154 bom2 = self.pool.get('mrp.bom').browse(cr, uid, sids[0], context=context)
155 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
157 def _compute_type(self, cr, uid, ids, field_name, arg, context):
158 res = dict(map(lambda x: (x,''), ids))
159 for line in self.browse(cr, uid, ids):
160 if line.type=='phantom' and not line.bom_id:
163 if line.bom_lines or line.type=='phantom':
165 if line.product_id.supply_method=='produce':
166 if line.product_id.procure_method=='make_to_stock':
167 res[line.id] = 'stock'
169 res[line.id] = 'order'
172 'name': fields.char('Name', size=64, required=True),
173 'code': fields.char('Code', size=16),
174 'active': fields.boolean('Active'),
175 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True, help=
176 "Use a phantom bill of material in raw materials lines that have to be " \
177 "automatically computed in on eproduction order and not one per level." \
178 "If you put \"Phantom/Set\" at the root level of a bill of material " \
179 "it is considered as a set or pack: the products are replaced by the components " \
180 "between the sale order to the picking without going through the production order." \
181 "The normal BoM will generate one production order per BoM level."),
182 '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', help="Rounding applied on the product quantity. For integer only values, put 1.0"),
193 'product_efficiency': fields.float('Product Efficiency', required=True, help="Efficiency on the production. A factor of 0.9 means a loss of 10% in the production."),
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 finished 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.\n' \
213 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
216 def _check_recursion(self, cr, uid, ids):
219 cr.execute('select distinct bom_id from mrp_bom where id in %s', (tuple(ids),))
220 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
226 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
230 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
232 prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
233 v = {'product_uom':prod.uom_id.id}
235 v['name'] = prod.name
239 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
241 # Why searching on BoM without parent ?
242 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
243 ids = map(lambda x: x[0], cr.fetchall())
246 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
248 for prop_id in bom.property_ids:
249 if prop_id.id in properties:
251 if (prop>max_prop) or ((max_prop==0) and not result):
256 def _bom_explode(self, cr, uid, bom, factor, properties, addthis=False, level=0):
257 factor = factor / (bom.product_efficiency or 1.0)
258 factor = rounding(factor, bom.product_rounding)
259 if factor<bom.product_rounding:
260 factor = bom.product_rounding
264 if bom.type=='phantom' and not bom.bom_lines:
265 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
267 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
268 result = result + res[0]
269 result2 = result2 + res[1]
274 if addthis and not bom.bom_lines:
277 'name': bom.product_id.name,
278 'product_id': bom.product_id.id,
279 'product_qty': bom.product_qty * factor,
280 'product_uom': bom.product_uom.id,
281 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
282 'product_uos': bom.product_uos and bom.product_uos.id or False,
285 for wc_use in bom.routing_id.workcenter_lines:
286 wc = wc_use.workcenter_id
287 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
288 mult = (d + (m and 1.0 or 0.0))
289 cycle = mult * wc_use.cycle_nbr
291 'name': bom.routing_id.name,
292 'workcenter_id': wc.id,
293 'sequence': level+(wc_use.sequence or 0),
295 'hour': float(wc_use.hour_nbr*mult + (wc.time_start+wc.time_stop+cycle*wc.time_cycle) * (wc.time_efficiency or 1.0)),
297 for bom2 in bom.bom_lines:
298 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
299 result = result + res[0]
300 result2 = result2 + res[1]
301 return result, result2
305 class mrp_bom_revision(osv.osv):
306 _name = 'mrp.bom.revision'
307 _description = 'Bill of material revisions'
309 'name': fields.char('Modification name', size=64, required=True),
310 'description': fields.text('Description'),
311 'date': fields.date('Modification Date'),
312 'indice': fields.char('Revision', size=16),
313 'last_indice': fields.char('last indice', size=64),
314 'author_id': fields.many2one('res.users', 'Author'),
315 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
319 'author_id': lambda x,y,z,c: z,
320 'date': lambda *a: time.strftime('%Y-%m-%d'),
328 return round(f / r) * r
330 class mrp_production(osv.osv):
331 _name = 'mrp.production'
332 _description = 'Production'
333 _date_name = 'date_planned'
335 def _get_sale_order(self,cr,uid,ids,field_name=False):
336 move_obj=self.pool.get('stock.move')
337 def get_parent_move(move_id):
338 move = move_obj.browse(cr,uid,move_id)
339 if move.move_dest_id:
340 return get_parent_move(move.move_dest_id.id)
342 productions=self.read(cr,uid,ids,['id','move_prod_id'])
344 for production in productions:
345 res[production['id']]=False
346 if production.get('move_prod_id',False):
347 parent_move_line=get_parent_move(production['move_prod_id'][0])
349 move = move_obj.browse(cr,uid,parent_move_line)
350 #TODO: fix me sale module can not be used here,
351 #as may be mrp can be installed without sale module
352 if field_name=='name':
353 res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.name or False
354 if field_name=='client_order_ref':
355 res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.client_order_ref or False
358 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
360 for prod in self.browse(cr, uid, ids, context=context):
365 for wc in prod.workcenter_lines:
366 result[prod.id]['hour_total'] += wc.hour
367 result[prod.id]['cycle_total'] += wc.cycle
370 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
372 for prod in self.browse(cr, uid, ids, context=context):
373 result[prod.id] = prod.date_planned
376 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
378 for prod in self.browse(cr, uid, ids, context=context):
379 result[prod.id] = prod.date_planned[:10]
382 def _sale_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
383 return self._get_sale_order(cr,uid,ids,field_name='name')
385 def _sale_ref_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
386 return self._get_sale_order(cr,uid,ids,field_name='client_order_ref')
389 'name': fields.char('Reference', size=64, required=True),
390 'origin': fields.char('Origin', size=64),
391 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
393 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
394 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
395 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
396 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
397 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
399 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
400 help="Location where the system will look for products used in raw materials."),
401 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
402 help="Location where the system will stock the finished products."),
404 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End'),
405 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
406 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
407 'date_start': fields.datetime('Start Date'),
408 'date_finnished': fields.datetime('End Date'),
410 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
411 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null'),
413 'picking_id': fields.many2one('stock.picking', 'Packing list', readonly=True,
414 help="This is the internal picking list take bring the raw materials to the production plan."),
415 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
416 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
418 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
419 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
420 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Workcenters Utilisation'),
422 '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),
423 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder'),
424 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder'),
426 'sale_name': fields.function(_sale_name_calc, method=True, type='char', string='Sale Name'),
427 'sale_ref': fields.function(_sale_ref_calc, method=True, type='char', string='Sale Ref'),
430 'priority': lambda *a: '1',
431 'state': lambda *a: 'draft',
432 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
433 'product_qty': lambda *a: 1.0,
434 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
436 _order = 'date_planned asc, priority desc';
437 def unlink(self, cr, uid, ids, context=None):
438 productions = self.read(cr, uid, ids, ['state'])
440 for s in productions:
441 if s['state'] in ['draft','cancel']:
442 unlink_ids.append(s['id'])
444 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
445 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
447 def copy(self, cr, uid, id, default=None,context=None):
451 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
453 'move_created_ids': [],
456 return super(mrp_production, self).copy(cr, uid, id, default, context)
458 def location_id_change(self, cr, uid, ids, src, dest, context={}):
462 return {'value': {'location_dest_id': src}}
465 def product_id_change(self, cr, uid, ids, product):
468 res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
469 uom = res['uom_id'] and res['uom_id'][0]
470 result = {'product_uom':uom}
471 return {'value':result}
473 def bom_id_change(self, cr, uid, ids, product):
476 res = self.pool.get('mrp.bom').read(cr, uid, [product], ['routing_id'])[0]
477 routing_id = res['routing_id'] and res['routing_id'][0]
478 result = {'routing_id':routing_id}
479 return {'value':result}
481 def action_picking_except(self, cr, uid, ids):
482 self.write(cr, uid, ids, {'state':'picking_except'})
485 def action_compute(self, cr, uid, ids, properties=[]):
487 for production in self.browse(cr, uid, ids):
488 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
489 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
490 bom_point = production.bom_id
491 bom_id = production.bom_id.id
493 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
495 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id)
496 routing_id = bom_point.routing_id.id or False
497 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
500 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
502 #if bom_point.routing_id and bom_point.routing_id.location_id:
503 # self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
505 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
506 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
510 line['production_id'] = production.id
511 self.pool.get('mrp.production.product.line').create(cr, uid, line)
512 for line in results2:
513 line['production_id'] = production.id
514 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
517 def action_cancel(self, cr, uid, ids):
518 for production in self.browse(cr, uid, ids):
519 if production.move_created_ids:
520 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
521 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
522 self.write(cr, uid, ids, {'state':'cancel'}) #,'move_lines':[(6,0,[])]})
525 #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
526 # between the end of the picking list and the call to this function
527 def action_ready(self, cr, uid, ids):
528 self.write(cr, uid, ids, {'state':'ready'})
529 for production in self.browse(cr, uid, ids):
530 if production.move_prod_id:
531 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
532 {'location_id':production.location_dest_id.id})
535 #TODO Review materials in function in_prod and prod_end.
536 def action_production_end(self, cr, uid, ids):
538 for production in self.browse(cr, uid, ids):
539 for res in production.move_lines:
540 for move in production.move_created_ids:
541 #XXX must use the orm
542 cr.execute('INSERT INTO stock_move_history_ids \
543 (parent_id, child_id) VALUES (%s,%s)',
545 # move_ids.append(res.id)
546 vals= {'state':'confirmed'}
547 new_moves = [x.id for x in production.move_created_ids]
548 self.pool.get('stock.move').write(cr, uid, new_moves, vals)
549 if not production.date_finnished:
550 self.write(cr, uid, [production.id],
551 {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
552 self.pool.get('stock.move').check_assign(cr, uid, new_moves)
553 self.pool.get('stock.move').action_done(cr, uid, new_moves)
554 self._costs_generate(cr, uid, production)
555 # self.pool.get('stock.move').action_done(cr, uid, move_ids)
556 self.write(cr, uid, ids, {'state': 'done'})
559 def _costs_generate(self, cr, uid, production):
561 for wc_line in production.workcenter_lines:
562 wc = wc_line.workcenter_id
563 if wc.costs_journal_id and wc.costs_general_account_id:
564 value = wc_line.hour * wc.costs_hour
565 account = wc.costs_hour_account_id.id
566 if value and account:
568 self.pool.get('account.analytic.line').create(cr, uid, {
569 'name': wc_line.name+' (H)',
571 'account_id': account,
572 'general_account_id': wc.costs_general_account_id.id,
573 'journal_id': wc.costs_journal_id.id,
576 if wc.costs_journal_id and wc.costs_general_account_id:
577 value = wc_line.cycle * wc.costs_cycle
578 account = wc.costs_cycle_account_id.id
579 if value and account:
581 self.pool.get('account.analytic.line').create(cr, uid, {
582 'name': wc_line.name+' (C)',
584 'account_id': account,
585 'general_account_id': wc.costs_general_account_id.id,
586 'journal_id': wc.costs_journal_id.id,
591 def action_in_production(self, cr, uid, ids):
593 for production in self.browse(cr, uid, ids):
594 for res in production.move_lines:
595 move_ids.append(res.id)
596 if not production.date_start:
597 self.write(cr, uid, [production.id],
598 {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
599 self.pool.get('stock.move').action_done(cr, uid, move_ids)
600 self.write(cr, uid, ids, {'state': 'in_production'})
603 def test_if_product(self, cr, uid, ids):
605 for production in self.browse(cr, uid, ids):
606 if not production.product_lines:
607 if not self.action_compute(cr, uid, [production.id]):
611 def _get_auto_picking(self, cr, uid, production):
614 def action_confirm(self, cr, uid, ids):
617 for production in self.browse(cr, uid, ids):
618 if not production.product_lines:
619 self.action_compute(cr, uid, [production.id])
620 production = self.browse(cr, uid, [production.id])[0]
622 pick_type = 'internal'
624 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
625 routing_loc = production.bom_id.routing_id.location_id
626 if routing_loc.usage<>'internal':
628 address_id = routing_loc.address_id and routing_loc.address_id.id or False
629 routing_loc = routing_loc.id
630 picking_id = self.pool.get('stock.picking').create(cr, uid, {
631 'origin': (production.origin or '').split(':')[0] +':'+production.name,
635 'address_id': address_id,
636 'auto_picking': self._get_auto_picking(cr, uid, production),
639 source = production.product_id.product_tmpl_id.property_stock_production.id
641 'name':'PROD:'+production.name,
642 'date_planned': production.date_planned,
643 'product_id': production.product_id.id,
644 'product_qty': production.product_qty,
645 'product_uom': production.product_uom.id,
646 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
647 'product_uos': production.product_uos and production.product_uos.id or False,
648 'location_id': source,
649 'location_dest_id': production.location_dest_id.id,
650 'move_dest_id': production.move_prod_id.id,
653 res_final_id = self.pool.get('stock.move').create(cr, uid, data)
655 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
657 for line in production.product_lines:
659 newdate = production.date_planned
660 if line.product_id.type in ('product', 'consu'):
661 res_dest_id = self.pool.get('stock.move').create(cr, uid, {
662 'name':'PROD:'+production.name,
663 'date_planned': production.date_planned,
664 'product_id': line.product_id.id,
665 'product_qty': line.product_qty,
666 'product_uom': line.product_uom.id,
667 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
668 'product_uos': line.product_uos and line.product_uos.id or False,
669 'location_id': routing_loc or production.location_src_id.id,
670 'location_dest_id': source,
671 'move_dest_id': res_final_id,
674 moves.append(res_dest_id)
675 move_id = self.pool.get('stock.move').create(cr, uid, {
676 'name':'PROD:'+production.name,
677 'picking_id':picking_id,
678 'product_id': line.product_id.id,
679 'product_qty': line.product_qty,
680 'product_uom': line.product_uom.id,
681 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
682 'product_uos': line.product_uos and line.product_uos.id or False,
683 'date_planned': newdate,
684 'move_dest_id': res_dest_id,
685 'location_id': production.location_src_id.id,
686 'location_dest_id': routing_loc or production.location_src_id.id,
689 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
690 'name': (production.origin or '').split(':')[0] + ':' + production.name,
691 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
692 'date_planned': newdate,
693 'product_id': line.product_id.id,
694 'product_qty': line.product_qty,
695 'product_uom': line.product_uom.id,
696 'product_uos_qty': line.product_uos and line.product_qty or False,
697 'product_uos': line.product_uos and line.product_uos.id or False,
698 'location_id': production.location_src_id.id,
699 'procure_method': line.product_id.procure_method,
702 wf_service = netsvc.LocalService("workflow")
703 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
704 proc_ids.append(proc_id)
705 wf_service = netsvc.LocalService("workflow")
706 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
707 self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
710 def force_production(self, cr, uid, ids, *args):
711 pick_obj = self.pool.get('stock.picking')
712 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
718 class stock_move(osv.osv):
720 _inherit = 'stock.move'
722 'production_id': fields.many2one('mrp.production', 'Production', select=True),
726 class mrp_production_workcenter_line(osv.osv):
727 _name = 'mrp.production.workcenter.line'
728 _description = 'Work Orders'
731 'name': fields.char('Work Order', size=64, required=True),
732 'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
733 'cycle': fields.float('Nbr of cycle', digits=(16,2)),
734 'hour': fields.float('Nbr of hour', digits=(16,2)),
735 'sequence': fields.integer('Sequence', required=True),
736 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
739 'sequence': lambda *a: 1,
740 'hour': lambda *a: 0,
741 'cycle': lambda *a: 0,
743 mrp_production_workcenter_line()
745 class mrp_production_product_line(osv.osv):
746 _name = 'mrp.production.product.line'
747 _description = 'Production scheduled products'
749 'name': fields.char('Name', size=64, required=True),
750 'product_id': fields.many2one('product.product', 'Product', required=True),
751 'product_qty': fields.float('Product Qty', required=True),
752 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
753 'product_uos_qty': fields.float('Product UOS Qty'),
754 'product_uos': fields.many2one('product.uom', 'Product UOS'),
755 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
757 mrp_production_product_line()
759 # ------------------------------------------------------------------
761 # ------------------------------------------------------------------
763 # Produce, Buy or Find products and place a move
764 # then wizard for picking lists & move
766 class mrp_procurement(osv.osv):
767 _name = "mrp.procurement"
768 _description = "Procurement"
770 'name': fields.char('Name', size=64, required=True),
771 'origin': fields.char('Origin', size=64,
772 help="Reference of the document that created this procurement.\n"
773 "This is automatically completed by Open ERP."),
774 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
775 'date_planned': fields.datetime('Scheduled date', required=True),
776 'date_close': fields.datetime('Date Closed'),
777 'product_id': fields.many2one('product.product', 'Product', required=True),
778 'product_qty': fields.float('Quantity', required=True, states={'draft':[('readonly',False)]}, readonly=True),
779 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
780 'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
781 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
782 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
784 'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
786 'close_move': fields.boolean('Close Move at end', required=True),
787 'location_id': fields.many2one('stock.location', 'Location', required=True),
788 'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
789 readonly=True, required=True, help="If you encode manually a procurement, you probably want to use" \
790 " a make to order method."),
792 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
793 'note': fields.text('Note'),
795 'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
797 'message': fields.char('Latest error', size=64),
798 'state': fields.selection([
800 ('confirmed','Confirmed'),
801 ('exception','Exception'),
802 ('running','Running'),
806 ('waiting','Waiting')], 'Status', required=True),
807 'note' : fields.text('Note'),
810 'state': lambda *a: 'draft',
811 'priority': lambda *a: '1',
812 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
813 'close_move': lambda *a: 0,
814 'procure_method': lambda *a: 'make_to_order',
817 def unlink(self, cr, uid, ids, context=None):
818 procurements = self.read(cr, uid, ids, ['state'])
820 for s in procurements:
821 if s['state'] in ['draft','cancel']:
822 unlink_ids.append(s['id'])
824 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Procurement Order(s) which are in %s State!' % s['state']))
825 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
827 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
829 w=self.pool.get('product.product').browse(cr,uid,product_id, context)
831 'product_uom':w.uom_id.id,
832 'product_uos':w.uos_id and w.uos_id.id or w.uom_id.id
837 def check_product(self, cr, uid, ids):
838 for procurement in self.browse(cr, uid, ids):
839 if procurement.product_id.type in ('product', 'consu'):
843 def get_phantom_bom_id(self, cr, uid, ids, context=None):
844 for procurement in self.browse(cr, uid, ids, context=context):
845 if procurement.move_id and procurement.move_id.product_id.supply_method=='produce' \
846 and procurement.move_id.product_id.procure_method=='make_to_order':
847 phantom_bom_id = self.pool.get('mrp.bom').search(cr, uid, [
848 ('product_id', '=', procurement.move_id.product_id.id),
849 ('bom_id', '=', False),
850 ('type', '=', 'phantom')])
851 return phantom_bom_id
854 def check_move_cancel(self, cr, uid, ids, context={}):
857 for procurement in self.browse(cr, uid, ids, context):
858 if procurement.move_id:
860 if not procurement.move_id.state=='cancel':
864 def check_move_done(self, cr, uid, ids, context={}):
866 for proc in self.browse(cr, uid, ids, context):
868 if not proc.move_id.state=='done':
873 # This method may be overrided by objects that override mrp.procurment
874 # for computing their own purpose
876 def _quantity_compute_get(self, cr, uid, proc, context={}):
877 if proc.product_id.type=='product':
878 if proc.move_id.product_uos:
879 return proc.move_id.product_uos_qty
882 def _uom_compute_get(self, cr, uid, proc, context={}):
883 if proc.product_id.type=='product':
884 if proc.move_id.product_uos:
885 return proc.move_id.product_uos.id
889 # Return the quantity of product shipped/produced/served, wich may be
890 # different from the planned quantity
892 def quantity_get(self, cr, uid, id, context={}):
893 proc = self.browse(cr, uid, id, context)
894 result = self._quantity_compute_get(cr, uid, proc, context)
896 result = proc.product_qty
899 def uom_get(self, cr, uid, id, context=None):
900 proc = self.browse(cr, uid, id, context)
901 result = self._uom_compute_get(cr, uid, proc, context)
903 result = proc.product_uom.id
906 def check_waiting(self, cr, uid, ids, context=[]):
907 for procurement in self.browse(cr, uid, ids, context=context):
908 if procurement.move_id and procurement.move_id.state=='auto':
912 def check_produce_service(self, cr, uid, procurement, context=[]):
915 def check_produce_product(self, cr, uid, procurement, context=[]):
916 properties = [x.id for x in procurement.property_ids]
917 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
919 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No BoM defined for this product !'), procurement.id))
923 def check_make_to_stock(self, cr, uid, ids, context={}):
925 for procurement in self.browse(cr, uid, ids, context=context):
926 if procurement.product_id.type=='service':
927 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
929 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
932 def check_produce(self, cr, uid, ids, context={}):
934 user = self.pool.get('res.users').browse(cr, uid, uid)
935 for procurement in self.browse(cr, uid, ids):
936 if procurement.product_id.product_tmpl_id.supply_method<>'produce':
937 if procurement.product_id.seller_ids:
938 partner = procurement.product_id.seller_ids[0].name
939 if user.company_id and user.company_id.partner_id:
940 if partner.id == user.company_id.partner_id.id:
943 if procurement.product_id.product_tmpl_id.type=='service':
944 res = res and self.check_produce_service(cr, uid, procurement, context)
946 res = res and self.check_produce_product(cr, uid, procurement, context)
951 def check_buy(self, cr, uid, ids):
952 user = self.pool.get('res.users').browse(cr, uid, uid)
953 for procurement in self.browse(cr, uid, ids):
954 if procurement.product_id.product_tmpl_id.supply_method<>'buy':
956 if not procurement.product_id.seller_ids:
957 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No supplier defined for this product !'), procurement.id))
959 partner = procurement.product_id.seller_ids[0].name
960 if user.company_id and user.company_id.partner_id:
961 if partner.id == user.company_id.partner_id.id:
963 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
965 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No address defined for the supplier'), procurement.id))
969 def test_cancel(self, cr, uid, ids):
970 for record in self.browse(cr, uid, ids):
971 if record.move_id and record.move_id.state=='cancel':
975 def action_confirm(self, cr, uid, ids, context={}):
976 for procurement in self.browse(cr, uid, ids):
977 if procurement.product_qty <= 0.00:
978 raise osv.except_osv(_('Data Insufficient !'), _('Please check the Quantity of Procurement Order(s), it should not be less than 1!'))
979 if procurement.product_id.type in ('product', 'consu'):
980 if not procurement.move_id:
981 source = procurement.location_id.id
982 if procurement.procure_method=='make_to_order':
983 source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
984 id = self.pool.get('stock.move').create(cr, uid, {
985 'name': 'PROC:'+procurement.name,
986 'location_id': source,
987 'location_dest_id': procurement.location_id.id,
988 'product_id': procurement.product_id.id,
989 'product_qty':procurement.product_qty,
990 'product_uom': procurement.product_uom.id,
991 'date_planned': procurement.date_planned,
994 self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
996 if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('draft','waiting',):
997 # properly call action_confirm() on stock.move to abide by the location chaining etc.
998 id = self.pool.get('stock.move').action_confirm(cr, uid, [procurement.move_id.id], context=context)
999 self.write(cr, uid, ids, {'state':'confirmed','message':''})
1002 def action_move_assigned(self, cr, uid, ids, context={}):
1003 self.write(cr, uid, ids, {'state':'running','message':_('from stock: products assigned.')})
1006 def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
1009 def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
1011 if procurement.move_id:
1012 id = procurement.move_id.id
1013 if not (procurement.move_id.state in ('done','assigned','cancel')):
1014 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
1015 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
1016 if not cr.fetchone()[0]:
1017 cr.execute('update mrp_procurement set message=%s where id=%s', (_('from stock and no minimum orderpoint rule defined'), procurement.id))
1020 def action_produce_assign_service(self, cr, uid, ids, context={}):
1021 for procurement in self.browse(cr, uid, ids):
1022 self.write(cr, uid, [procurement.id], {'state':'running'})
1025 def action_produce_assign_product(self, cr, uid, ids, context={}):
1027 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1028 for procurement in self.browse(cr, uid, ids):
1029 res_id = procurement.move_id.id
1030 loc_id = procurement.location_id.id
1031 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)
1032 newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
1033 produce_id = self.pool.get('mrp.production').create(cr, uid, {
1034 'origin': procurement.origin,
1035 'product_id': procurement.product_id.id,
1036 'product_qty': procurement.product_qty,
1037 'product_uom': procurement.product_uom.id,
1038 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
1039 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
1040 'location_src_id': procurement.location_id.id,
1041 'location_dest_id': procurement.location_id.id,
1042 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
1043 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1044 'move_prod_id': res_id,
1046 self.write(cr, uid, [procurement.id], {'state':'running'})
1047 bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
1048 [produce_id], properties=[x.id for x in procurement.property_ids])
1049 wf_service = netsvc.LocalService("workflow")
1050 wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1051 self.pool.get('stock.move').write(cr, uid, [res_id],
1052 {'location_id':procurement.location_id.id})
1055 def action_po_assign(self, cr, uid, ids, context={}):
1057 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1058 for procurement in self.browse(cr, uid, ids):
1059 res_id = procurement.move_id.id
1060 partner = procurement.product_id.seller_ids[0].name
1061 partner_id = partner.id
1062 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1063 pricelist_id = partner.property_product_pricelist_purchase.id
1065 uom_id = procurement.product_id.uom_po_id.id
1067 qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1068 if procurement.product_id.seller_ids[0].qty:
1069 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1071 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1073 newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
1074 newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1075 newdate = newdate - procurement.product_id.seller_ids[0].delay
1077 #Passing partner_id to context for purchase order line integrity of Line name
1078 context.update({'lang':partner.lang, 'partner_id':partner_id})
1080 product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1083 'name': product.partner_ref,
1085 'product_id': procurement.product_id.id,
1086 'product_uom': uom_id,
1087 'price_unit': price,
1088 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1089 'move_dest_id': res_id,
1090 'notes':product.description_purchase,
1093 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1094 taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes_ids)
1096 'taxes_id':[(6,0,taxes)]
1098 purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1099 'origin': procurement.origin,
1100 'partner_id': partner_id,
1101 'partner_address_id': address_id,
1102 'location_id': procurement.location_id.id,
1103 'pricelist_id': pricelist_id,
1104 'order_line': [(0,0,line)],
1105 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1107 self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1110 def action_cancel(self, cr, uid, ids):
1113 for proc in self.browse(cr, uid, ids):
1114 if proc.close_move and proc.move_id:
1115 if proc.move_id.state not in ('done','cancel'):
1116 todo2.append(proc.move_id.id)
1118 if proc.move_id and proc.move_id.state=='waiting':
1119 todo.append(proc.move_id.id)
1121 self.pool.get('stock.move').action_cancel(cr, uid, todo2)
1123 self.pool.get('stock.move').write(cr, uid, todo, {'state':'assigned'})
1124 self.write(cr, uid, ids, {'state':'cancel'})
1125 wf_service = netsvc.LocalService("workflow")
1127 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1130 def action_check_finnished(self, cr, uid, ids):
1131 return self.check_move_done(cr, uid, ids)
1133 def action_check(self, cr, uid, ids):
1135 for procurement in self.browse(cr, uid, ids):
1136 if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1137 self.action_done(cr, uid, [procurement.id])
1141 def action_ready(self, cr, uid, ids):
1142 res = self.write(cr, uid, ids, {'state':'ready'})
1145 def action_done(self, cr, uid, ids):
1146 for procurement in self.browse(cr, uid, ids):
1147 if procurement.move_id:
1148 if procurement.close_move and (procurement.move_id.state <> 'done'):
1149 self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1150 res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1151 wf_service = netsvc.LocalService("workflow")
1153 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1156 def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1158 use_new_cursor: False or the dbname
1162 self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1163 self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1164 use_new_cursor=use_new_cursor, context=context)
1168 class stock_warehouse_orderpoint(osv.osv):
1169 _name = "stock.warehouse.orderpoint"
1170 _description = "Orderpoint minimum rule"
1172 'name': fields.char('Name', size=32, required=True),
1173 'active': fields.boolean('Active'),
1174 'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1175 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
1176 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
1177 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')], ondelete="cascade"),
1178 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1179 'product_min_qty': fields.float('Min Quantity', required=True,
1180 help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1181 "a procurement to bring the virtual stock to the Max Quantity."),
1182 'product_max_qty': fields.float('Max Quantity', required=True,
1183 help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1184 "a procurement to bring the virtual stock to the Max Quantity."),
1185 'qty_multiple': fields.integer('Qty Multiple', required=True,
1186 help="The procurement quantity will by rounded up to this multiple."),
1187 'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order', ondelete="set null")
1190 'active': lambda *a: 1,
1191 'logic': lambda *a: 'max',
1192 'qty_multiple': lambda *a: 1,
1193 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1194 'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1197 _sql_constraints = [
1198 ( 'qty_multiple_check', 'CHECK( qty_multiple > 0 )', _('Qty Multiple must be greater than zero.')),
1201 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1203 w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1204 v = {'location_id':w.lot_stock_id.id}
1207 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1209 prod=self.pool.get('product.product').browse(cr,uid,product_id)
1210 v = {'product_uom':prod.uom_id.id}
1213 def copy(self, cr, uid, id, default=None,context={}):
1217 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.warehouse.orderpoint') or '',
1219 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
1220 stock_warehouse_orderpoint()
1223 class StockMove(osv.osv):
1224 _inherit = 'stock.move'
1226 'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1228 def copy(self, cr, uid, id, default=None, context=None):
1229 default = default or {}
1230 default['procurements'] = []
1231 return super(StockMove, self).copy(cr, uid, id, default, context)
1233 def _action_explode(self, cr, uid, move, context={}):
1234 if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1235 bis = self.pool.get('mrp.bom').search(cr, uid, [
1236 ('product_id','=',move.product_id.id),
1237 ('bom_id','=',False),
1238 ('type','=','phantom')])
1240 factor = move.product_qty
1241 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1242 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1244 if move.state=='assigned':
1248 'picking_id': move.picking_id.id,
1249 'product_id': line['product_id'],
1250 'product_uom': line['product_uom'],
1251 'product_qty': line['product_qty'],
1252 'product_uos': line['product_uos'],
1253 'product_uos_qty': line['product_uos_qty'],
1255 'name': line['name'],
1256 'move_history_ids': [(6,0,[move.id])],
1257 'move_history_ids2': [(6,0,[])],
1260 mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1261 prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1262 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1263 'name': (move.picking_id.origin or ''),
1264 'origin': (move.picking_id.origin or ''),
1265 'date_planned': move.date_planned,
1266 'product_id': line['product_id'],
1267 'product_qty': line['product_qty'],
1268 'product_uom': line['product_uom'],
1269 'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1270 'product_uos': line['product_uos'],
1271 'location_id': move.location_id.id,
1272 'procure_method': prodobj.procure_method,
1275 wf_service = netsvc.LocalService("workflow")
1276 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1277 self.pool.get('stock.move').write(cr, uid, [move.id], {
1278 'location_id': move.location_dest_id.id, # src and dest locations identical to have correct inventory, dummy move
1279 'auto_validate': True,
1280 'picking_id': False,
1283 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1284 wf_service = netsvc.LocalService("workflow")
1285 wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1290 class StockPicking(osv.osv):
1291 _inherit = 'stock.picking'
1293 def test_finnished(self, cursor, user, ids):
1294 wf_service = netsvc.LocalService("workflow")
1295 res = super(StockPicking, self).test_finnished(cursor, user, ids)
1296 for picking in self.browse(cursor, user, ids):
1297 for move in picking.move_lines:
1298 if move.state == 'done' and move.procurements:
1299 for procurement in move.procurements:
1300 wf_service.trg_validate(user, 'mrp.procurement',
1301 procurement.id, 'button_check', cursor)
1305 # Explode picking by replacing phantom BoMs
1307 def action_explode(self, cr, uid, picks, *args):
1308 for move in self.pool.get('stock.move').browse(cr, uid, picks):
1309 self.pool.get('stock.move')._action_explode(cr, uid, move)
1314 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: