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 itertools import groupby
23 from operator import itemgetter
24 from datetime import datetime
25 from osv import osv, fields
26 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 workcenter. Explain here what's a cycle according to this workcenter."),
44 '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."),
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 Workcenter 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 Workcenter 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),
59 'capacity_per_cycle': 1.0,
60 'resource_type': 'material',
65 class mrp_routing(osv.osv):
67 For specifying the routings of workcenters.
70 _description = 'Routing'
72 'name': fields.char('Name', size=64, required=True),
73 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the routing without removing it."),
74 'code': fields.char('Code', size=8),
76 'note': fields.text('Description'),
77 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
79 'location_id': fields.many2one('stock.location', 'Production Location',
80 help="Keep empty if you produce at the location where the finished products are needed." \
81 "Set a location if you produce at a fixed location. This can be a partner location " \
82 "if you subcontract the manufacturing operations."
86 'active': lambda *a: 1,
90 class mrp_routing_workcenter(osv.osv):
92 Defines working cycles and hours of a workcenter using routings.
94 _name = 'mrp.routing.workcenter'
95 _description = 'Workcenter Usage'
97 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
98 'name': fields.char('Name', size=64, required=True),
99 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing workcenters."),
100 'cycle_nbr': fields.float('Number of Cycles', required=True,
101 help="Number of operations this workcenter can do."),
102 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for doing one cycle."),
103 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
104 help="Routing indicates all the workcenters used, for how long and/or cycles." \
105 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
106 'note': fields.text('Description')
109 'cycle_nbr': lambda *a: 1.0,
110 'hour_nbr': lambda *a: 0.0,
112 mrp_routing_workcenter()
114 class mrp_bom(osv.osv):
116 Defines bills of material for a product.
119 _description = 'Bill of Material'
121 def _child_compute(self, cr, uid, ids, name, arg, context={}):
123 @param self: The object pointer
124 @param cr: The current row, from the database cursor,
125 @param uid: The current user ID for security checks
126 @param ids: List of selected IDs
127 @param name: Name of the field
128 @param arg: User defined argument
129 @param context: A standard dictionary for contextual values
130 @return: Dictionary of values
133 bom_obj = self.pool.get('mrp.bom')
134 bom_id = context and context.get('active_id', False) or False
135 cr.execute('select id from mrp_bom')
136 if all(bom_id != r[0] for r in cr.fetchall()):
139 bom_parent = bom_obj.browse(cr, uid, bom_id)
140 for bom in self.browse(cr, uid, ids, context=context):
141 if (bom_parent) or (bom.id == bom_id):
142 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
147 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
148 if (bom.type=='phantom' or ok):
149 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
151 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
152 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
156 def _compute_type(self, cr, uid, ids, field_name, arg, context):
157 """ Sets particular method for the selected bom type.
158 @param field_name: Name of the field
159 @param arg: User defined argument
160 @return: Dictionary of values
162 res = dict(map(lambda x: (x,''), ids))
163 for line in self.browse(cr, uid, ids):
164 if line.type == 'phantom' and not line.bom_id:
167 if line.bom_lines or line.type == 'phantom':
169 if line.product_id.supply_method == 'produce':
170 if line.product_id.procure_method == 'make_to_stock':
171 res[line.id] = 'stock'
173 res[line.id] = 'order'
177 'name': fields.char('Name', size=64, required=True),
178 'code': fields.char('Reference', size=16),
179 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the bills of material without removing it."),
180 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
181 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
182 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
183 "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."),
184 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
185 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
186 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
187 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
188 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
189 'product_id': fields.many2one('product.product', 'Product', required=True),
190 'product_uos_qty': fields.float('Product UOS Qty'),
191 '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."),
192 'product_qty': fields.float('Product Qty', required=True),
193 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
194 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
195 'product_efficiency': fields.float('Product Efficiency', required=True, help="Material efficiency. A factor of 0.9 means a loss of 10% in the production."),
196 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
197 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
198 '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."),
199 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
200 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
201 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
202 'company_id': fields.many2one('res.company','Company',required=True),
205 'active': lambda *a: 1,
206 'product_efficiency': lambda *a: 1.0,
207 'product_qty': lambda *a: 1.0,
208 'product_rounding': lambda *a: 0.0,
209 'type': lambda *a: 'normal',
210 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
214 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
215 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
218 def _check_recursion(self, cr, uid, ids):
221 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
222 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
228 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
232 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
233 """ Changes UoM and name if product_id changes.
234 @param name: Name of the field
235 @param product_id: Changed product_id
236 @return: Dictionary of changed values
239 prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
240 v = {'product_uom': prod.uom_id.id}
242 v['name'] = prod.name
246 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
247 """ Finds BoM for particular product and product uom.
248 @param product_id: Selected product.
249 @param product_uom: Unit of measure of a product.
250 @param properties: List of related properties.
251 @return: False or BoM id.
253 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
254 ids = map(lambda x: x[0], cr.fetchall())
257 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
259 for prop_id in bom.property_ids:
260 if prop_id.id in properties:
262 if (prop > max_prop) or ((max_prop == 0) and not result):
267 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
268 """ Finds Products and Workcenters for related BoM for manufacturing order.
269 @param bom: BoM of particular product.
270 @param factor: Factor of product UoM.
271 @param properties: A List of properties Ids.
272 @param addthis: If BoM found then True else False.
273 @param level: Depth level to find BoM lines starts from 10.
274 @return: result: List of dictionaries containing product details.
275 result2: List of dictionaries containing workcenter details.
277 factor = factor / (bom.product_efficiency or 1.0)
278 factor = rounding(factor, bom.product_rounding)
279 if factor < bom.product_rounding:
280 factor = bom.product_rounding
284 if bom.type == 'phantom' and not bom.bom_lines:
285 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
287 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
288 result = result + res[0]
289 result2 = result2 + res[1]
294 if addthis and not bom.bom_lines:
297 'name': bom.product_id.name,
298 'product_id': bom.product_id.id,
299 'product_qty': bom.product_qty * factor,
300 'product_uom': bom.product_uom.id,
301 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
302 'product_uos': bom.product_uos and bom.product_uos.id or False,
305 for wc_use in bom.routing_id.workcenter_lines:
306 wc = wc_use.workcenter_id
307 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
308 mult = (d + (m and 1.0 or 0.0))
309 cycle = mult * wc_use.cycle_nbr
311 'name': bom.routing_id.name,
312 'workcenter_id': wc.id,
313 'sequence': level+(wc_use.sequence or 0),
315 '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)),
317 for bom2 in bom.bom_lines:
318 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
319 result = result + res[0]
320 result2 = result2 + res[1]
321 return result, result2
325 class mrp_bom_revision(osv.osv):
326 _name = 'mrp.bom.revision'
327 _description = 'Bill of Material Revision'
329 'name': fields.char('Modification name', size=64, required=True),
330 'description': fields.text('Description'),
331 'date': fields.date('Modification Date'),
332 'indice': fields.char('Revision', size=16),
333 'last_indice': fields.char('last indice', size=64),
334 'author_id': fields.many2one('res.users', 'Author'),
335 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
339 'author_id': lambda x, y, z, c: z,
340 'date': lambda *a: time.strftime('%Y-%m-%d'),
348 return round(f / r) * r
350 class many2many_domain(fields.many2many):
351 def set(self, cr, obj, id, name, values, user=None, context=None):
352 return super(many2many_domain, self).set(cr, obj, id, name, values, user=user,
355 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
359 move_obj = obj.pool.get('stock.move')
363 valid_move_ids = move_obj.search(cr, user, self._domain) # move ids relative to domain argument
365 cr.execute("SELECT production_id, move_id from mrp_production_move_ids where production_id in %s and move_id in %s",
366 [tuple(ids), tuple(valid_move_ids)])
367 related_move_map = cr.fetchall()
368 related_move_dict = dict((k, list(set([v[1] for v in itr]))) for k, itr in groupby(related_move_map, itemgetter(0)))
369 res.update(related_move_dict)
373 class one2many_domain(fields.one2many):
374 def set(self, cr, obj, id, field, values, user=None, context=None):
375 return super(one2many_domain, self).set(cr, obj, id, field, values,
376 user=user, context=context)
378 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
382 move_obj = obj.pool.get('stock.move')
386 move_ids = move_obj.search(cr, user, self._domain+[('production_id', 'in', tuple(ids))], context=context)
387 related_move_dict = dict([(o.production_id.id, [o.id]) for o in move_obj.browse(cr, user, move_ids, context=context)])
388 res.update(related_move_dict)
392 class mrp_production(osv.osv):
394 Production Orders / Manufacturing Orders
396 _name = 'mrp.production'
397 _description = 'Manufacturing Order'
398 _date_name = 'date_planned'
400 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
401 """ Calculates total hours and total no. of cycles for a production order.
402 @param prop: Name of field.
404 @return: Dictionary of values.
407 for prod in self.browse(cr, uid, ids, context=context):
412 for wc in prod.workcenter_lines:
413 result[prod.id]['hour_total'] += wc.hour
414 result[prod.id]['cycle_total'] += wc.cycle
417 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
418 """ Finds production end date.
419 @param prop: Name of field.
421 @return: Dictionary of values.
424 for prod in self.browse(cr, uid, ids, context=context):
425 result[prod.id] = prod.date_planned
428 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
429 """ Finds production planned date.
430 @param prop: Name of field.
432 @return: Dictionary of values.
435 for prod in self.browse(cr, uid, ids, context=context):
436 result[prod.id] = prod.date_planned[:10]
440 'name': fields.char('Reference', size=64, required=True),
441 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
442 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
444 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
445 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
446 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
447 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
448 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
450 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
451 help="Location where the system will look for components."),
452 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
453 help="Location where the system will stock the finished products."),
455 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
456 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
457 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
458 'date_start': fields.datetime('Start Date'),
459 'date_finnished': fields.datetime('End Date'),
461 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
462 '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."),
464 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
465 help="This is the internal picking list that brings the finished product to the production plan"),
466 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
467 'move_lines': many2many_domain('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
468 'move_lines2': many2many_domain('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
469 'move_created_ids': one2many_domain('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
470 'move_created_ids2': one2many_domain('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
471 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
472 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
473 '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,
474 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\'.\
475 \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\'.'),
476 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
477 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
478 'company_id': fields.many2one('res.company','Company',required=True),
481 'priority': lambda *a: '1',
482 'state': lambda *a: 'draft',
483 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
484 'product_qty': lambda *a: 1.0,
485 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
486 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
488 _order = 'priority desc, date_planned asc';
490 def _check_qty(self, cr, uid, ids):
491 orders = self.browse(cr, uid, ids)
493 if order.product_qty <= 0:
498 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
501 def unlink(self, cr, uid, ids, context=None):
502 productions = self.read(cr, uid, ids, ['state'])
504 for s in productions:
505 if s['state'] in ['draft','cancel']:
506 unlink_ids.append(s['id'])
508 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
509 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
511 def copy(self, cr, uid, id, default=None, context=None):
515 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
518 'move_created_ids' : [],
519 'move_created_ids2' : [],
520 'product_lines' : [],
523 return super(mrp_production, self).copy(cr, uid, id, default, context)
525 def location_id_change(self, cr, uid, ids, src, dest, context={}):
526 """ Changes destination location if source location is changed.
527 @param src: Source location id.
528 @param dest: Destination location id.
529 @return: Dictionary of values.
534 return {'value': {'location_dest_id': src}}
537 def product_id_change(self, cr, uid, ids, product_id, context=None):
538 """ Finds UoM of changed product.
539 @param product_id: Id of changed product.
540 @return: Dictionary of values.
544 'product_uom': False,
548 bom_obj = self.pool.get('mrp.bom')
549 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
550 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
553 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
554 routing_id = bom_point.routing_id.id or False
556 'product_uom': product.uom_id and product.uom_id.id or False,
558 'routing_id': routing_id
560 return {'value': result}
562 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
563 """ Finds routing for changed BoM.
564 @param product: Id of product.
565 @return: Dictionary of values.
571 bom_pool = self.pool.get('mrp.bom')
572 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
573 routing_id = bom_point.routing_id.id or False
575 'routing_id': routing_id
577 return {'value': result}
579 def action_picking_except(self, cr, uid, ids):
580 """ Changes the state to Exception.
583 self.write(cr, uid, ids, {'state': 'picking_except'})
586 def action_compute(self, cr, uid, ids, properties=[]):
587 """ Computes bills of material of a product.
588 @param properties: List containing dictionaries of properties.
589 @return: No. of products.
592 bom_obj = self.pool.get('mrp.bom')
593 prod_line_obj = self.pool.get('mrp.production.product.line')
594 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
595 for production in self.browse(cr, uid, ids):
596 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
597 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
598 bom_point = production.bom_id
599 bom_id = production.bom_id.id
601 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
603 bom_point = bom_obj.browse(cr, uid, bom_id)
604 routing_id = bom_point.routing_id.id or False
605 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
608 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
610 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
611 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
615 line['production_id'] = production.id
616 prod_line_obj.create(cr, uid, line)
617 for line in results2:
618 line['production_id'] = production.id
619 workcenter_line_obj.create(cr, uid, line)
622 def action_cancel(self, cr, uid, ids):
623 """ Cancels the production order and related stock moves.
626 move_obj = self.pool.get('stock.move')
627 for production in self.browse(cr, uid, ids):
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' for %s %s is Ready to produce.") % (
649 production.product_qty,
650 production.product_id.name,
652 self.log(cr, uid, production_id, message)
655 def action_production_end(self, cr, uid, ids):
656 """ Changes production state to Finish and writes finished date.
659 for production in self.browse(cr, uid, ids):
660 self._costs_generate(cr, uid, production)
661 return self.write(cr, uid, ids, {'state': 'done', 'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
663 def test_production_done(self, cr, uid, ids):
664 """ Tests whether production is done or not.
665 @return: True or False
668 for production in self.browse(cr, uid, ids):
669 if production.move_lines:
672 if production.move_created_ids:
676 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
677 """ To produce final product based on production mode (consume/consume&produce).
678 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
679 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
680 and stock move lines of final product will be also done/produced.
681 @param production_id: the ID of mrp.production object
682 @param production_qty: specify qty to produce
683 @param production_mode: specify production mode (consume/consume&produce).
687 stock_mov_obj = self.pool.get('stock.move')
688 production = self.browse(cr, uid, production_id)
690 final_product_todo = []
693 if production_mode == 'consume_produce':
694 produced_qty = production_qty
696 for produced_product in production.move_created_ids2:
697 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
699 produced_qty += produced_product.product_qty
701 if production_mode in ['consume','consume_produce']:
702 consumed_products = {}
704 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
706 for consumed_product in production.move_lines2:
707 consumed = consumed_product.product_qty
708 if consumed_product.scrapped:
710 if not consumed_products.get(consumed_product.product_id.id, False):
711 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
712 check[consumed_product.product_id.id] = 0
713 for f in production.product_lines:
714 if f.product_id.id == consumed_product.product_id.id:
715 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
716 check[consumed_product.product_id.id] += consumed_product.product_qty
717 consumed = check[consumed_product.product_id.id]
718 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
719 consumed_products[consumed_product.product_id.id] = rest_consumed
721 for raw_product in production.move_lines:
722 for f in production.product_lines:
723 if f.product_id.id == raw_product.product_id.id:
724 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
725 if consumed_qty == 0:
726 consumed_qty = production_qty * f.product_qty / production.product_qty
728 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
730 if production_mode == 'consume_produce':
731 # To produce remaining qty of final product
732 vals = {'state':'confirmed'}
733 final_product_todo = [x.id for x in production.move_created_ids]
734 stock_mov_obj.write(cr, uid, final_product_todo, vals)
735 produced_products = {}
736 for produced_product in production.move_created_ids2:
737 if produced_product.scrapped:
739 if not produced_products.get(produced_product.product_id.id, False):
740 produced_products[produced_product.product_id.id] = 0
741 produced_products[produced_product.product_id.id] += produced_product.product_qty
743 for produce_product in production.move_created_ids:
744 produced_qty = produced_products.get(produce_product.product_id.id, 0)
745 rest_qty = production.product_qty - produced_qty
746 if rest_qty <= production_qty:
747 production_qty = rest_qty
749 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, production.location_dest_id.id, context=context)
751 for raw_product in production.move_lines2:
753 parent_move_ids = [x.id for x in raw_product.move_history_ids]
754 for final_product in production.move_created_ids2:
755 if final_product.id not in parent_move_ids:
756 new_parent_ids.append(final_product.id)
757 for new_parent_id in new_parent_ids:
758 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
760 wf_service = netsvc.LocalService("workflow")
761 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
762 for (id, name) in self.pool.get('product.product').name_get(cr, uid, [production.product_id.id]):
763 message = str(production_qty) + ' ' + production.product_uom.name +" '" + name + _("' have been manufactured for ") + production.name
764 self.log(cr, uid, production_id, message)
767 def _costs_generate(self, cr, uid, production):
768 """ Calculates total costs at the end of the production.
769 @param production: Id of production order.
770 @return: Calculated amount.
773 analytic_line_obj = self.pool.get('account.analytic.line')
774 for wc_line in production.workcenter_lines:
775 wc = wc_line.workcenter_id
776 if wc.costs_journal_id and wc.costs_general_account_id:
777 value = wc_line.hour * wc.costs_hour
778 account = wc.costs_hour_account_id.id
779 if value and account:
781 analytic_line_obj.create(cr, uid, {
782 'name': wc_line.name + ' (H)',
784 'account_id': account,
785 'general_account_id': wc.costs_general_account_id.id,
786 'journal_id': wc.costs_journal_id.id,
789 if wc.costs_journal_id and wc.costs_general_account_id:
790 value = wc_line.cycle * wc.costs_cycle
791 account = wc.costs_cycle_account_id.id
792 if value and account:
794 analytic_line_obj.create(cr, uid, {
795 'name': wc_line.name+' (C)',
797 'account_id': account,
798 'general_account_id': wc.costs_general_account_id.id,
799 'journal_id': wc.costs_journal_id.id,
804 def action_in_production(self, cr, uid, ids):
805 """ Changes state to In Production and writes starting date.
808 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
811 def test_if_product(self, cr, uid, ids):
813 @return: True or False
816 for production in self.browse(cr, uid, ids):
817 if not production.product_lines:
818 if not self.action_compute(cr, uid, [production.id]):
822 def _get_auto_picking(self, cr, uid, production):
825 def action_confirm(self, cr, uid, ids):
826 """ Confirms production order.
827 @return: Newly generated picking Id.
831 seq_obj = self.pool.get('ir.sequence')
832 pick_obj = self.pool.get('stock.picking')
833 move_obj = self.pool.get('stock.move')
834 proc_obj = self.pool.get('procurement.order')
835 wf_service = netsvc.LocalService("workflow")
836 for production in self.browse(cr, uid, ids):
837 if not production.product_lines:
838 self.action_compute(cr, uid, [production.id])
839 production = self.browse(cr, uid, [production.id])[0]
841 pick_type = 'internal'
843 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
844 routing_loc = production.bom_id.routing_id.location_id
845 if routing_loc.usage <> 'internal':
847 address_id = routing_loc.address_id and routing_loc.address_id.id or False
848 routing_loc = routing_loc.id
849 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
850 picking_id = pick_obj.create(cr, uid, {
852 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
856 'address_id': address_id,
857 'auto_picking': self._get_auto_picking(cr, uid, production),
858 'company_id': production.company_id.id,
861 source = production.product_id.product_tmpl_id.property_stock_production.id
863 'name':'PROD:' + production.name,
864 'date_planned': production.date_planned,
865 'product_id': production.product_id.id,
866 'product_qty': production.product_qty,
867 'product_uom': production.product_uom.id,
868 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
869 'product_uos': production.product_uos and production.product_uos.id or False,
870 'location_id': source,
871 'location_dest_id': production.location_dest_id.id,
872 'move_dest_id': production.move_prod_id.id,
874 'company_id': production.company_id.id,
876 res_final_id = move_obj.create(cr, uid, data)
878 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
880 for line in production.product_lines:
882 newdate = production.date_planned
883 if line.product_id.type in ('product', 'consu'):
884 res_dest_id = move_obj.create(cr, uid, {
885 'name':'PROD:' + production.name,
886 'date_planned': production.date_planned,
887 'product_id': line.product_id.id,
888 'product_qty': line.product_qty,
889 'product_uom': line.product_uom.id,
890 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
891 'product_uos': line.product_uos and line.product_uos.id or False,
892 'location_id': routing_loc or production.location_src_id.id,
893 'location_dest_id': source,
894 'move_dest_id': res_final_id,
896 'company_id': production.company_id.id,
898 moves.append(res_dest_id)
899 move_id = move_obj.create(cr, uid, {
900 'name':'PROD:' + production.name,
901 'picking_id':picking_id,
902 'product_id': line.product_id.id,
903 'product_qty': line.product_qty,
904 'product_uom': line.product_uom.id,
905 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
906 'product_uos': line.product_uos and line.product_uos.id or False,
907 'date_planned': newdate,
908 'move_dest_id': res_dest_id,
909 'location_id': production.location_src_id.id,
910 'location_dest_id': routing_loc or production.location_src_id.id,
912 'company_id': production.company_id.id,
914 proc_id = proc_obj.create(cr, uid, {
915 'name': (production.origin or '').split(':')[0] + ':' + production.name,
916 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
917 'date_planned': newdate,
918 'product_id': line.product_id.id,
919 'product_qty': line.product_qty,
920 'product_uom': line.product_uom.id,
921 'product_uos_qty': line.product_uos and line.product_qty or False,
922 'product_uos': line.product_uos and line.product_uos.id or False,
923 'location_id': production.location_src_id.id,
924 'procure_method': line.product_id.procure_method,
926 'company_id': production.company_id.id,
928 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
929 proc_ids.append(proc_id)
930 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
931 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
932 message = ("%s '%s' %s %s %s %s %s %s.") % (
933 _('Manufacturing Order'),
936 production.product_qty,
937 production.product_id.name,
938 _('scheduled for date '),
939 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d'),
942 self.log(cr, uid, production.id, message)
945 def force_production(self, cr, uid, ids, *args):
946 """ Assigns products.
947 @param *args: Arguments
950 pick_obj = self.pool.get('stock.picking')
951 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
956 class mrp_production_workcenter_line(osv.osv):
957 _name = 'mrp.production.workcenter.line'
958 _description = 'Work Order'
962 'name': fields.char('Work Order', size=64, required=True),
963 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
964 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
965 'hour': fields.float('Nbr of hours', digits=(16,2)),
966 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
967 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
970 'sequence': lambda *a: 1,
971 'hour': lambda *a: 0,
972 'cycle': lambda *a: 0,
974 mrp_production_workcenter_line()
976 class mrp_production_product_line(osv.osv):
977 _name = 'mrp.production.product.line'
978 _description = 'Production Scheduled Product'
980 'name': fields.char('Name', size=64, required=True),
981 'product_id': fields.many2one('product.product', 'Product', required=True),
982 'product_qty': fields.float('Product Qty', required=True),
983 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
984 'product_uos_qty': fields.float('Product UOS Qty'),
985 'product_uos': fields.many2one('product.uom', 'Product UOS'),
986 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
988 mrp_production_product_line()
990 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: