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."
84 'company_id': fields.many2one('res.company', 'Company'),
87 'active': lambda *a: 1,
88 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
92 class mrp_routing_workcenter(osv.osv):
94 Defines working cycles and hours of a workcenter using routings.
96 _name = 'mrp.routing.workcenter'
97 _description = 'Workcenter Usage'
99 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
100 'name': fields.char('Name', size=64, required=True),
101 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing workcenters."),
102 'cycle_nbr': fields.float('Number of Cycles', required=True,
103 help="Number of operations this workcenter can do."),
104 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for doing one cycle."),
105 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
106 help="Routing indicates all the workcenters used, for how long and/or cycles." \
107 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
108 'note': fields.text('Description'),
109 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company'),
112 'cycle_nbr': lambda *a: 1.0,
113 'hour_nbr': lambda *a: 0.0,
115 mrp_routing_workcenter()
117 class mrp_bom(osv.osv):
119 Defines bills of material for a product.
122 _description = 'Bill of Material'
124 def _child_compute(self, cr, uid, ids, name, arg, context={}):
126 @param self: The object pointer
127 @param cr: The current row, from the database cursor,
128 @param uid: The current user ID for security checks
129 @param ids: List of selected IDs
130 @param name: Name of the field
131 @param arg: User defined argument
132 @param context: A standard dictionary for contextual values
133 @return: Dictionary of values
136 bom_obj = self.pool.get('mrp.bom')
137 bom_id = context and context.get('active_id', False) or False
138 cr.execute('select id from mrp_bom')
139 if all(bom_id != r[0] for r in cr.fetchall()):
142 bom_parent = bom_obj.browse(cr, uid, bom_id)
143 for bom in self.browse(cr, uid, ids, context=context):
144 if (bom_parent) or (bom.id == bom_id):
145 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
150 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
151 if (bom.type=='phantom' or ok):
152 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
154 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
155 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
159 def _compute_type(self, cr, uid, ids, field_name, arg, context):
160 """ Sets particular method for the selected bom type.
161 @param field_name: Name of the field
162 @param arg: User defined argument
163 @return: Dictionary of values
165 res = dict(map(lambda x: (x,''), ids))
166 for line in self.browse(cr, uid, ids):
167 if line.type == 'phantom' and not line.bom_id:
170 if line.bom_lines or line.type == 'phantom':
172 if line.product_id.supply_method == 'produce':
173 if line.product_id.procure_method == 'make_to_stock':
174 res[line.id] = 'stock'
176 res[line.id] = 'order'
180 'name': fields.char('Name', size=64, required=True),
181 'code': fields.char('Reference', size=16),
182 '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."),
183 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
184 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
185 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
186 "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."),
187 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
188 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
189 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
190 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
191 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
192 'product_id': fields.many2one('product.product', 'Product', required=True),
193 'product_uos_qty': fields.float('Product UOS Qty'),
194 '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."),
195 'product_qty': fields.float('Product Qty', required=True),
196 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
197 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
198 'product_efficiency': fields.float('Product Efficiency', required=True, help="Material efficiency. A factor of 0.9 means a loss of 10% in the production."),
199 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
200 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
201 '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."),
202 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
203 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
204 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
205 'company_id': fields.many2one('res.company','Company',required=True),
208 'active': lambda *a: 1,
209 'product_efficiency': lambda *a: 1.0,
210 'product_qty': lambda *a: 1.0,
211 'product_rounding': lambda *a: 0.0,
212 'type': lambda *a: 'normal',
213 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
217 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
218 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
221 def _check_recursion(self, cr, uid, ids):
224 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
225 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
231 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
235 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
236 """ Changes UoM and name if product_id changes.
237 @param name: Name of the field
238 @param product_id: Changed product_id
239 @return: Dictionary of changed values
242 prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
243 v = {'product_uom': prod.uom_id.id}
245 v['name'] = prod.name
249 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
250 """ Finds BoM for particular product and product uom.
251 @param product_id: Selected product.
252 @param product_uom: Unit of measure of a product.
253 @param properties: List of related properties.
254 @return: False or BoM id.
256 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
257 ids = map(lambda x: x[0], cr.fetchall())
260 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
262 for prop_id in bom.property_ids:
263 if prop_id.id in properties:
265 if (prop > max_prop) or ((max_prop == 0) and not result):
270 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
271 """ Finds Products and Workcenters for related BoM for manufacturing order.
272 @param bom: BoM of particular product.
273 @param factor: Factor of product UoM.
274 @param properties: A List of properties Ids.
275 @param addthis: If BoM found then True else False.
276 @param level: Depth level to find BoM lines starts from 10.
277 @return: result: List of dictionaries containing product details.
278 result2: List of dictionaries containing workcenter details.
280 factor = factor / (bom.product_efficiency or 1.0)
281 factor = rounding(factor, bom.product_rounding)
282 if factor < bom.product_rounding:
283 factor = bom.product_rounding
287 if bom.type == 'phantom' and not bom.bom_lines:
288 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
290 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
291 result = result + res[0]
292 result2 = result2 + res[1]
297 if addthis and not bom.bom_lines:
300 'name': bom.product_id.name,
301 'product_id': bom.product_id.id,
302 'product_qty': bom.product_qty * factor,
303 'product_uom': bom.product_uom.id,
304 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
305 'product_uos': bom.product_uos and bom.product_uos.id or False,
308 for wc_use in bom.routing_id.workcenter_lines:
309 wc = wc_use.workcenter_id
310 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
311 mult = (d + (m and 1.0 or 0.0))
312 cycle = mult * wc_use.cycle_nbr
314 'name': bom.routing_id.name,
315 'workcenter_id': wc.id,
316 'sequence': level+(wc_use.sequence or 0),
318 '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)),
320 for bom2 in bom.bom_lines:
321 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
322 result = result + res[0]
323 result2 = result2 + res[1]
324 return result, result2
328 class mrp_bom_revision(osv.osv):
329 _name = 'mrp.bom.revision'
330 _description = 'Bill of Material Revision'
332 'name': fields.char('Modification name', size=64, required=True),
333 'description': fields.text('Description'),
334 'date': fields.date('Modification Date'),
335 'indice': fields.char('Revision', size=16),
336 'last_indice': fields.char('last indice', size=64),
337 'author_id': fields.many2one('res.users', 'Author'),
338 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
342 'author_id': lambda x, y, z, c: z,
343 'date': lambda *a: time.strftime('%Y-%m-%d'),
351 return round(f / r) * r
353 class many2many_domain(fields.many2many):
354 def set(self, cr, obj, id, name, values, user=None, context=None):
355 return super(many2many_domain, self).set(cr, obj, id, name, values, user=user,
358 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
362 move_obj = obj.pool.get('stock.move')
366 valid_move_ids = move_obj.search(cr, user, self._domain) # move ids relative to domain argument
368 cr.execute("SELECT production_id, move_id from mrp_production_move_ids where production_id in %s and move_id in %s",
369 [tuple(ids), tuple(valid_move_ids)])
370 related_move_map = cr.fetchall()
371 related_move_dict = dict((k, list(set([v[1] for v in itr]))) for k, itr in groupby(related_move_map, itemgetter(0)))
372 res.update(related_move_dict)
376 class one2many_domain(fields.one2many):
377 def set(self, cr, obj, id, field, values, user=None, context=None):
378 return super(one2many_domain, self).set(cr, obj, id, field, values,
379 user=user, context=context)
381 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
385 move_obj = obj.pool.get('stock.move')
389 move_ids = move_obj.search(cr, user, self._domain+[('production_id', 'in', tuple(ids))], context=context)
390 related_move_dict = dict([(o.production_id.id, [o.id]) for o in move_obj.browse(cr, user, move_ids, context=context)])
391 res.update(related_move_dict)
395 class mrp_production(osv.osv):
397 Production Orders / Manufacturing Orders
399 _name = 'mrp.production'
400 _description = 'Manufacturing Order'
401 _date_name = 'date_planned'
403 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
404 """ Calculates total hours and total no. of cycles for a production order.
405 @param prop: Name of field.
407 @return: Dictionary of values.
410 for prod in self.browse(cr, uid, ids, context=context):
415 for wc in prod.workcenter_lines:
416 result[prod.id]['hour_total'] += wc.hour
417 result[prod.id]['cycle_total'] += wc.cycle
420 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
421 """ Finds production end date.
422 @param prop: Name of field.
424 @return: Dictionary of values.
427 for prod in self.browse(cr, uid, ids, context=context):
428 result[prod.id] = prod.date_planned
431 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
432 """ Finds production planned date.
433 @param prop: Name of field.
435 @return: Dictionary of values.
438 for prod in self.browse(cr, uid, ids, context=context):
439 result[prod.id] = prod.date_planned[:10]
443 'name': fields.char('Reference', size=64, required=True),
444 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
445 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
447 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
448 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
449 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
450 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
451 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
453 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
454 help="Location where the system will look for components."),
455 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
456 help="Location where the system will stock the finished products."),
458 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
459 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
460 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
461 'date_start': fields.datetime('Start Date'),
462 'date_finished': fields.datetime('End Date'),
464 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
465 '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."),
467 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
468 help="This is the internal picking list that brings the finished product to the production plan"),
469 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
470 '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)]}),
471 'move_lines2': many2many_domain('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
472 'move_created_ids': one2many_domain('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
473 'move_created_ids2': one2many_domain('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
474 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
475 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
476 '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,
477 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\'.\
478 \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\'.'),
479 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
480 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
481 'company_id': fields.many2one('res.company','Company',required=True),
484 'priority': lambda *a: '1',
485 'state': lambda *a: 'draft',
486 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
487 'product_qty': lambda *a: 1.0,
488 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
489 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
491 _order = 'priority desc, date_planned asc';
493 def _check_qty(self, cr, uid, ids):
494 orders = self.browse(cr, uid, ids)
496 if order.product_qty <= 0:
501 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
504 def unlink(self, cr, uid, ids, context=None):
505 productions = self.read(cr, uid, ids, ['state'])
507 for s in productions:
508 if s['state'] in ['draft','cancel']:
509 unlink_ids.append(s['id'])
511 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
512 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
514 def copy(self, cr, uid, id, default=None, context=None):
518 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
521 'move_created_ids' : [],
522 'move_created_ids2' : [],
523 'product_lines' : [],
526 return super(mrp_production, self).copy(cr, uid, id, default, context)
528 def location_id_change(self, cr, uid, ids, src, dest, context={}):
529 """ Changes destination location if source location is changed.
530 @param src: Source location id.
531 @param dest: Destination location id.
532 @return: Dictionary of values.
537 return {'value': {'location_dest_id': src}}
540 def product_id_change(self, cr, uid, ids, product_id, context=None):
541 """ Finds UoM of changed product.
542 @param product_id: Id of changed product.
543 @return: Dictionary of values.
547 'product_uom': False,
551 bom_obj = self.pool.get('mrp.bom')
552 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
553 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
556 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
557 routing_id = bom_point.routing_id.id or False
559 'product_uom': product.uom_id and product.uom_id.id or False,
561 'routing_id': routing_id
563 return {'value': result}
565 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
566 """ Finds routing for changed BoM.
567 @param product: Id of product.
568 @return: Dictionary of values.
574 bom_pool = self.pool.get('mrp.bom')
575 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
576 routing_id = bom_point.routing_id.id or False
578 'routing_id': routing_id
580 return {'value': result}
582 def action_picking_except(self, cr, uid, ids):
583 """ Changes the state to Exception.
586 self.write(cr, uid, ids, {'state': 'picking_except'})
589 def action_compute(self, cr, uid, ids, properties=[]):
590 """ Computes bills of material of a product.
591 @param properties: List containing dictionaries of properties.
592 @return: No. of products.
595 bom_obj = self.pool.get('mrp.bom')
596 prod_line_obj = self.pool.get('mrp.production.product.line')
597 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
598 for production in self.browse(cr, uid, ids):
599 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
600 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
601 bom_point = production.bom_id
602 bom_id = production.bom_id.id
604 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
606 bom_point = bom_obj.browse(cr, uid, bom_id)
607 routing_id = bom_point.routing_id.id or False
608 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
611 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
613 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
614 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
618 line['production_id'] = production.id
619 prod_line_obj.create(cr, uid, line)
620 for line in results2:
621 line['production_id'] = production.id
622 workcenter_line_obj.create(cr, uid, line)
625 def action_cancel(self, cr, uid, ids):
626 """ Cancels the production order and related stock moves.
629 move_obj = self.pool.get('stock.move')
630 for production in self.browse(cr, uid, ids):
631 if production.move_created_ids:
632 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
633 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
634 self.write(cr, uid, ids, {'state': 'cancel'})
637 def action_ready(self, cr, uid, ids):
638 """ Changes the production state to Ready and location id of stock move.
641 move_obj = self.pool.get('stock.move')
642 self.write(cr, uid, ids, {'state': 'ready'})
644 for (production_id,name) in self.name_get(cr, uid, ids):
645 production = self.browse(cr, uid, production_id)
646 if production.move_prod_id:
647 move_obj.write(cr, uid, [production.move_prod_id.id],
648 {'location_id': production.location_dest_id.id})
650 message = ("Manufacturing Order '%s' for %s %s is Ready to produce.") % (
652 production.product_qty,
653 production.product_id.name,
655 self.log(cr, uid, production_id, message)
658 def action_production_end(self, cr, uid, ids):
659 """ Changes production state to Finish and writes finished date.
662 for production in self.browse(cr, uid, ids):
663 self._costs_generate(cr, uid, production)
664 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
666 def test_production_done(self, cr, uid, ids):
667 """ Tests whether production is done or not.
668 @return: True or False
671 for production in self.browse(cr, uid, ids):
672 if production.move_lines:
675 if production.move_created_ids:
679 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
680 """ To produce final product based on production mode (consume/consume&produce).
681 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
682 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
683 and stock move lines of final product will be also done/produced.
684 @param production_id: the ID of mrp.production object
685 @param production_qty: specify qty to produce
686 @param production_mode: specify production mode (consume/consume&produce).
690 stock_mov_obj = self.pool.get('stock.move')
691 production = self.browse(cr, uid, production_id)
693 final_product_todo = []
696 if production_mode == 'consume_produce':
697 produced_qty = production_qty
699 for produced_product in production.move_created_ids2:
700 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
702 produced_qty += produced_product.product_qty
704 if production_mode in ['consume','consume_produce']:
705 consumed_products = {}
707 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
709 for consumed_product in production.move_lines2:
710 consumed = consumed_product.product_qty
711 if consumed_product.scrapped:
713 if not consumed_products.get(consumed_product.product_id.id, False):
714 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
715 check[consumed_product.product_id.id] = 0
716 for f in production.product_lines:
717 if f.product_id.id == consumed_product.product_id.id:
718 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
719 check[consumed_product.product_id.id] += consumed_product.product_qty
720 consumed = check[consumed_product.product_id.id]
721 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
722 consumed_products[consumed_product.product_id.id] = rest_consumed
724 for raw_product in production.move_lines:
725 for f in production.product_lines:
726 if f.product_id.id == raw_product.product_id.id:
727 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
728 if consumed_qty == 0:
729 consumed_qty = production_qty * f.product_qty / production.product_qty
731 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
733 if production_mode == 'consume_produce':
734 # To produce remaining qty of final product
735 vals = {'state':'confirmed'}
736 final_product_todo = [x.id for x in production.move_created_ids]
737 stock_mov_obj.write(cr, uid, final_product_todo, vals)
738 produced_products = {}
739 for produced_product in production.move_created_ids2:
740 if produced_product.scrapped:
742 if not produced_products.get(produced_product.product_id.id, False):
743 produced_products[produced_product.product_id.id] = 0
744 produced_products[produced_product.product_id.id] += produced_product.product_qty
746 for produce_product in production.move_created_ids:
747 produced_qty = produced_products.get(produce_product.product_id.id, 0)
748 rest_qty = production.product_qty - produced_qty
749 if rest_qty <= production_qty:
750 production_qty = rest_qty
752 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, production.location_dest_id.id, context=context)
754 for raw_product in production.move_lines2:
756 parent_move_ids = [x.id for x in raw_product.move_history_ids]
757 for final_product in production.move_created_ids2:
758 if final_product.id not in parent_move_ids:
759 new_parent_ids.append(final_product.id)
760 for new_parent_id in new_parent_ids:
761 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
763 wf_service = netsvc.LocalService("workflow")
764 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
765 for (id, name) in self.pool.get('product.product').name_get(cr, uid, [production.product_id.id]):
766 message = str(production_qty) + ' ' + production.product_uom.name +" '" + name + _("' have been manufactured for ") + production.name
767 self.log(cr, uid, production_id, message)
770 def _costs_generate(self, cr, uid, production):
771 """ Calculates total costs at the end of the production.
772 @param production: Id of production order.
773 @return: Calculated amount.
776 analytic_line_obj = self.pool.get('account.analytic.line')
777 for wc_line in production.workcenter_lines:
778 wc = wc_line.workcenter_id
779 if wc.costs_journal_id and wc.costs_general_account_id:
780 value = wc_line.hour * wc.costs_hour
781 account = wc.costs_hour_account_id.id
782 if value and account:
784 analytic_line_obj.create(cr, uid, {
785 'name': wc_line.name + ' (H)',
787 'account_id': account,
788 'general_account_id': wc.costs_general_account_id.id,
789 'journal_id': wc.costs_journal_id.id,
792 if wc.costs_journal_id and wc.costs_general_account_id:
793 value = wc_line.cycle * wc.costs_cycle
794 account = wc.costs_cycle_account_id.id
795 if value and account:
797 analytic_line_obj.create(cr, uid, {
798 'name': wc_line.name+' (C)',
800 'account_id': account,
801 'general_account_id': wc.costs_general_account_id.id,
802 'journal_id': wc.costs_journal_id.id,
807 def action_in_production(self, cr, uid, ids):
808 """ Changes state to In Production and writes starting date.
811 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
814 def test_if_product(self, cr, uid, ids):
816 @return: True or False
819 for production in self.browse(cr, uid, ids):
820 if not production.product_lines:
821 if not self.action_compute(cr, uid, [production.id]):
825 def _get_auto_picking(self, cr, uid, production):
828 def action_confirm(self, cr, uid, ids):
829 """ Confirms production order.
830 @return: Newly generated picking Id.
834 seq_obj = self.pool.get('ir.sequence')
835 pick_obj = self.pool.get('stock.picking')
836 move_obj = self.pool.get('stock.move')
837 proc_obj = self.pool.get('procurement.order')
838 wf_service = netsvc.LocalService("workflow")
839 for production in self.browse(cr, uid, ids):
840 if not production.product_lines:
841 self.action_compute(cr, uid, [production.id])
842 production = self.browse(cr, uid, [production.id])[0]
844 pick_type = 'internal'
846 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
847 routing_loc = production.bom_id.routing_id.location_id
848 if routing_loc.usage <> 'internal':
850 address_id = routing_loc.address_id and routing_loc.address_id.id or False
851 routing_loc = routing_loc.id
852 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
853 picking_id = pick_obj.create(cr, uid, {
855 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
859 'address_id': address_id,
860 'auto_picking': self._get_auto_picking(cr, uid, production),
861 'company_id': production.company_id.id,
864 source = production.product_id.product_tmpl_id.property_stock_production.id
866 'name':'PROD:' + production.name,
867 'date': production.date_planned,
868 'product_id': production.product_id.id,
869 'product_qty': production.product_qty,
870 'product_uom': production.product_uom.id,
871 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
872 'product_uos': production.product_uos and production.product_uos.id or False,
873 'location_id': source,
874 'location_dest_id': production.location_dest_id.id,
875 'move_dest_id': production.move_prod_id.id,
877 'company_id': production.company_id.id,
879 res_final_id = move_obj.create(cr, uid, data)
881 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
883 for line in production.product_lines:
885 newdate = production.date_planned
886 if line.product_id.type in ('product', 'consu'):
887 res_dest_id = move_obj.create(cr, uid, {
888 'name':'PROD:' + production.name,
889 'date': production.date_planned,
890 'product_id': line.product_id.id,
891 'product_qty': line.product_qty,
892 'product_uom': line.product_uom.id,
893 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
894 'product_uos': line.product_uos and line.product_uos.id or False,
895 'location_id': routing_loc or production.location_src_id.id,
896 'location_dest_id': source,
897 'move_dest_id': res_final_id,
899 'company_id': production.company_id.id,
901 moves.append(res_dest_id)
902 move_id = move_obj.create(cr, uid, {
903 'name':'PROD:' + production.name,
904 'picking_id':picking_id,
905 'product_id': line.product_id.id,
906 'product_qty': line.product_qty,
907 'product_uom': line.product_uom.id,
908 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
909 'product_uos': line.product_uos and line.product_uos.id or False,
911 'move_dest_id': res_dest_id,
912 'location_id': production.location_src_id.id,
913 'location_dest_id': routing_loc or production.location_src_id.id,
915 'company_id': production.company_id.id,
917 proc_id = proc_obj.create(cr, uid, {
918 'name': (production.origin or '').split(':')[0] + ':' + production.name,
919 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
920 'date_planned': newdate,
921 'product_id': line.product_id.id,
922 'product_qty': line.product_qty,
923 'product_uom': line.product_uom.id,
924 'product_uos_qty': line.product_uos and line.product_qty or False,
925 'product_uos': line.product_uos and line.product_uos.id or False,
926 'location_id': production.location_src_id.id,
927 'procure_method': line.product_id.procure_method,
929 'company_id': production.company_id.id,
931 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
932 proc_ids.append(proc_id)
933 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
934 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
935 message = ("%s '%s' %s %s %s %s %s %s.") % (
936 _('Manufacturing Order'),
939 production.product_qty,
940 production.product_id.name,
941 _('scheduled for date '),
942 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d'),
945 self.log(cr, uid, production.id, message)
948 def force_production(self, cr, uid, ids, *args):
949 """ Assigns products.
950 @param *args: Arguments
953 pick_obj = self.pool.get('stock.picking')
954 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
959 class mrp_production_workcenter_line(osv.osv):
960 _name = 'mrp.production.workcenter.line'
961 _description = 'Work Order'
965 'name': fields.char('Work Order', size=64, required=True),
966 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
967 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
968 'hour': fields.float('Nbr of hours', digits=(16,2)),
969 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
970 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
973 'sequence': lambda *a: 1,
974 'hour': lambda *a: 0,
975 'cycle': lambda *a: 0,
977 mrp_production_workcenter_line()
979 class mrp_production_product_line(osv.osv):
980 _name = 'mrp.production.product.line'
981 _description = 'Production Scheduled Product'
983 'name': fields.char('Name', size=64, required=True),
984 'product_id': fields.many2one('product.product', 'Product', required=True),
985 'product_qty': fields.float('Product Qty', required=True),
986 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
987 'product_uos_qty': fields.float('Product UOS Qty'),
988 'product_uos': fields.many2one('product.uom', 'Product UOS'),
989 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
991 mrp_production_product_line()
993 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: