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 from tools.translate import _
30 #----------------------------------------------------------
32 #----------------------------------------------------------
33 # capacity_hour : capacity per hour. default: 1.0.
34 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
35 # unit_per_cycle : how many units are produced for one cycle
37 class mrp_workcenter(osv.osv):
38 _name = 'mrp.workcenter'
39 _description = 'Work Center'
40 _inherits = {'resource.resource':"resource_id"}
42 'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
43 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this workcenter can do in parallel. If this workcenter represents a team of 5 workers, the capacity per cycle is 5."),
44 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
45 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
46 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
47 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Workcenter per hour."),
48 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
49 help="Complete this only if you want automatic analytic accounting entries on production orders."),
50 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Workcenter per cycle."),
51 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
52 help="Complete this only if you want automatic analytic accounting entries on production orders."),
53 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
54 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
55 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
56 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to track easily your production costs in the analytic accounting."),
59 'capacity_per_cycle': 1.0,
60 'resource_type': 'material',
63 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
69 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
70 value = {'costs_hour' :cost.standard_price}
71 return {'value': value}
76 class mrp_routing(osv.osv):
78 For specifying the routings of workcenters.
81 _description = 'Routing'
83 'name': fields.char('Name', size=64, required=True),
84 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
85 'code': fields.char('Code', size=8),
87 'note': fields.text('Description'),
88 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
90 'location_id': fields.many2one('stock.location', 'Production Location',
91 help="Keep empty if you produce at the location where the finished products are needed." \
92 "Set a location if you produce at a fixed location. This can be a partner location " \
93 "if you subcontract the manufacturing operations."
95 'company_id': fields.many2one('res.company', 'Company'),
98 'active': lambda *a: 1,
99 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
103 class mrp_routing_workcenter(osv.osv):
105 Defines working cycles and hours of a workcenter using routings.
107 _name = 'mrp.routing.workcenter'
108 _description = 'Workcenter Usage'
110 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
111 'name': fields.char('Name', size=64, required=True),
112 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing workcenters."),
113 'cycle_nbr': fields.float('Number of Cycles', required=True,
114 help="Number of iterations this workcenter has to do in the specified operation of the routing."),
115 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for this workcenter to achieve the operation of the specified routing."),
116 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
117 help="Routing indicates all the workcenters used, for how long and/or cycles." \
118 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
119 'note': fields.text('Description'),
120 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company'),
123 'cycle_nbr': lambda *a: 1.0,
124 'hour_nbr': lambda *a: 0.0,
126 mrp_routing_workcenter()
128 class mrp_bom(osv.osv):
130 Defines bills of material for a product.
133 _description = 'Bill of Material'
135 def _child_compute(self, cr, uid, ids, name, arg, context={}):
137 @param self: The object pointer
138 @param cr: The current row, from the database cursor,
139 @param uid: The current user ID for security checks
140 @param ids: List of selected IDs
141 @param name: Name of the field
142 @param arg: User defined argument
143 @param context: A standard dictionary for contextual values
144 @return: Dictionary of values
147 bom_obj = self.pool.get('mrp.bom')
148 bom_id = context and context.get('active_id', False) or False
149 cr.execute('select id from mrp_bom')
150 if all(bom_id != r[0] for r in cr.fetchall()):
153 bom_parent = bom_obj.browse(cr, uid, bom_id)
154 for bom in self.browse(cr, uid, ids, context=context):
155 if (bom_parent) or (bom.id == bom_id):
156 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
161 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
162 if (bom.type=='phantom' or ok):
163 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
165 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
166 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
170 def _compute_type(self, cr, uid, ids, field_name, arg, context):
171 """ Sets particular method for the selected bom type.
172 @param field_name: Name of the field
173 @param arg: User defined argument
174 @return: Dictionary of values
176 res = dict(map(lambda x: (x,''), ids))
177 for line in self.browse(cr, uid, ids):
178 if line.type == 'phantom' and not line.bom_id:
181 if line.bom_lines or line.type == 'phantom':
183 if line.product_id.supply_method == 'produce':
184 if line.product_id.procure_method == 'make_to_stock':
185 res[line.id] = 'stock'
187 res[line.id] = 'order'
191 'name': fields.char('Name', size=64, required=True),
192 'code': fields.char('Reference', size=16),
193 '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."),
194 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
195 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
196 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
197 "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."),
198 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
199 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
200 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
201 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
202 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
203 'product_id': fields.many2one('product.product', 'Product', required=True),
204 'product_uos_qty': fields.float('Product UOS Qty'),
205 '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."),
206 'product_qty': fields.float('Product Qty', required=True),
207 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
208 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
209 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
210 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
211 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
212 '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 future loads on workcenters based on production planning."),
213 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
214 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
215 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
216 'company_id': fields.many2one('res.company','Company',required=True),
219 'active': lambda *a: 1,
220 'product_efficiency': lambda *a: 1.0,
221 'product_qty': lambda *a: 1.0,
222 'product_rounding': lambda *a: 0.0,
223 'type': lambda *a: 'normal',
224 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
228 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
229 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
232 def _check_recursion(self, cr, uid, ids):
235 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
236 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
242 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
246 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
247 """ Changes UoM and name if product_id changes.
248 @param name: Name of the field
249 @param product_id: Changed product_id
250 @return: Dictionary of changed values
253 prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
254 v = {'product_uom': prod.uom_id.id}
256 v['name'] = prod.name
260 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
261 """ Finds BoM for particular product and product uom.
262 @param product_id: Selected product.
263 @param product_uom: Unit of measure of a product.
264 @param properties: List of related properties.
265 @return: False or BoM id.
267 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
268 ids = map(lambda x: x[0], cr.fetchall())
271 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
273 for prop_id in bom.property_ids:
274 if prop_id.id in properties:
276 if (prop > max_prop) or ((max_prop == 0) and not result):
281 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
282 """ Finds Products and Workcenters for related BoM for manufacturing order.
283 @param bom: BoM of particular product.
284 @param factor: Factor of product UoM.
285 @param properties: A List of properties Ids.
286 @param addthis: If BoM found then True else False.
287 @param level: Depth level to find BoM lines starts from 10.
288 @return: result: List of dictionaries containing product details.
289 result2: List of dictionaries containing workcenter details.
291 factor = factor / (bom.product_efficiency or 1.0)
292 factor = rounding(factor, bom.product_rounding)
293 if factor < bom.product_rounding:
294 factor = bom.product_rounding
298 if bom.type == 'phantom' and not bom.bom_lines:
299 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
301 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
302 result = result + res[0]
303 result2 = result2 + res[1]
308 if addthis and not bom.bom_lines:
311 'name': bom.product_id.name,
312 'product_id': bom.product_id.id,
313 'product_qty': bom.product_qty * factor,
314 'product_uom': bom.product_uom.id,
315 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
316 'product_uos': bom.product_uos and bom.product_uos.id or False,
319 for wc_use in bom.routing_id.workcenter_lines:
320 wc = wc_use.workcenter_id
321 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
322 mult = (d + (m and 1.0 or 0.0))
323 cycle = mult * wc_use.cycle_nbr
325 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
326 'workcenter_id': wc.id,
327 'sequence': level+(wc_use.sequence or 0),
329 '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)),
331 for bom2 in bom.bom_lines:
332 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
333 result = result + res[0]
334 result2 = result2 + res[1]
335 return result, result2
337 def copy_data(self, cr, uid, id, default=None, context=None):
342 bom_data = self.read(cr, uid, id, [], context=context)
343 default.update({'name': bom_data['name'] + _(' (copy)')})
344 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
348 class mrp_bom_revision(osv.osv):
349 _name = 'mrp.bom.revision'
350 _description = 'Bill of Material Revision'
352 'name': fields.char('Modification name', size=64, required=True),
353 'description': fields.text('Description'),
354 'date': fields.date('Modification Date'),
355 'indice': fields.char('Revision', size=16),
356 'last_indice': fields.char('last indice', size=64),
357 'author_id': fields.many2one('res.users', 'Author'),
358 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
362 'author_id': lambda x, y, z, c: z,
363 'date': lambda *a: time.strftime('%Y-%m-%d'),
371 return round(f / r) * r
373 class mrp_production(osv.osv):
375 Production Orders / Manufacturing Orders
377 _name = 'mrp.production'
378 _description = 'Manufacturing Order'
379 _date_name = 'date_planned'
381 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
382 """ Calculates total hours and total no. of cycles for a production order.
383 @param prop: Name of field.
385 @return: Dictionary of values.
388 for prod in self.browse(cr, uid, ids, context=context):
393 for wc in prod.workcenter_lines:
394 result[prod.id]['hour_total'] += wc.hour
395 result[prod.id]['cycle_total'] += wc.cycle
398 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
399 """ Finds production end date.
400 @param prop: Name of field.
402 @return: Dictionary of values.
405 for prod in self.browse(cr, uid, ids, context=context):
406 result[prod.id] = prod.date_planned
409 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
410 """ Finds production planned date.
411 @param prop: Name of field.
413 @return: Dictionary of values.
416 for prod in self.browse(cr, uid, ids, context=context):
417 result[prod.id] = prod.date_planned[:10]
421 'name': fields.char('Reference', size=64, required=True),
422 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
423 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
425 'product_id': fields.many2one('product.product', 'Product', required=True, ),
426 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
427 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
428 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
429 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
431 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
432 help="Location where the system will look for components."),
433 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
434 help="Location where the system will stock the finished products."),
436 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
437 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
438 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
439 'date_start': fields.datetime('Start Date'),
440 'date_finished': fields.datetime('End Date'),
442 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
443 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', 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 future loads on workcenters based on production plannification."),
445 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
446 help="This is the internal picking list that brings the finished product to the production plan"),
447 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
448 '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)]}),
449 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
450 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
451 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
452 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
453 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
454 'state': fields.selection([('draft','Draft'),('picking_except', 'Picking Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','In Production'),('cancel','Cancelled'),('done','Done')],'State', readonly=True,
455 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\'.\
456 \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\'.'),
457 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
458 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
459 'company_id': fields.many2one('res.company','Company',required=True),
462 'priority': lambda *a: '1',
463 'state': lambda *a: 'draft',
464 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
465 'product_qty': lambda *a: 1.0,
466 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
467 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
469 _order = 'priority desc, date_planned asc';
471 def _check_qty(self, cr, uid, ids):
472 orders = self.browse(cr, uid, ids)
474 if order.product_qty <= 0:
479 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
482 def unlink(self, cr, uid, ids, context=None):
483 productions = self.read(cr, uid, ids, ['state'])
485 for s in productions:
486 if s['state'] in ['draft','cancel']:
487 unlink_ids.append(s['id'])
489 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!') % s['state'])
490 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
492 def copy(self, cr, uid, id, default=None, context=None):
496 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
499 'move_created_ids' : [],
500 'move_created_ids2' : [],
501 'product_lines' : [],
504 return super(mrp_production, self).copy(cr, uid, id, default, context)
506 def location_id_change(self, cr, uid, ids, src, dest, context={}):
507 """ Changes destination location if source location is changed.
508 @param src: Source location id.
509 @param dest: Destination location id.
510 @return: Dictionary of values.
515 return {'value': {'location_dest_id': src}}
518 def product_id_change(self, cr, uid, ids, product_id, context=None):
519 """ Finds UoM of changed product.
520 @param product_id: Id of changed product.
521 @return: Dictionary of values.
525 'product_uom': False,
529 bom_obj = self.pool.get('mrp.bom')
530 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
531 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
534 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
535 routing_id = bom_point.routing_id.id or False
537 'product_uom': product.uom_id and product.uom_id.id or False,
539 'routing_id': routing_id
541 return {'value': result}
543 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
544 """ Finds routing for changed BoM.
545 @param product: Id of product.
546 @return: Dictionary of values.
552 bom_pool = self.pool.get('mrp.bom')
553 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
554 routing_id = bom_point.routing_id.id or False
556 'routing_id': routing_id
558 return {'value': result}
560 def action_picking_except(self, cr, uid, ids):
561 """ Changes the state to Exception.
564 self.write(cr, uid, ids, {'state': 'picking_except'})
567 def action_compute(self, cr, uid, ids, properties=[]):
568 """ Computes bills of material of a product.
569 @param properties: List containing dictionaries of properties.
570 @return: No. of products.
573 bom_obj = self.pool.get('mrp.bom')
574 prod_line_obj = self.pool.get('mrp.production.product.line')
575 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
576 for production in self.browse(cr, uid, ids):
577 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
578 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
579 bom_point = production.bom_id
580 bom_id = production.bom_id.id
582 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
584 bom_point = bom_obj.browse(cr, uid, bom_id)
585 routing_id = bom_point.routing_id.id or False
586 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
589 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
591 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
592 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
596 line['production_id'] = production.id
597 prod_line_obj.create(cr, uid, line)
598 for line in results2:
599 line['production_id'] = production.id
600 workcenter_line_obj.create(cr, uid, line)
603 def action_cancel(self, cr, uid, ids):
604 """ Cancels the production order and related stock moves.
607 move_obj = self.pool.get('stock.move')
608 for production in self.browse(cr, uid, ids):
609 if production.move_created_ids:
610 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
611 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
612 self.write(cr, uid, ids, {'state': 'cancel'})
615 def action_ready(self, cr, uid, ids):
616 """ Changes the production state to Ready and location id of stock move.
619 move_obj = self.pool.get('stock.move')
620 self.write(cr, uid, ids, {'state': 'ready'})
622 for (production_id,name) in self.name_get(cr, uid, ids):
623 production = self.browse(cr, uid, production_id)
624 if production.move_prod_id:
625 move_obj.write(cr, uid, [production.move_prod_id.id],
626 {'location_id': production.location_dest_id.id})
628 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
629 self.log(cr, uid, production_id, message)
632 def action_production_end(self, cr, uid, ids):
633 """ Changes production state to Finish and writes finished date.
636 for production in self.browse(cr, uid, ids):
637 self._costs_generate(cr, uid, production)
638 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
640 def test_production_done(self, cr, uid, ids):
641 """ Tests whether production is done or not.
642 @return: True or False
645 for production in self.browse(cr, uid, ids):
646 if production.move_lines:
649 if production.move_created_ids:
653 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
654 """ To produce final product based on production mode (consume/consume&produce).
655 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
656 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
657 and stock move lines of final product will be also done/produced.
658 @param production_id: the ID of mrp.production object
659 @param production_qty: specify qty to produce
660 @param production_mode: specify production mode (consume/consume&produce).
664 stock_mov_obj = self.pool.get('stock.move')
665 production = self.browse(cr, uid, production_id)
667 final_product_todo = []
670 if production_mode == 'consume_produce':
671 produced_qty = production_qty
673 for produced_product in production.move_created_ids2:
674 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
676 produced_qty += produced_product.product_qty
678 if production_mode in ['consume','consume_produce']:
679 consumed_products = {}
681 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
683 for consumed_product in production.move_lines2:
684 consumed = consumed_product.product_qty
685 if consumed_product.scrapped:
687 if not consumed_products.get(consumed_product.product_id.id, False):
688 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
689 check[consumed_product.product_id.id] = 0
690 for f in production.product_lines:
691 if f.product_id.id == consumed_product.product_id.id:
692 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
693 check[consumed_product.product_id.id] += consumed_product.product_qty
694 consumed = check[consumed_product.product_id.id]
695 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
696 consumed_products[consumed_product.product_id.id] = rest_consumed
698 for raw_product in production.move_lines:
699 for f in production.product_lines:
700 if f.product_id.id == raw_product.product_id.id:
701 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
702 if consumed_qty == 0:
703 consumed_qty = production_qty * f.product_qty / production.product_qty
705 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
707 if production_mode == 'consume_produce':
708 # To produce remaining qty of final product
709 vals = {'state':'confirmed'}
710 final_product_todo = [x.id for x in production.move_created_ids]
711 stock_mov_obj.write(cr, uid, final_product_todo, vals)
712 produced_products = {}
713 for produced_product in production.move_created_ids2:
714 if produced_product.scrapped:
716 if not produced_products.get(produced_product.product_id.id, False):
717 produced_products[produced_product.product_id.id] = 0
718 produced_products[produced_product.product_id.id] += produced_product.product_qty
720 for produce_product in production.move_created_ids:
721 produced_qty = produced_products.get(produce_product.product_id.id, 0)
722 rest_qty = production.product_qty - produced_qty
723 if rest_qty <= production_qty:
724 production_qty = rest_qty
726 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
728 for raw_product in production.move_lines2:
730 parent_move_ids = [x.id for x in raw_product.move_history_ids]
731 for final_product in production.move_created_ids2:
732 if final_product.id not in parent_move_ids:
733 new_parent_ids.append(final_product.id)
734 for new_parent_id in new_parent_ids:
735 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
737 wf_service = netsvc.LocalService("workflow")
738 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
741 def _costs_generate(self, cr, uid, production):
742 """ Calculates total costs at the end of the production.
743 @param production: Id of production order.
744 @return: Calculated amount.
747 analytic_line_obj = self.pool.get('account.analytic.line')
748 for wc_line in production.workcenter_lines:
749 wc = wc_line.workcenter_id
750 if wc.costs_journal_id and wc.costs_general_account_id:
751 value = wc_line.hour * wc.costs_hour
752 account = wc.costs_hour_account_id.id
753 if value and account:
755 analytic_line_obj.create(cr, uid, {
756 'name': wc_line.name + ' (H)',
758 'account_id': account,
759 'general_account_id': wc.costs_general_account_id.id,
760 'journal_id': wc.costs_journal_id.id,
762 'product_id': wc.product_id.id,
763 'unit_amount': wc_line.hour,
764 'product_uom_id': wc.product_id.uom_id.id
766 if wc.costs_journal_id and wc.costs_general_account_id:
767 value = wc_line.cycle * wc.costs_cycle
768 account = wc.costs_cycle_account_id.id
769 if value and account:
771 analytic_line_obj.create(cr, uid, {
772 'name': wc_line.name+' (C)',
774 'account_id': account,
775 'general_account_id': wc.costs_general_account_id.id,
776 'journal_id': wc.costs_journal_id.id,
778 'product_id': wc.product_id.id,
779 'unit_amount': wc_line.cycle,
780 'product_uom_id': wc.product_id.uom_id.id
784 def action_in_production(self, cr, uid, ids):
785 """ Changes state to In Production and writes starting date.
788 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
791 def test_if_product(self, cr, uid, ids):
793 @return: True or False
796 for production in self.browse(cr, uid, ids):
797 if not production.product_lines:
798 if not self.action_compute(cr, uid, [production.id]):
802 def _get_auto_picking(self, cr, uid, production):
805 def action_confirm(self, cr, uid, ids):
806 """ Confirms production order.
807 @return: Newly generated picking Id.
811 seq_obj = self.pool.get('ir.sequence')
812 pick_obj = self.pool.get('stock.picking')
813 move_obj = self.pool.get('stock.move')
814 proc_obj = self.pool.get('procurement.order')
815 wf_service = netsvc.LocalService("workflow")
816 for production in self.browse(cr, uid, ids):
817 if not production.product_lines:
818 self.action_compute(cr, uid, [production.id])
819 production = self.browse(cr, uid, [production.id])[0]
821 pick_type = 'internal'
823 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
824 routing_loc = production.bom_id.routing_id.location_id
825 if routing_loc.usage <> 'internal':
827 address_id = routing_loc.address_id and routing_loc.address_id.id or False
828 routing_loc = routing_loc.id
829 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
830 picking_id = pick_obj.create(cr, uid, {
832 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
836 'address_id': address_id,
837 'auto_picking': self._get_auto_picking(cr, uid, production),
838 'company_id': production.company_id.id,
841 source = production.product_id.product_tmpl_id.property_stock_production.id
843 'name':'PROD:' + production.name,
844 'date': production.date_planned,
845 'product_id': production.product_id.id,
846 'product_qty': production.product_qty,
847 'product_uom': production.product_uom.id,
848 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
849 'product_uos': production.product_uos and production.product_uos.id or False,
850 'location_id': source,
851 'location_dest_id': production.location_dest_id.id,
852 'move_dest_id': production.move_prod_id.id,
854 'company_id': production.company_id.id,
856 res_final_id = move_obj.create(cr, uid, data)
858 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
860 for line in production.product_lines:
862 newdate = production.date_planned
863 if line.product_id.type in ('product', 'consu'):
864 res_dest_id = move_obj.create(cr, uid, {
865 'name':'PROD:' + production.name,
866 'date': production.date_planned,
867 'product_id': line.product_id.id,
868 'product_qty': line.product_qty,
869 'product_uom': line.product_uom.id,
870 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
871 'product_uos': line.product_uos and line.product_uos.id or False,
872 'location_id': routing_loc or production.location_src_id.id,
873 'location_dest_id': source,
874 'move_dest_id': res_final_id,
876 'company_id': production.company_id.id,
878 moves.append(res_dest_id)
879 move_id = move_obj.create(cr, uid, {
880 'name':'PROD:' + production.name,
881 'picking_id':picking_id,
882 'product_id': line.product_id.id,
883 'product_qty': line.product_qty,
884 'product_uom': line.product_uom.id,
885 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
886 'product_uos': line.product_uos and line.product_uos.id or False,
888 'move_dest_id': res_dest_id,
889 'location_id': production.location_src_id.id,
890 'location_dest_id': routing_loc or production.location_src_id.id,
892 'company_id': production.company_id.id,
894 proc_id = proc_obj.create(cr, uid, {
895 'name': (production.origin or '').split(':')[0] + ':' + production.name,
896 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
897 'date_planned': newdate,
898 'product_id': line.product_id.id,
899 'product_qty': line.product_qty,
900 'product_uom': line.product_uom.id,
901 'product_uos_qty': line.product_uos and line.product_qty or False,
902 'product_uos': line.product_uos and line.product_uos.id or False,
903 'location_id': production.location_src_id.id,
904 'procure_method': line.product_id.procure_method,
906 'company_id': production.company_id.id,
908 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
909 proc_ids.append(proc_id)
910 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
911 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
912 message = _("Manufacturing order '%s' is scheduled for the %s.") % (
914 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
916 self.log(cr, uid, production.id, message)
919 def force_production(self, cr, uid, ids, *args):
920 """ Assigns products.
921 @param *args: Arguments
924 pick_obj = self.pool.get('stock.picking')
925 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
930 class mrp_production_workcenter_line(osv.osv):
931 _name = 'mrp.production.workcenter.line'
932 _description = 'Work Order'
936 'name': fields.char('Work Order', size=64, required=True),
937 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
938 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
939 'hour': fields.float('Nbr of hours', digits=(16,2)),
940 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
941 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
944 'sequence': lambda *a: 1,
945 'hour': lambda *a: 0,
946 'cycle': lambda *a: 0,
948 mrp_production_workcenter_line()
950 class mrp_production_product_line(osv.osv):
951 _name = 'mrp.production.product.line'
952 _description = 'Production Scheduled Product'
954 'name': fields.char('Name', size=64, required=True),
955 'product_id': fields.many2one('product.product', 'Product', required=True),
956 'product_qty': fields.float('Product Qty', required=True),
957 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
958 'product_uos_qty': fields.float('Product UOS Qty'),
959 'product_uos': fields.many2one('product.uom', 'Product UOS'),
960 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
962 mrp_production_product_line()
964 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: