1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import datetime
23 from osv import osv, fields
24 import decimal_precision as dp
25 from tools.translate import _
31 #----------------------------------------------------------
33 #----------------------------------------------------------
34 # capacity_hour : capacity per hour. default: 1.0.
35 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
36 # unit_per_cycle : how many units are produced for one cycle
38 class mrp_workcenter(osv.osv):
39 _name = 'mrp.workcenter'
40 _description = 'Work Center'
41 _inherits = {'resource.resource':"resource_id"}
43 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
44 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this Work Center can do in parallel. If this Work Center represents a team of 5 workers, the capacity per cycle is 5."),
45 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
46 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
47 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
48 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
49 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
50 help="Complete this only if you want automatic analytic accounting entries on production orders."),
51 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
52 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
53 help="Complete this only if you want automatic analytic accounting entries on production orders."),
54 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
55 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
56 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
57 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to track easily your production costs in the analytic accounting."),
60 'capacity_per_cycle': 1.0,
61 'resource_type': 'material',
64 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
68 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
69 value = {'costs_hour': cost.standard_price}
70 return {'value': value}
75 class mrp_routing(osv.osv):
77 For specifying the routings of Work Centers.
80 _description = 'Routing'
82 'name': fields.char('Name', size=64, required=True),
83 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
84 'code': fields.char('Code', size=8),
86 'note': fields.text('Description'),
87 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
89 'location_id': fields.many2one('stock.location', 'Production Location',
90 help="Keep empty if you produce at the location where the finished products are needed." \
91 "Set a location if you produce at a fixed location. This can be a partner location " \
92 "if you subcontract the manufacturing operations."
94 'company_id': fields.many2one('res.company', 'Company'),
97 'active': lambda *a: 1,
98 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
102 class mrp_routing_workcenter(osv.osv):
104 Defines working cycles and hours of a Work Center using routings.
106 _name = 'mrp.routing.workcenter'
107 _description = 'Work Center Usage'
109 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
110 'name': fields.char('Name', size=64, required=True),
111 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
112 'cycle_nbr': fields.float('Number of Cycles', required=True,
113 help="Number of iterations this work center has to do in the specified operation of the routing."),
114 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for this Work Center to achieve the operation of the specified routing."),
115 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
116 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
117 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
118 'note': fields.text('Description'),
119 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
122 'cycle_nbr': lambda *a: 1.0,
123 'hour_nbr': lambda *a: 0.0,
125 mrp_routing_workcenter()
127 class mrp_bom(osv.osv):
129 Defines bills of material for a product.
132 _description = 'Bill of Material'
134 def _child_compute(self, cr, uid, ids, name, arg, context=None):
136 @param self: The object pointer
137 @param cr: The current row, from the database cursor,
138 @param uid: The current user ID for security checks
139 @param ids: List of selected IDs
140 @param name: Name of the field
141 @param arg: User defined argument
142 @param context: A standard dictionary for contextual values
143 @return: Dictionary of values
148 bom_obj = self.pool.get('mrp.bom')
149 bom_id = context and context.get('active_id', False) or False
150 cr.execute('select id from mrp_bom')
151 if all(bom_id != r[0] for r in cr.fetchall()):
154 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
155 for bom in self.browse(cr, uid, ids, context=context):
156 if (bom_parent) or (bom.id == bom_id):
157 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
162 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
163 if (bom.type=='phantom' or ok):
164 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
166 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
167 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
171 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
172 """ Sets particular method for the selected bom type.
173 @param field_name: Name of the field
174 @param arg: User defined argument
175 @return: Dictionary of values
177 res = dict.fromkeys(ids, False)
178 for line in self.browse(cr, uid, ids, context=context):
179 if line.type == 'phantom' and not line.bom_id:
182 if line.bom_lines or line.type == 'phantom':
184 if line.product_id.supply_method == 'produce':
185 if line.product_id.procure_method == 'make_to_stock':
186 res[line.id] = 'stock'
188 res[line.id] = 'order'
192 'name': fields.char('Name', size=64, required=True),
193 'code': fields.char('Reference', size=16),
194 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the bills of material without removing it."),
195 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
196 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
197 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
198 "If a Phantom BoM is used for a root product, it will be sold and shipped as a set of components, instead of being produced."),
199 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
200 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
201 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
202 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
203 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
204 'product_id': fields.many2one('product.product', 'Product', required=True),
205 'product_uos_qty': fields.float('Product UOS Qty'),
206 'product_uos': fields.many2one('product.uom', 'Product UOS', help="Product UOS (Unit of Sale) is the unit of measurement for the invoicing and promotion of stock."),
207 'product_qty': fields.float('Product Qty', required=True, digits_compute=dp.get_precision('Product UoM')),
208 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
209 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
210 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
211 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
212 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
213 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
214 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
215 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
216 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
217 'company_id': fields.many2one('res.company','Company',required=True),
220 'active': lambda *a: 1,
221 'product_efficiency': lambda *a: 1.0,
222 'product_qty': lambda *a: 1.0,
223 'product_rounding': lambda *a: 0.0,
224 'type': lambda *a: 'normal',
225 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
229 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
230 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
233 def _check_recursion(self, cr, uid, ids, context=None):
236 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
237 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
243 def _check_product(self, cr, uid, ids, context=None):
245 boms = self.browse(cr, uid, ids, context=context)
249 if bom.product_id.id in all_prod:
251 all_prod.append(bom.product_id.id)
252 lines = bom.bom_lines
254 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
256 return check_bom(boms)
259 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
260 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
263 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
264 """ Changes UoM and name if product_id changes.
265 @param name: Name of the field
266 @param product_id: Changed product_id
267 @return: Dictionary of changed values
270 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
271 return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}}
274 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
275 """ Finds BoM for particular product and product uom.
276 @param product_id: Selected product.
277 @param product_uom: Unit of measure of a product.
278 @param properties: List of related properties.
279 @return: False or BoM id.
281 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
282 ids = map(lambda x: x[0], cr.fetchall())
285 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
287 for prop_id in bom.property_ids:
288 if prop_id.id in properties:
290 if (prop > max_prop) or ((max_prop == 0) and not result):
295 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0, routing_id=False):
296 """ Finds Products and Work Centers for related BoM for manufacturing order.
297 @param bom: BoM of particular product.
298 @param factor: Factor of product UoM.
299 @param properties: A List of properties Ids.
300 @param addthis: If BoM found then True else False.
301 @param level: Depth level to find BoM lines starts from 10.
302 @return: result: List of dictionaries containing product details.
303 result2: List of dictionaries containing Work Center details.
305 routing_obj = self.pool.get('mrp.routing')
306 factor = factor / (bom.product_efficiency or 1.0)
307 factor = rounding(factor, bom.product_rounding)
308 if factor < bom.product_rounding:
309 factor = bom.product_rounding
313 if bom.type == 'phantom' and not bom.bom_lines:
314 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
317 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
318 result = result + res[0]
319 result2 = result2 + res[1]
324 if addthis and not bom.bom_lines:
327 'name': bom.product_id.name,
328 'product_id': bom.product_id.id,
329 'product_qty': bom.product_qty * factor,
330 'product_uom': bom.product_uom.id,
331 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
332 'product_uos': bom.product_uos and bom.product_uos.id or False,
334 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
336 for wc_use in routing.workcenter_lines:
337 wc = wc_use.workcenter_id
338 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
339 mult = (d + (m and 1.0 or 0.0))
340 cycle = mult * wc_use.cycle_nbr
342 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
343 'workcenter_id': wc.id,
344 'sequence': level+(wc_use.sequence or 0),
346 'hour': float(wc_use.hour_nbr*mult + ((wc.time_start or 0.0)+(wc.time_stop or 0.0)+cycle*(wc.time_cycle or 0.0)) * (wc.time_efficiency or 1.0)),
348 for bom2 in bom.bom_lines:
349 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
350 result = result + res[0]
351 result2 = result2 + res[1]
352 return result, result2
354 def copy_data(self, cr, uid, id, default=None, context=None):
357 bom_data = self.read(cr, uid, id, [], context=context)
358 default.update({'name': bom_data['name'] + ' ' + _('Copy'), 'bom_id':False})
359 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
363 class mrp_bom_revision(osv.osv):
364 _name = 'mrp.bom.revision'
365 _description = 'Bill of Material Revision'
367 'name': fields.char('Modification name', size=64, required=True),
368 'description': fields.text('Description'),
369 'date': fields.date('Modification Date'),
370 'indice': fields.char('Revision', size=16),
371 'last_indice': fields.char('last indice', size=64),
372 'author_id': fields.many2one('res.users', 'Author'),
373 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
377 'author_id': lambda x, y, z, c: z,
378 'date': lambda *a: time.strftime('%Y-%m-%d'),
387 return math.ceil(f / r) * r
389 class mrp_production(osv.osv):
391 Production Orders / Manufacturing Orders
393 _name = 'mrp.production'
394 _description = 'Manufacturing Order'
395 _date_name = 'date_planned'
397 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
398 """ Calculates total hours and total no. of cycles for a production order.
399 @param prop: Name of field.
401 @return: Dictionary of values.
404 for prod in self.browse(cr, uid, ids, context=context):
409 for wc in prod.workcenter_lines:
410 result[prod.id]['hour_total'] += wc.hour
411 result[prod.id]['cycle_total'] += wc.cycle
414 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context=None):
415 """ Finds production end date.
416 @param prop: Name of field.
418 @return: Dictionary of values.
421 for prod in self.browse(cr, uid, ids, context=context):
422 result[prod.id] = prod.date_planned
425 def _production_date(self, cr, uid, ids, prop, unknow_none, context=None):
426 """ Finds production planned date.
427 @param prop: Name of field.
429 @return: Dictionary of values.
432 for prod in self.browse(cr, uid, ids, context=context):
433 result[prod.id] = prod.date_planned[:10]
437 'name': fields.char('Reference', size=64, required=True),
438 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
439 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', select=True),
441 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft':[('readonly',False)]}),
442 'product_qty': fields.float('Product Qty', digits_compute=dp.get_precision('Product UoM'), required=True, states={'draft':[('readonly',False)]}, readonly=True),
443 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
444 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
445 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
447 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
448 readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will look for components."),
449 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
450 readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will stock the finished products."),
452 'date_planned_end': fields.function(_production_date_end, type='date', string='Scheduled End Date'),
453 'date_planned_date': fields.function(_production_date, type='date', string='Scheduled Date'),
454 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
455 'date_start': fields.datetime('Start Date', select=True),
456 'date_finished': fields.datetime('End Date', select=True),
458 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]}),
459 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]}, help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production plannification."),
460 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True, ondelete="restrict",
461 help="This is the Internal Picking List that brings the finished product to the production plan"),
462 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
463 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
464 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
465 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
466 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products', domain=[('state','in', ('done', 'cancel'))]),
467 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
468 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
469 'state': fields.selection([('draft','New'),('picking_except', 'Picking Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','Production Started'),('cancel','Cancelled'),('done','Done')],'State', readonly=True,
470 help='When the production order is created the state is set to \'Draft\'.\n If the order is confirmed the state is set to \'Waiting Goods\'.\n If any exceptions are there, the state is set to \'Picking Exception\'.\
471 \nIf the stock is available then the state is set to \'Ready to Produce\'.\n When the production gets started then the state is set to \'In Production\'.\n When the production is over, the state is set to \'Done\'.'),
472 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
473 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
474 'user_id':fields.many2one('res.users', 'Responsible'),
475 'company_id': fields.many2one('res.company','Company',required=True),
478 'priority': lambda *a: '1',
479 'state': lambda *a: 'draft',
480 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
481 'product_qty': lambda *a: 1.0,
482 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
483 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
486 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
488 _order = 'priority desc, date_planned asc';
490 def _check_qty(self, cr, uid, ids, context=None):
491 for order in self.browse(cr, uid, ids, context=context):
492 if order.product_qty <= 0:
497 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
500 def unlink(self, cr, uid, ids, context=None):
501 for production in self.browse(cr, uid, ids, context=context):
502 if production.state not in ('draft', 'cancel'):
503 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a manufacturing order in state \'%s\'') % production.state)
504 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
506 def copy(self, cr, uid, id, default=None, context=None):
510 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
513 'move_created_ids' : [],
514 'move_created_ids2' : [],
515 'product_lines' : [],
518 return super(mrp_production, self).copy(cr, uid, id, default, context)
520 def location_id_change(self, cr, uid, ids, src, dest, context=None):
521 """ Changes destination location if source location is changed.
522 @param src: Source location id.
523 @param dest: Destination location id.
524 @return: Dictionary of values.
529 return {'value': {'location_dest_id': src}}
532 def product_id_change(self, cr, uid, ids, product_id, context=None):
533 """ Finds UoM of changed product.
534 @param product_id: Id of changed product.
535 @return: Dictionary of values.
539 'product_uom': False,
543 bom_obj = self.pool.get('mrp.bom')
544 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
545 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
548 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
549 routing_id = bom_point.routing_id.id or False
551 'product_uom': product.uom_id and product.uom_id.id or False,
553 'routing_id': routing_id
555 return {'value': result}
557 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
558 """ Finds routing for changed BoM.
559 @param product: Id of product.
560 @return: Dictionary of values.
566 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
567 routing_id = bom_point.routing_id.id or False
569 'routing_id': routing_id
571 return {'value': result}
573 def action_picking_except(self, cr, uid, ids):
574 """ Changes the state to Exception.
577 self.write(cr, uid, ids, {'state': 'picking_except'})
580 def action_compute(self, cr, uid, ids, properties=[], context=None):
581 """ Computes bills of material of a product.
582 @param properties: List containing dictionaries of properties.
583 @return: No. of products.
586 bom_obj = self.pool.get('mrp.bom')
587 uom_obj = self.pool.get('product.uom')
588 prod_line_obj = self.pool.get('mrp.production.product.line')
589 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
590 for production in self.browse(cr, uid, ids):
591 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
592 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
593 bom_point = production.bom_id
594 bom_id = production.bom_id.id
596 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
598 bom_point = bom_obj.browse(cr, uid, bom_id)
599 routing_id = bom_point.routing_id.id or False
600 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
603 raise osv.except_osv(_('Error'), _("Couldn't find a bill of material for this product."))
604 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
605 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
609 line['production_id'] = production.id
610 prod_line_obj.create(cr, uid, line)
611 for line in results2:
612 line['production_id'] = production.id
613 workcenter_line_obj.create(cr, uid, line)
616 def action_cancel(self, cr, uid, ids, context=None):
617 """ Cancels the production order and related stock moves.
622 move_obj = self.pool.get('stock.move')
623 for production in self.browse(cr, uid, ids, context=context):
624 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
625 raise osv.except_osv(
626 _('Could not cancel manufacturing order !'),
627 _('You must first cancel related internal picking attached to this manufacturing order.'))
628 if production.move_created_ids:
629 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
630 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
631 self.write(cr, uid, ids, {'state': 'cancel'})
634 def action_ready(self, cr, uid, ids):
635 """ Changes the production state to Ready and location id of stock move.
638 move_obj = self.pool.get('stock.move')
639 self.write(cr, uid, ids, {'state': 'ready'})
641 for (production_id,name) in self.name_get(cr, uid, ids):
642 production = self.browse(cr, uid, production_id)
643 if production.move_prod_id:
644 move_obj.write(cr, uid, [production.move_prod_id.id],
645 {'location_id': production.location_dest_id.id})
647 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
648 self.log(cr, uid, production_id, message)
651 def action_production_end(self, cr, uid, ids):
652 """ Changes production state to Finish and writes finished date.
655 for production in self.browse(cr, uid, ids):
656 self._costs_generate(cr, uid, production)
657 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
659 def test_production_done(self, cr, uid, ids):
660 """ Tests whether production is done or not.
661 @return: True or False
664 for production in self.browse(cr, uid, ids):
665 if production.move_lines:
668 if production.move_created_ids:
672 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
673 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
674 it's always equal to the quantity encoded in the production order or the production wizard, but if the
675 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
677 :param production_id: ID of the mrp.order
678 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
679 :return: The factor to apply to the quantity that we should produce for the given production order.
683 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
684 """ To produce final product based on production mode (consume/consume&produce).
685 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
686 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
687 and stock move lines of final product will be also done/produced.
688 @param production_id: the ID of mrp.production object
689 @param production_qty: specify qty to produce
690 @param production_mode: specify production mode (consume/consume&produce).
693 stock_mov_obj = self.pool.get('stock.move')
694 production = self.browse(cr, uid, production_id, context=context)
697 for produced_product in production.move_created_ids2:
698 if (produced_product.scrapped) or (produced_product.product_id.id <> production.product_id.id):
700 produced_qty += produced_product.product_qty
702 if production_mode in ['consume','consume_produce']:
705 # Calculate already consumed qtys
706 for consumed in production.move_lines2:
707 if consumed.scrapped:
709 if not consumed_data.get(consumed.product_id.id, False):
710 consumed_data[consumed.product_id.id] = 0
711 consumed_data[consumed.product_id.id] += consumed.product_qty
713 # Find product qty to be consumed and consume it
714 for scheduled in production.product_lines:
716 # total qty of consumed product we need after this consumption
717 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
719 # qty available for consume and produce
720 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
723 # there will be nothing to consume for this raw material
726 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
728 # qtys we have to consume
729 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
732 # if qtys we have to consume is more than qtys available to consume
733 prod_name = scheduled.product_id.name_get()[0][1]
734 raise osv.except_osv(_('Warning!'), _('You are going to consume total %s quantities of "%s".\nBut you can consume upto total %s quantities.' % (qty, prod_name, qty_avail)))
736 # we already have more qtys consumed than we need
739 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
741 if production_mode == 'consume_produce':
742 # To produce remaining qty of final product
743 #vals = {'state':'confirmed'}
744 #final_product_todo = [x.id for x in production.move_created_ids]
745 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
746 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
747 produced_products = {}
748 for produced_product in production.move_created_ids2:
749 if produced_product.scrapped:
751 if not produced_products.get(produced_product.product_id.id, False):
752 produced_products[produced_product.product_id.id] = 0
753 produced_products[produced_product.product_id.id] += produced_product.product_qty
755 for produce_product in production.move_created_ids:
756 produced_qty = produced_products.get(produce_product.product_id.id, 0)
757 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
758 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
760 if rest_qty < production_qty:
761 prod_name = produce_product.product_id.name_get()[0][1]
762 raise osv.except_osv(_('Warning!'), _('You are going to produce total %s quantities of "%s".\nBut you can produce upto total %s quantities.' % (production_qty, prod_name, rest_qty)))
764 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
766 for raw_product in production.move_lines2:
768 parent_move_ids = [x.id for x in raw_product.move_history_ids]
769 for final_product in production.move_created_ids2:
770 if final_product.id not in parent_move_ids:
771 new_parent_ids.append(final_product.id)
772 for new_parent_id in new_parent_ids:
773 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
775 wf_service = netsvc.LocalService("workflow")
776 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
779 def _costs_generate(self, cr, uid, production):
780 """ Calculates total costs at the end of the production.
781 @param production: Id of production order.
782 @return: Calculated amount.
785 analytic_line_obj = self.pool.get('account.analytic.line')
786 for wc_line in production.workcenter_lines:
787 wc = wc_line.workcenter_id
788 if wc.costs_journal_id and wc.costs_general_account_id:
789 value = wc_line.hour * wc.costs_hour
790 account = wc.costs_hour_account_id.id
791 if value and account:
793 analytic_line_obj.create(cr, uid, {
794 'name': wc_line.name + ' (H)',
796 'account_id': account,
797 'general_account_id': wc.costs_general_account_id.id,
798 'journal_id': wc.costs_journal_id.id,
800 'product_id': wc.product_id.id,
801 'unit_amount': wc_line.hour,
802 'product_uom_id': wc.product_id.uom_id.id
804 if wc.costs_journal_id and wc.costs_general_account_id:
805 value = wc_line.cycle * wc.costs_cycle
806 account = wc.costs_cycle_account_id.id
807 if value and account:
809 analytic_line_obj.create(cr, uid, {
810 'name': wc_line.name+' (C)',
812 'account_id': account,
813 'general_account_id': wc.costs_general_account_id.id,
814 'journal_id': wc.costs_journal_id.id,
816 'product_id': wc.product_id.id,
817 'unit_amount': wc_line.cycle,
818 'product_uom_id': wc.product_id.uom_id.id
822 def action_in_production(self, cr, uid, ids):
823 """ Changes state to In Production and writes starting date.
826 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
829 def test_if_product(self, cr, uid, ids):
831 @return: True or False
834 for production in self.browse(cr, uid, ids):
835 if not production.product_lines:
836 if not self.action_compute(cr, uid, [production.id]):
840 def _get_auto_picking(self, cr, uid, production):
843 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
844 wf_service = netsvc.LocalService("workflow")
845 procurement_order = self.pool.get('procurement.order')
846 production = production_line.production_id
847 location_id = production.location_src_id.id
848 date_planned = production.date_planned
849 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
850 procurement_id = procurement_order.create(cr, uid, {
851 'name': procurement_name,
852 'origin': procurement_name,
853 'date_planned': date_planned,
854 'product_id': production_line.product_id.id,
855 'product_qty': production_line.product_qty,
856 'product_uom': production_line.product_uom.id,
857 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
858 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
859 'location_id': location_id,
860 'procure_method': production_line.product_id.procure_method,
861 'move_id': shipment_move_id,
862 'company_id': production.company_id.id,
864 wf_service.trg_validate(uid, procurement_order._name, procurement_id, 'button_confirm', cr)
865 return procurement_id
867 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
868 stock_move = self.pool.get('stock.move')
869 production = production_line.production_id
870 date_planned = production.date_planned
871 # Internal shipment is created for Stockable and Consumer Products
872 if production_line.product_id.type not in ('product', 'consu'):
874 move_name = _('PROD: %s') % production.name
875 source_location_id = production.location_src_id.id
876 if not destination_location_id:
877 destination_location_id = source_location_id
878 return stock_move.create(cr, uid, {
880 'picking_id': shipment_id,
881 'product_id': production_line.product_id.id,
882 'product_qty': production_line.product_qty,
883 'product_uom': production_line.product_uom.id,
884 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
885 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
886 'date': date_planned,
887 'move_dest_id': parent_move_id,
888 'location_id': source_location_id,
889 'location_dest_id': destination_location_id,
891 'company_id': production.company_id.id,
894 def _make_production_internal_shipment(self, cr, uid, production, context=None):
895 ir_sequence = self.pool.get('ir.sequence')
896 stock_picking = self.pool.get('stock.picking')
898 pick_type = 'internal'
901 # Take routing address as a Shipment Address.
902 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
903 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
904 routing_loc = production.bom_id.routing_id.location_id
905 if routing_loc.usage <> 'internal':
907 address_id = routing_loc.address_id and routing_loc.address_id.id or False
909 # Take next Sequence number of shipment base on type
910 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
912 picking_id = stock_picking.create(cr, uid, {
914 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
918 'address_id': address_id,
919 'auto_picking': self._get_auto_picking(cr, uid, production),
920 'company_id': production.company_id.id,
922 production.write({'picking_id': picking_id}, context=context)
925 def _make_production_produce_line(self, cr, uid, production, context=None):
926 stock_move = self.pool.get('stock.move')
927 source_location_id = production.product_id.product_tmpl_id.property_stock_production.id
928 destination_location_id = production.location_dest_id.id
929 move_name = _('PROD: %s') + production.name
932 'date': production.date_planned,
933 'product_id': production.product_id.id,
934 'product_qty': production.product_qty,
935 'product_uom': production.product_uom.id,
936 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
937 'product_uos': production.product_uos and production.product_uos.id or False,
938 'location_id': source_location_id,
939 'location_dest_id': destination_location_id,
940 'move_dest_id': production.move_prod_id.id,
942 'company_id': production.company_id.id,
944 move_id = stock_move.create(cr, uid, data, context=context)
945 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
948 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
949 stock_move = self.pool.get('stock.move')
950 production = production_line.production_id
951 # Internal shipment is created for Stockable and Consumer Products
952 if production_line.product_id.type not in ('product', 'consu'):
954 move_name = _('PROD: %s') % production.name
955 destination_location_id = production.product_id.product_tmpl_id.property_stock_production.id
956 if not source_location_id:
957 source_location_id = production.location_src_id.id
958 move_id = stock_move.create(cr, uid, {
960 'date': production.date_planned,
961 'product_id': production_line.product_id.id,
962 'product_qty': production_line.product_qty,
963 'product_uom': production_line.product_uom.id,
964 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
965 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
966 'location_id': source_location_id,
967 'location_dest_id': destination_location_id,
968 'move_dest_id': parent_move_id,
970 'company_id': production.company_id.id,
972 production.write({'move_lines': [(4, move_id)]}, context=context)
975 def action_confirm(self, cr, uid, ids, context=None):
976 """ Confirms production order.
977 @return: Newly generated Shipment Id.
980 wf_service = netsvc.LocalService("workflow")
981 uncompute_ids = filter(lambda x:x, [not x.product_lines and x.id or False for x in self.browse(cr, uid, ids, context=context)])
982 self.action_compute(cr, uid, uncompute_ids, context=context)
983 for production in self.browse(cr, uid, ids, context=context):
984 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
985 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
987 # Take routing location as a Source Location.
988 source_location_id = production.location_src_id.id
989 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
990 source_location_id = production.bom_id.routing_id.location_id.id
992 for line in production.product_lines:
993 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
994 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
995 destination_location_id=source_location_id, context=context)
996 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
998 wf_service.trg_validate(uid, 'stock.picking', shipment_id, 'button_confirm', cr)
999 production.write({'state':'confirmed'}, context=context)
1000 message = _("Manufacturing order '%s' is scheduled for the %s.") % (
1002 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
1004 self.log(cr, uid, production.id, message)
1007 def force_production(self, cr, uid, ids, *args):
1008 """ Assigns products.
1009 @param *args: Arguments
1012 pick_obj = self.pool.get('stock.picking')
1013 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1018 class mrp_production_workcenter_line(osv.osv):
1019 _name = 'mrp.production.workcenter.line'
1020 _description = 'Work Order'
1024 'name': fields.char('Work Order', size=64, required=True),
1025 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1026 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
1027 'hour': fields.float('Nbr of hours', digits=(16,2)),
1028 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1029 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
1032 'sequence': lambda *a: 1,
1033 'hour': lambda *a: 0,
1034 'cycle': lambda *a: 0,
1036 mrp_production_workcenter_line()
1038 class mrp_production_product_line(osv.osv):
1039 _name = 'mrp.production.product.line'
1040 _description = 'Production Scheduled Product'
1042 'name': fields.char('Name', size=64, required=True),
1043 'product_id': fields.many2one('product.product', 'Product', required=True),
1044 'product_qty': fields.float('Product Qty', digits_compute=dp.get_precision('Product UoM'), required=True),
1045 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1046 'product_uos_qty': fields.float('Product UOS Qty'),
1047 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1048 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1050 mrp_production_product_line()
1052 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: