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
303 def set_indices(self, cr, uid, ids, context = {}):
304 if not ids or (ids and not ids[0]):
306 res = self.read(cr, uid, ids, ['revision_ids', 'revision_type'])
307 rev_ids = res[0]['revision_ids']
310 for rev_id in rev_ids:
311 if res[0]['revision_type'] == 'numeric':
312 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : idx})
314 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : "%c"%(idx+96,)})
320 class mrp_bom_revision(osv.osv):
321 _name = 'mrp.bom.revision'
322 _description = 'Bill of material revisions'
324 'name': fields.char('Modification name', size=64, required=True),
325 'description': fields.text('Description'),
326 'date': fields.date('Modification Date'),
327 'indice': fields.char('Revision', size=16),
328 'last_indice': fields.char('last indice', size=64),
329 'author_id': fields.many2one('res.users', 'Author'),
330 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
334 'author_id': lambda x,y,z,c: z,
335 'date': lambda *a: time.strftime('%Y-%m-%d'),
343 return round(f / r) * r
345 class mrp_production(osv.osv):
346 _name = 'mrp.production'
347 _description = 'Production'
348 _date_name = 'date_planned'
350 def _get_sale_order(self,cr,uid,ids,field_name=False):
351 move_obj=self.pool.get('stock.move')
352 def get_parent_move(move_id):
353 move = move_obj.browse(cr,uid,move_id)
354 if move.move_dest_id:
355 return get_parent_move(move.move_dest_id.id)
357 productions=self.read(cr,uid,ids,['id','move_prod_id'])
359 for production in productions:
360 res[production['id']]=False
361 if production.get('move_prod_id',False):
362 parent_move_line=get_parent_move(production['move_prod_id'][0])
364 move = move_obj.browse(cr,uid,parent_move_line)
365 #TODO: fix me sale module can not be used here,
366 #as may be mrp can be installed without sale module
367 if field_name=='name':
368 res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.name or False
369 if field_name=='client_order_ref':
370 res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.client_order_ref or False
373 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
375 for prod in self.browse(cr, uid, ids, context=context):
380 for wc in prod.workcenter_lines:
381 result[prod.id]['hour_total'] += wc.hour
382 result[prod.id]['cycle_total'] += wc.cycle
385 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
387 for prod in self.browse(cr, uid, ids, context=context):
388 result[prod.id] = prod.date_planned
391 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
393 for prod in self.browse(cr, uid, ids, context=context):
394 result[prod.id] = prod.date_planned[:10]
397 def _sale_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
398 return self._get_sale_order(cr,uid,ids,field_name='name')
400 def _sale_ref_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
401 return self._get_sale_order(cr,uid,ids,field_name='client_order_ref')
404 'name': fields.char('Reference', size=64, required=True),
405 'origin': fields.char('Origin', size=64),
406 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
408 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
409 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
410 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
411 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
412 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
414 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
415 help="Location where the system will look for products used in raw materials."),
416 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
417 help="Location where the system will stock the finished products."),
419 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End'),
420 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
421 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
422 'date_start': fields.datetime('Start Date'),
423 'date_finnished': fields.datetime('End Date'),
425 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
426 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null'),
428 'picking_id': fields.many2one('stock.picking', 'Packing list', readonly=True,
429 help="This is the internal picking list take bring the raw materials to the production plan."),
430 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
431 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
433 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
434 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
435 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Workcenters Utilisation'),
437 '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),
438 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder'),
439 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder'),
441 'sale_name': fields.function(_sale_name_calc, method=True, type='char', string='Sale Name'),
442 'sale_ref': fields.function(_sale_ref_calc, method=True, type='char', string='Sale Ref'),
445 'priority': lambda *a: '1',
446 'state': lambda *a: 'draft',
447 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
448 'product_qty': lambda *a: 1.0,
449 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
451 _order = 'date_planned asc, priority desc';
452 def unlink(self, cr, uid, ids, context=None):
453 productions = self.read(cr, uid, ids, ['state'])
455 for s in productions:
456 if s['state'] in ['draft','cancel']:
457 unlink_ids.append(s['id'])
459 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
460 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
462 def copy(self, cr, uid, id, default=None,context=None):
466 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
468 'move_created_ids': [],
471 return super(mrp_production, self).copy(cr, uid, id, default, context)
473 def location_id_change(self, cr, uid, ids, src, dest, context={}):
477 return {'value': {'location_dest_id': src}}
480 def product_id_change(self, cr, uid, ids, product):
483 res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
484 uom = res['uom_id'] and res['uom_id'][0]
485 result = {'product_uom':uom}
486 return {'value':result}
488 def bom_id_change(self, cr, uid, ids, product):
491 res = self.pool.get('mrp.bom').read(cr, uid, [product], ['routing_id'])[0]
492 routing_id = res['routing_id'] and res['routing_id'][0]
493 result = {'routing_id':routing_id}
494 return {'value':result}
496 def action_picking_except(self, cr, uid, ids):
497 self.write(cr, uid, ids, {'state':'picking_except'})
500 def action_compute(self, cr, uid, ids, properties=[]):
502 for production in self.browse(cr, uid, ids):
503 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
504 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
505 bom_point = production.bom_id
506 bom_id = production.bom_id.id
508 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
510 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id)
511 routing_id = bom_point.routing_id.id or False
512 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
515 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
517 #if bom_point.routing_id and bom_point.routing_id.location_id:
518 # self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
520 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
521 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
525 line['production_id'] = production.id
526 self.pool.get('mrp.production.product.line').create(cr, uid, line)
527 for line in results2:
528 line['production_id'] = production.id
529 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
532 def action_cancel(self, cr, uid, ids):
533 for production in self.browse(cr, uid, ids):
534 if production.move_created_ids:
535 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
536 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
537 self.write(cr, uid, ids, {'state':'cancel'}) #,'move_lines':[(6,0,[])]})
540 #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
541 # between the end of the picking list and the call to this function
542 def action_ready(self, cr, uid, ids):
543 self.write(cr, uid, ids, {'state':'ready'})
544 for production in self.browse(cr, uid, ids):
545 if production.move_prod_id:
546 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
547 {'location_id':production.location_dest_id.id})
550 #TODO Review materials in function in_prod and prod_end.
551 def action_production_end(self, cr, uid, ids):
553 for production in self.browse(cr, uid, ids):
554 for res in production.move_lines:
555 for move in production.move_created_ids:
556 #XXX must use the orm
557 cr.execute('INSERT INTO stock_move_history_ids \
558 (parent_id, child_id) VALUES (%s,%s)',
560 # move_ids.append(res.id)
561 vals= {'state':'confirmed'}
562 new_moves = [x.id for x in production.move_created_ids]
563 self.pool.get('stock.move').write(cr, uid, new_moves, vals)
564 if not production.date_finnished:
565 self.write(cr, uid, [production.id],
566 {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
567 self.pool.get('stock.move').check_assign(cr, uid, new_moves)
568 self.pool.get('stock.move').action_done(cr, uid, new_moves)
569 self._costs_generate(cr, uid, production)
570 # self.pool.get('stock.move').action_done(cr, uid, move_ids)
571 self.write(cr, uid, ids, {'state': 'done'})
574 def _costs_generate(self, cr, uid, production):
576 for wc_line in production.workcenter_lines:
577 wc = wc_line.workcenter_id
578 if wc.costs_journal_id and wc.costs_general_account_id:
579 value = wc_line.hour * wc.costs_hour
580 account = wc.costs_hour_account_id.id
581 if value and account:
583 self.pool.get('account.analytic.line').create(cr, uid, {
584 'name': wc_line.name+' (H)',
586 'account_id': account,
587 'general_account_id': wc.costs_general_account_id.id,
588 'journal_id': wc.costs_journal_id.id,
591 if wc.costs_journal_id and wc.costs_general_account_id:
592 value = wc_line.cycle * wc.costs_cycle
593 account = wc.costs_cycle_account_id.id
594 if value and account:
596 self.pool.get('account.analytic.line').create(cr, uid, {
597 'name': wc_line.name+' (C)',
599 'account_id': account,
600 'general_account_id': wc.costs_general_account_id.id,
601 'journal_id': wc.costs_journal_id.id,
606 def action_in_production(self, cr, uid, ids):
608 for production in self.browse(cr, uid, ids):
609 for res in production.move_lines:
610 move_ids.append(res.id)
611 if not production.date_start:
612 self.write(cr, uid, [production.id],
613 {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
614 self.pool.get('stock.move').action_done(cr, uid, move_ids)
615 self.write(cr, uid, ids, {'state': 'in_production'})
618 def test_if_product(self, cr, uid, ids):
620 for production in self.browse(cr, uid, ids):
621 if not production.product_lines:
622 if not self.action_compute(cr, uid, [production.id]):
626 def _get_auto_picking(self, cr, uid, production):
629 def action_confirm(self, cr, uid, ids):
632 for production in self.browse(cr, uid, ids):
633 if not production.product_lines:
634 self.action_compute(cr, uid, [production.id])
635 production = self.browse(cr, uid, [production.id])[0]
637 pick_type = 'internal'
639 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
640 routing_loc = production.bom_id.routing_id.location_id
641 if routing_loc.usage<>'internal':
643 address_id = routing_loc.address_id and routing_loc.address_id.id or False
644 routing_loc = routing_loc.id
645 picking_id = self.pool.get('stock.picking').create(cr, uid, {
646 'origin': (production.origin or '').split(':')[0] +':'+production.name,
650 'address_id': address_id,
651 'auto_picking': self._get_auto_picking(cr, uid, production),
654 source = production.product_id.product_tmpl_id.property_stock_production.id
656 'name':'PROD:'+production.name,
657 'date_planned': production.date_planned,
658 'product_id': production.product_id.id,
659 'product_qty': production.product_qty,
660 'product_uom': production.product_uom.id,
661 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
662 'product_uos': production.product_uos and production.product_uos.id or False,
663 'location_id': source,
664 'location_dest_id': production.location_dest_id.id,
665 'move_dest_id': production.move_prod_id.id,
668 res_final_id = self.pool.get('stock.move').create(cr, uid, data)
670 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
672 for line in production.product_lines:
674 newdate = production.date_planned
675 if line.product_id.type in ('product', 'consu'):
676 res_dest_id = self.pool.get('stock.move').create(cr, uid, {
677 'name':'PROD:'+production.name,
678 'date_planned': production.date_planned,
679 'product_id': line.product_id.id,
680 'product_qty': line.product_qty,
681 'product_uom': line.product_uom.id,
682 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
683 'product_uos': line.product_uos and line.product_uos.id or False,
684 'location_id': routing_loc or production.location_src_id.id,
685 'location_dest_id': source,
686 'move_dest_id': res_final_id,
689 moves.append(res_dest_id)
690 move_id = self.pool.get('stock.move').create(cr, uid, {
691 'name':'PROD:'+production.name,
692 'picking_id':picking_id,
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_uos_qty or False,
697 'product_uos': line.product_uos and line.product_uos.id or False,
698 'date_planned': newdate,
699 'move_dest_id': res_dest_id,
700 'location_id': production.location_src_id.id,
701 'location_dest_id': routing_loc or production.location_src_id.id,
704 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
705 'name': (production.origin or '').split(':')[0] + ':' + production.name,
706 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
707 'date_planned': newdate,
708 'product_id': line.product_id.id,
709 'product_qty': line.product_qty,
710 'product_uom': line.product_uom.id,
711 'product_uos_qty': line.product_uos and line.product_qty or False,
712 'product_uos': line.product_uos and line.product_uos.id or False,
713 'location_id': production.location_src_id.id,
714 'procure_method': line.product_id.procure_method,
717 wf_service = netsvc.LocalService("workflow")
718 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
719 proc_ids.append(proc_id)
720 wf_service = netsvc.LocalService("workflow")
721 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
722 self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
725 def force_production(self, cr, uid, ids, *args):
726 pick_obj = self.pool.get('stock.picking')
727 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
733 class stock_move(osv.osv):
735 _inherit = 'stock.move'
737 'production_id': fields.many2one('mrp.production', 'Production', select=True),
741 class mrp_production_workcenter_line(osv.osv):
742 _name = 'mrp.production.workcenter.line'
743 _description = 'Work Orders'
746 'name': fields.char('Work Order', size=64, required=True),
747 'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
748 'cycle': fields.float('Nbr of cycle', digits=(16,2)),
749 'hour': fields.float('Nbr of hour', digits=(16,2)),
750 'sequence': fields.integer('Sequence', required=True),
751 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
754 'sequence': lambda *a: 1,
755 'hour': lambda *a: 0,
756 'cycle': lambda *a: 0,
758 mrp_production_workcenter_line()
760 class mrp_production_product_line(osv.osv):
761 _name = 'mrp.production.product.line'
762 _description = 'Production scheduled products'
764 'name': fields.char('Name', size=64, required=True),
765 'product_id': fields.many2one('product.product', 'Product', required=True),
766 'product_qty': fields.float('Product Qty', required=True),
767 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
768 'product_uos_qty': fields.float('Product UOS Qty'),
769 'product_uos': fields.many2one('product.uom', 'Product UOS'),
770 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
772 mrp_production_product_line()
774 # ------------------------------------------------------------------
776 # ------------------------------------------------------------------
778 # Produce, Buy or Find products and place a move
779 # then wizard for picking lists & move
781 class mrp_procurement(osv.osv):
782 _name = "mrp.procurement"
783 _description = "Procurement"
785 'name': fields.char('Name', size=64, required=True),
786 'origin': fields.char('Origin', size=64,
787 help="Reference of the document that created this procurement.\n"
788 "This is automatically completed by Open ERP."),
789 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
790 'date_planned': fields.datetime('Scheduled date', required=True),
791 'date_close': fields.datetime('Date Closed'),
792 'product_id': fields.many2one('product.product', 'Product', required=True),
793 'product_qty': fields.float('Quantity', required=True, states={'draft':[('readonly',False)]}, readonly=True),
794 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
795 'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
796 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
797 'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
799 'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
801 'close_move': fields.boolean('Close Move at end', required=True),
802 'location_id': fields.many2one('stock.location', 'Location', required=True),
803 'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
804 readonly=True, required=True, help="If you encode manually a procurement, you probably want to use" \
805 " a make to order method."),
807 'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
808 'note': fields.text('Note'),
810 'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
812 'message': fields.char('Latest error', size=64),
813 'state': fields.selection([
815 ('confirmed','Confirmed'),
816 ('exception','Exception'),
817 ('running','Running'),
821 ('waiting','Waiting')], 'Status', required=True),
822 'note' : fields.text('Note'),
825 'state': lambda *a: 'draft',
826 'priority': lambda *a: '1',
827 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
828 'close_move': lambda *a: 0,
829 'procure_method': lambda *a: 'make_to_order',
832 def unlink(self, cr, uid, ids, context=None):
833 procurements = self.read(cr, uid, ids, ['state'])
835 for s in procurements:
836 if s['state'] in ['draft','cancel']:
837 unlink_ids.append(s['id'])
839 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Procurement Order(s) which are in %s State!' % s['state']))
840 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
842 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
844 w=self.pool.get('product.product').browse(cr,uid,product_id, context)
846 'product_uom':w.uom_id.id,
847 'product_uos':w.uos_id and w.uos_id.id or w.uom_id.id
852 def check_product(self, cr, uid, ids):
853 for procurement in self.browse(cr, uid, ids):
854 if procurement.product_id.type in ('product', 'consu'):
858 def get_phantom_bom_id(self, cr, uid, ids, context=None):
859 for procurement in self.browse(cr, uid, ids, context=context):
860 if procurement.move_id and procurement.move_id.product_id.supply_method=='produce' \
861 and procurement.move_id.product_id.procure_method=='make_to_order':
862 phantom_bom_id = self.pool.get('mrp.bom').search(cr, uid, [
863 ('product_id', '=', procurement.move_id.product_id.id),
864 ('bom_id', '=', False),
865 ('type', '=', 'phantom')])
866 return phantom_bom_id
869 def check_move_cancel(self, cr, uid, ids, context={}):
872 for procurement in self.browse(cr, uid, ids, context):
873 if procurement.move_id:
875 if not procurement.move_id.state=='cancel':
879 def check_move_done(self, cr, uid, ids, context={}):
881 for proc in self.browse(cr, uid, ids, context):
883 if not proc.move_id.state=='done':
888 # This method may be overrided by objects that override mrp.procurment
889 # for computing their own purpose
891 def _quantity_compute_get(self, cr, uid, proc, context={}):
892 if proc.product_id.type=='product':
893 if proc.move_id.product_uos:
894 return proc.move_id.product_uos_qty
897 def _uom_compute_get(self, cr, uid, proc, context={}):
898 if proc.product_id.type=='product':
899 if proc.move_id.product_uos:
900 return proc.move_id.product_uos.id
904 # Return the quantity of product shipped/produced/served, wich may be
905 # different from the planned quantity
907 def quantity_get(self, cr, uid, id, context={}):
908 proc = self.browse(cr, uid, id, context)
909 result = self._quantity_compute_get(cr, uid, proc, context)
911 result = proc.product_qty
914 def uom_get(self, cr, uid, id, context=None):
915 proc = self.browse(cr, uid, id, context)
916 result = self._uom_compute_get(cr, uid, proc, context)
918 result = proc.product_uom.id
921 def check_waiting(self, cr, uid, ids, context=[]):
922 for procurement in self.browse(cr, uid, ids, context=context):
923 if procurement.move_id and procurement.move_id.state=='auto':
927 def check_produce_service(self, cr, uid, procurement, context=[]):
930 def check_produce_product(self, cr, uid, procurement, context=[]):
931 properties = [x.id for x in procurement.property_ids]
932 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
934 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No BoM defined for this product !'), procurement.id))
938 def check_make_to_stock(self, cr, uid, ids, context={}):
940 for procurement in self.browse(cr, uid, ids, context=context):
941 if procurement.product_id.type=='service':
942 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
944 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
947 def check_produce(self, cr, uid, ids, context={}):
949 user = self.pool.get('res.users').browse(cr, uid, uid)
950 for procurement in self.browse(cr, uid, ids):
951 if procurement.product_id.product_tmpl_id.supply_method<>'produce':
952 if procurement.product_id.seller_ids:
953 partner = procurement.product_id.seller_ids[0].name
954 if user.company_id and user.company_id.partner_id:
955 if partner.id == user.company_id.partner_id.id:
958 if procurement.product_id.product_tmpl_id.type=='service':
959 res = res and self.check_produce_service(cr, uid, procurement, context)
961 res = res and self.check_produce_product(cr, uid, procurement, context)
966 def check_buy(self, cr, uid, ids):
967 user = self.pool.get('res.users').browse(cr, uid, uid)
968 for procurement in self.browse(cr, uid, ids):
969 if procurement.product_id.product_tmpl_id.supply_method<>'buy':
971 if not procurement.product_id.seller_ids:
972 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No supplier defined for this product !'), procurement.id))
974 partner = procurement.product_id.seller_ids[0].name
975 if user.company_id and user.company_id.partner_id:
976 if partner.id == user.company_id.partner_id.id:
978 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
980 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No address defined for the supplier'), procurement.id))
984 def test_cancel(self, cr, uid, ids):
985 for record in self.browse(cr, uid, ids):
986 if record.move_id and record.move_id.state=='cancel':
990 def action_confirm(self, cr, uid, ids, context={}):
991 for procurement in self.browse(cr, uid, ids):
992 if procurement.product_qty <= 0.00:
993 raise osv.except_osv(_('Data Insufficient !'), _('Please check the Quantity of Procurement Order(s), it should not be less than 1!'))
994 if procurement.product_id.type in ('product', 'consu'):
995 if not procurement.move_id:
996 source = procurement.location_id.id
997 if procurement.procure_method=='make_to_order':
998 source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
999 id = self.pool.get('stock.move').create(cr, uid, {
1000 'name': 'PROC:'+procurement.name,
1001 'location_id': source,
1002 'location_dest_id': procurement.location_id.id,
1003 'product_id': procurement.product_id.id,
1004 'product_qty':procurement.product_qty,
1005 'product_uom': procurement.product_uom.id,
1006 'date_planned': procurement.date_planned,
1007 'state':'confirmed',
1009 self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
1011 if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('draft','waiting',):
1012 # properly call action_confirm() on stock.move to abide by the location chaining etc.
1013 id = self.pool.get('stock.move').action_confirm(cr, uid, [procurement.move_id.id], context=context)
1014 self.write(cr, uid, ids, {'state':'confirmed','message':''})
1017 def action_move_assigned(self, cr, uid, ids, context={}):
1018 self.write(cr, uid, ids, {'state':'running','message':_('from stock: products assigned.')})
1021 def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
1024 def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
1026 if procurement.move_id:
1027 id = procurement.move_id.id
1028 if not (procurement.move_id.state in ('done','assigned','cancel')):
1029 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
1030 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
1031 if not cr.fetchone()[0]:
1032 cr.execute('update mrp_procurement set message=%s where id=%s', (_('from stock and no minimum orderpoint rule defined'), procurement.id))
1035 def action_produce_assign_service(self, cr, uid, ids, context={}):
1036 for procurement in self.browse(cr, uid, ids):
1037 self.write(cr, uid, [procurement.id], {'state':'running'})
1040 def action_produce_assign_product(self, cr, uid, ids, context={}):
1042 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1043 for procurement in self.browse(cr, uid, ids):
1044 res_id = procurement.move_id.id
1045 loc_id = procurement.location_id.id
1046 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)
1047 newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
1048 produce_id = self.pool.get('mrp.production').create(cr, uid, {
1049 'origin': procurement.origin,
1050 'product_id': procurement.product_id.id,
1051 'product_qty': procurement.product_qty,
1052 'product_uom': procurement.product_uom.id,
1053 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
1054 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
1055 'location_src_id': procurement.location_id.id,
1056 'location_dest_id': procurement.location_id.id,
1057 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
1058 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1059 'move_prod_id': res_id,
1061 self.write(cr, uid, [procurement.id], {'state':'running'})
1062 bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
1063 [produce_id], properties=[x.id for x in procurement.property_ids])
1064 wf_service = netsvc.LocalService("workflow")
1065 wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1066 self.pool.get('stock.move').write(cr, uid, [res_id],
1067 {'location_id':procurement.location_id.id})
1070 def action_po_assign(self, cr, uid, ids, context={}):
1072 company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1073 for procurement in self.browse(cr, uid, ids):
1074 res_id = procurement.move_id.id
1075 partner = procurement.product_id.seller_ids[0].name
1076 partner_id = partner.id
1077 address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1078 pricelist_id = partner.property_product_pricelist_purchase.id
1080 uom_id = procurement.product_id.uom_po_id.id
1082 qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1083 if procurement.product_id.seller_ids[0].qty:
1084 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1086 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1088 newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
1089 newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1090 newdate = newdate - procurement.product_id.seller_ids[0].delay
1092 #Passing partner_id to context for purchase order line integrity of Line name
1093 context.update({'lang':partner.lang, 'partner_id':partner_id})
1095 product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1098 'name': product.partner_ref,
1100 'product_id': procurement.product_id.id,
1101 'product_uom': uom_id,
1102 'price_unit': price,
1103 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1104 'move_dest_id': res_id,
1105 'notes':product.description_purchase,
1108 taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1109 taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes_ids)
1111 'taxes_id':[(6,0,taxes)]
1113 purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1114 'origin': procurement.origin,
1115 'partner_id': partner_id,
1116 'partner_address_id': address_id,
1117 'location_id': procurement.location_id.id,
1118 'pricelist_id': pricelist_id,
1119 'order_line': [(0,0,line)],
1120 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1122 self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1125 def action_cancel(self, cr, uid, ids):
1128 for proc in self.browse(cr, uid, ids):
1130 if proc.move_id.state not in ('done','cancel'):
1131 todo2.append(proc.move_id.id)
1133 if proc.move_id and proc.move_id.state=='waiting':
1134 todo.append(proc.move_id.id)
1136 self.pool.get('stock.move').action_cancel(cr, uid, todo2)
1138 self.pool.get('stock.move').write(cr, uid, todo, {'state':'assigned'})
1139 self.write(cr, uid, ids, {'state':'cancel'})
1140 wf_service = netsvc.LocalService("workflow")
1142 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1145 def action_check_finnished(self, cr, uid, ids):
1146 return self.check_move_done(cr, uid, ids)
1148 def action_check(self, cr, uid, ids):
1150 for procurement in self.browse(cr, uid, ids):
1151 if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1152 self.action_done(cr, uid, [procurement.id])
1156 def action_ready(self, cr, uid, ids):
1157 res = self.write(cr, uid, ids, {'state':'ready'})
1160 def action_done(self, cr, uid, ids):
1161 for procurement in self.browse(cr, uid, ids):
1162 if procurement.move_id:
1163 if procurement.close_move and (procurement.move_id.state <> 'done'):
1164 self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1165 res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1166 wf_service = netsvc.LocalService("workflow")
1168 wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1171 def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1173 use_new_cursor: False or the dbname
1177 self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1178 self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1179 use_new_cursor=use_new_cursor, context=context)
1183 class stock_warehouse_orderpoint(osv.osv):
1184 _name = "stock.warehouse.orderpoint"
1185 _description = "Orderpoint minimum rule"
1187 'name': fields.char('Name', size=32, required=True),
1188 'active': fields.boolean('Active'),
1189 'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1190 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
1191 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
1192 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')], ondelete="cascade"),
1193 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1194 'product_min_qty': fields.float('Min Quantity', required=True,
1195 help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1196 "a procurement to bring the virtual stock to the Max Quantity."),
1197 'product_max_qty': fields.float('Max Quantity', required=True,
1198 help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1199 "a procurement to bring the virtual stock to the Max Quantity."),
1200 'qty_multiple': fields.integer('Qty Multiple', required=True,
1201 help="The procurement quantity will by rounded up to this multiple."),
1202 'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order', ondelete="set null")
1205 'active': lambda *a: 1,
1206 'logic': lambda *a: 'max',
1207 'qty_multiple': lambda *a: 1,
1208 'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1209 'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1212 _sql_constraints = [
1213 ( 'qty_multiple_check', 'CHECK( qty_multiple > 0 )', _('Qty Multiple must be greater than zero.')),
1216 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1218 w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1219 v = {'location_id':w.lot_stock_id.id}
1222 def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1224 prod=self.pool.get('product.product').browse(cr,uid,product_id)
1225 v = {'product_uom':prod.uom_id.id}
1228 def copy(self, cr, uid, id, default=None,context={}):
1232 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.warehouse.orderpoint') or '',
1234 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
1235 stock_warehouse_orderpoint()
1238 class StockMove(osv.osv):
1239 _inherit = 'stock.move'
1241 'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1243 def copy(self, cr, uid, id, default=None, context=None):
1244 default = default or {}
1245 default['procurements'] = []
1246 return super(StockMove, self).copy(cr, uid, id, default, context)
1248 def _action_explode(self, cr, uid, move, context={}):
1249 if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1250 bis = self.pool.get('mrp.bom').search(cr, uid, [
1251 ('product_id','=',move.product_id.id),
1252 ('bom_id','=',False),
1253 ('type','=','phantom')])
1255 factor = move.product_qty
1256 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1257 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1259 if move.state=='assigned':
1263 'picking_id': move.picking_id.id,
1264 'product_id': line['product_id'],
1265 'product_uom': line['product_uom'],
1266 'product_qty': line['product_qty'],
1267 'product_uos': line['product_uos'],
1268 'product_uos_qty': line['product_uos_qty'],
1270 'name': line['name'],
1271 'move_history_ids': [(6,0,[move.id])],
1272 'move_history_ids2': [(6,0,[])],
1275 mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1276 prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1277 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1278 'name': (move.picking_id.origin or ''),
1279 'origin': (move.picking_id.origin or ''),
1280 'date_planned': move.date_planned,
1281 'product_id': line['product_id'],
1282 'product_qty': line['product_qty'],
1283 'product_uom': line['product_uom'],
1284 'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1285 'product_uos': line['product_uos'],
1286 'location_id': move.location_id.id,
1287 'procure_method': prodobj.procure_method,
1290 wf_service = netsvc.LocalService("workflow")
1291 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1292 self.pool.get('stock.move').write(cr, uid, [move.id], {
1293 'location_id': move.location_dest_id.id, # src and dest locations identical to have correct inventory, dummy move
1294 'auto_validate': True,
1295 'picking_id': False,
1298 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1299 wf_service = netsvc.LocalService("workflow")
1300 wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1305 class StockPicking(osv.osv):
1306 _inherit = 'stock.picking'
1308 def test_finnished(self, cursor, user, ids):
1309 wf_service = netsvc.LocalService("workflow")
1310 res = super(StockPicking, self).test_finnished(cursor, user, ids)
1311 for picking in self.browse(cursor, user, ids):
1312 for move in picking.move_lines:
1313 if move.state == 'done' and move.procurements:
1314 for procurement in move.procurements:
1315 wf_service.trg_validate(user, 'mrp.procurement',
1316 procurement.id, 'button_check', cursor)
1320 # Explode picking by replacing phantom BoMs
1322 def action_explode(self, cr, uid, picks, *args):
1323 for move in self.pool.get('stock.move').browse(cr, uid, picks):
1324 self.pool.get('stock.move')._action_explode(cr, uid, move)
1329 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: