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),
58 'capacity_per_cycle': 1.0,
59 'resource_type': 'material',
64 class mrp_routing(osv.osv):
66 For specifying the routings of workcenters.
69 _description = 'Routing'
71 'name': fields.char('Name', size=64, required=True),
72 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
73 'code': fields.char('Code', size=8),
75 'note': fields.text('Description'),
76 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
78 'location_id': fields.many2one('stock.location', 'Production Location',
79 help="Keep empty if you produce at the location where the finished products are needed." \
80 "Set a location if you produce at a fixed location. This can be a partner location " \
81 "if you subcontract the manufacturing operations."
83 'company_id': fields.many2one('res.company', 'Company'),
86 'active': lambda *a: 1,
87 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
91 class mrp_routing_workcenter(osv.osv):
93 Defines working cycles and hours of a workcenter using routings.
95 _name = 'mrp.routing.workcenter'
96 _description = 'Workcenter Usage'
98 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
99 'name': fields.char('Name', size=64, required=True),
100 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing workcenters."),
101 'cycle_nbr': fields.float('Number of Cycles', required=True,
102 help="Number of operations this workcenter can do."),
103 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for doing one cycle."),
104 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
105 help="Routing indicates all the workcenters used, for how long and/or cycles." \
106 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
107 'note': fields.text('Description'),
108 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company'),
111 'cycle_nbr': lambda *a: 1.0,
112 'hour_nbr': lambda *a: 0.0,
114 mrp_routing_workcenter()
116 class mrp_bom(osv.osv):
118 Defines bills of material for a product.
121 _description = 'Bill of Material'
123 def _child_compute(self, cr, uid, ids, name, arg, context={}):
125 @param self: The object pointer
126 @param cr: The current row, from the database cursor,
127 @param uid: The current user ID for security checks
128 @param ids: List of selected IDs
129 @param name: Name of the field
130 @param arg: User defined argument
131 @param context: A standard dictionary for contextual values
132 @return: Dictionary of values
135 bom_obj = self.pool.get('mrp.bom')
136 bom_id = context and context.get('active_id', False) or False
137 cr.execute('select id from mrp_bom')
138 if all(bom_id != r[0] for r in cr.fetchall()):
141 bom_parent = bom_obj.browse(cr, uid, bom_id)
142 for bom in self.browse(cr, uid, ids, context=context):
143 if (bom_parent) or (bom.id == bom_id):
144 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
149 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
150 if (bom.type=='phantom' or ok):
151 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
153 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
154 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
158 def _compute_type(self, cr, uid, ids, field_name, arg, context):
159 """ Sets particular method for the selected bom type.
160 @param field_name: Name of the field
161 @param arg: User defined argument
162 @return: Dictionary of values
164 res = dict(map(lambda x: (x,''), ids))
165 for line in self.browse(cr, uid, ids):
166 if line.type == 'phantom' and not line.bom_id:
169 if line.bom_lines or line.type == 'phantom':
171 if line.product_id.supply_method == 'produce':
172 if line.product_id.procure_method == 'make_to_stock':
173 res[line.id] = 'stock'
175 res[line.id] = 'order'
179 'name': fields.char('Name', size=64, required=True),
180 'code': fields.char('Reference', size=16),
181 '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."),
182 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
183 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
184 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
185 "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."),
186 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
187 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
188 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
189 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
190 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
191 'product_id': fields.many2one('product.product', 'Product', required=True),
192 'product_uos_qty': fields.float('Product UOS Qty'),
193 '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."),
194 'product_qty': fields.float('Product Qty', required=True),
195 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
196 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
197 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
198 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
199 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
200 '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."),
201 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
202 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
203 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
204 'company_id': fields.many2one('res.company','Company',required=True),
207 'active': lambda *a: 1,
208 'product_efficiency': lambda *a: 1.0,
209 'product_qty': lambda *a: 1.0,
210 'product_rounding': lambda *a: 0.0,
211 'type': lambda *a: 'normal',
212 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
216 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
217 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
220 def _check_recursion(self, cr, uid, ids):
223 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
224 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
230 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
234 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
235 """ Changes UoM and name if product_id changes.
236 @param name: Name of the field
237 @param product_id: Changed product_id
238 @return: Dictionary of changed values
241 prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
242 v = {'product_uom': prod.uom_id.id}
244 v['name'] = prod.name
248 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
249 """ Finds BoM for particular product and product uom.
250 @param product_id: Selected product.
251 @param product_uom: Unit of measure of a product.
252 @param properties: List of related properties.
253 @return: False or BoM id.
255 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
256 ids = map(lambda x: x[0], cr.fetchall())
259 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
261 for prop_id in bom.property_ids:
262 if prop_id.id in properties:
264 if (prop > max_prop) or ((max_prop == 0) and not result):
269 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
270 """ Finds Products and Workcenters for related BoM for manufacturing order.
271 @param bom: BoM of particular product.
272 @param factor: Factor of product UoM.
273 @param properties: A List of properties Ids.
274 @param addthis: If BoM found then True else False.
275 @param level: Depth level to find BoM lines starts from 10.
276 @return: result: List of dictionaries containing product details.
277 result2: List of dictionaries containing workcenter details.
279 factor = factor / (bom.product_efficiency or 1.0)
280 factor = rounding(factor, bom.product_rounding)
281 if factor < bom.product_rounding:
282 factor = bom.product_rounding
286 if bom.type == 'phantom' and not bom.bom_lines:
287 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
289 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
290 result = result + res[0]
291 result2 = result2 + res[1]
296 if addthis and not bom.bom_lines:
299 'name': bom.product_id.name,
300 'product_id': bom.product_id.id,
301 'product_qty': bom.product_qty * factor,
302 'product_uom': bom.product_uom.id,
303 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
304 'product_uos': bom.product_uos and bom.product_uos.id or False,
307 for wc_use in bom.routing_id.workcenter_lines:
308 wc = wc_use.workcenter_id
309 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
310 mult = (d + (m and 1.0 or 0.0))
311 cycle = mult * wc_use.cycle_nbr
313 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
314 'workcenter_id': wc.id,
315 'sequence': level+(wc_use.sequence or 0),
317 '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)),
319 for bom2 in bom.bom_lines:
320 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
321 result = result + res[0]
322 result2 = result2 + res[1]
323 return result, result2
325 def copy_data(self, cr, uid, id, default=None, context=None):
330 bom_data = self.read(cr, uid, id, [], context=context)
331 default.update({'name': bom_data['name'] + _(' (copy)')})
332 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
336 class mrp_bom_revision(osv.osv):
337 _name = 'mrp.bom.revision'
338 _description = 'Bill of Material Revision'
340 'name': fields.char('Modification name', size=64, required=True),
341 'description': fields.text('Description'),
342 'date': fields.date('Modification Date'),
343 'indice': fields.char('Revision', size=16),
344 'last_indice': fields.char('last indice', size=64),
345 'author_id': fields.many2one('res.users', 'Author'),
346 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
350 'author_id': lambda x, y, z, c: z,
351 'date': lambda *a: time.strftime('%Y-%m-%d'),
359 return round(f / r) * r
361 class mrp_production(osv.osv):
363 Production Orders / Manufacturing Orders
365 _name = 'mrp.production'
366 _description = 'Manufacturing Order'
367 _date_name = 'date_planned'
369 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
370 """ Calculates total hours and total no. of cycles for a production order.
371 @param prop: Name of field.
373 @return: Dictionary of values.
376 for prod in self.browse(cr, uid, ids, context=context):
381 for wc in prod.workcenter_lines:
382 result[prod.id]['hour_total'] += wc.hour
383 result[prod.id]['cycle_total'] += wc.cycle
386 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
387 """ Finds production end date.
388 @param prop: Name of field.
390 @return: Dictionary of values.
393 for prod in self.browse(cr, uid, ids, context=context):
394 result[prod.id] = prod.date_planned
397 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
398 """ Finds production planned date.
399 @param prop: Name of field.
401 @return: Dictionary of values.
404 for prod in self.browse(cr, uid, ids, context=context):
405 result[prod.id] = prod.date_planned[:10]
409 'name': fields.char('Reference', size=64, required=True),
410 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
411 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
413 'product_id': fields.many2one('product.product', 'Product', required=True, ),
414 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
415 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
416 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
417 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
419 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
420 help="Location where the system will look for components."),
421 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
422 help="Location where the system will stock the finished products."),
424 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
425 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
426 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
427 'date_start': fields.datetime('Start Date'),
428 'date_finished': fields.datetime('End Date'),
430 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
431 '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."),
433 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
434 help="This is the internal picking list that brings the finished product to the production plan"),
435 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
436 '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)]}),
437 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
438 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
439 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
440 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
441 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
442 '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,
443 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\'.\
444 \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\'.'),
445 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
446 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
447 'company_id': fields.many2one('res.company','Company',required=True),
450 'priority': lambda *a: '1',
451 'state': lambda *a: 'draft',
452 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
453 'product_qty': lambda *a: 1.0,
454 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
455 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
457 _order = 'priority desc, date_planned asc';
459 def _check_qty(self, cr, uid, ids):
460 orders = self.browse(cr, uid, ids)
462 if order.product_qty <= 0:
467 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
470 def unlink(self, cr, uid, ids, context=None):
471 productions = self.read(cr, uid, ids, ['state'])
473 for s in productions:
474 if s['state'] in ['draft','cancel']:
475 unlink_ids.append(s['id'])
477 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!') % s['state'])
478 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
480 def copy(self, cr, uid, id, default=None, context=None):
484 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
487 'move_created_ids' : [],
488 'move_created_ids2' : [],
489 'product_lines' : [],
492 return super(mrp_production, self).copy(cr, uid, id, default, context)
494 def location_id_change(self, cr, uid, ids, src, dest, context={}):
495 """ Changes destination location if source location is changed.
496 @param src: Source location id.
497 @param dest: Destination location id.
498 @return: Dictionary of values.
503 return {'value': {'location_dest_id': src}}
506 def product_id_change(self, cr, uid, ids, product_id, context=None):
507 """ Finds UoM of changed product.
508 @param product_id: Id of changed product.
509 @return: Dictionary of values.
513 'product_uom': False,
517 bom_obj = self.pool.get('mrp.bom')
518 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
519 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
522 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
523 routing_id = bom_point.routing_id.id or False
525 'product_uom': product.uom_id and product.uom_id.id or False,
527 'routing_id': routing_id
529 return {'value': result}
531 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
532 """ Finds routing for changed BoM.
533 @param product: Id of product.
534 @return: Dictionary of values.
540 bom_pool = self.pool.get('mrp.bom')
541 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
542 routing_id = bom_point.routing_id.id or False
544 'routing_id': routing_id
546 return {'value': result}
548 def action_picking_except(self, cr, uid, ids):
549 """ Changes the state to Exception.
552 self.write(cr, uid, ids, {'state': 'picking_except'})
555 def action_compute(self, cr, uid, ids, properties=[]):
556 """ Computes bills of material of a product.
557 @param properties: List containing dictionaries of properties.
558 @return: No. of products.
561 bom_obj = self.pool.get('mrp.bom')
562 prod_line_obj = self.pool.get('mrp.production.product.line')
563 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
564 for production in self.browse(cr, uid, ids):
565 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
566 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
567 bom_point = production.bom_id
568 bom_id = production.bom_id.id
570 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
572 bom_point = bom_obj.browse(cr, uid, bom_id)
573 routing_id = bom_point.routing_id.id or False
574 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
577 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
579 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
580 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
584 line['production_id'] = production.id
585 prod_line_obj.create(cr, uid, line)
586 for line in results2:
587 line['production_id'] = production.id
588 workcenter_line_obj.create(cr, uid, line)
591 def action_cancel(self, cr, uid, ids):
592 """ Cancels the production order and related stock moves.
595 move_obj = self.pool.get('stock.move')
596 for production in self.browse(cr, uid, ids):
597 if production.move_created_ids:
598 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
599 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
600 self.write(cr, uid, ids, {'state': 'cancel'})
603 def action_ready(self, cr, uid, ids):
604 """ Changes the production state to Ready and location id of stock move.
607 move_obj = self.pool.get('stock.move')
608 self.write(cr, uid, ids, {'state': 'ready'})
610 for (production_id,name) in self.name_get(cr, uid, ids):
611 production = self.browse(cr, uid, production_id)
612 if production.move_prod_id:
613 move_obj.write(cr, uid, [production.move_prod_id.id],
614 {'location_id': production.location_dest_id.id})
616 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
617 self.log(cr, uid, production_id, message)
620 def action_production_end(self, cr, uid, ids):
621 """ Changes production state to Finish and writes finished date.
624 for production in self.browse(cr, uid, ids):
625 self._costs_generate(cr, uid, production)
626 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
628 def test_production_done(self, cr, uid, ids):
629 """ Tests whether production is done or not.
630 @return: True or False
633 for production in self.browse(cr, uid, ids):
634 if production.move_lines:
637 if production.move_created_ids:
641 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
642 """ To produce final product based on production mode (consume/consume&produce).
643 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
644 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
645 and stock move lines of final product will be also done/produced.
646 @param production_id: the ID of mrp.production object
647 @param production_qty: specify qty to produce
648 @param production_mode: specify production mode (consume/consume&produce).
652 stock_mov_obj = self.pool.get('stock.move')
653 production = self.browse(cr, uid, production_id)
655 final_product_todo = []
658 if production_mode == 'consume_produce':
659 produced_qty = production_qty
661 for produced_product in production.move_created_ids2:
662 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
664 produced_qty += produced_product.product_qty
666 if production_mode in ['consume','consume_produce']:
667 consumed_products = {}
669 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
671 for consumed_product in production.move_lines2:
672 consumed = consumed_product.product_qty
673 if consumed_product.scrapped:
675 if not consumed_products.get(consumed_product.product_id.id, False):
676 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
677 check[consumed_product.product_id.id] = 0
678 for f in production.product_lines:
679 if f.product_id.id == consumed_product.product_id.id:
680 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
681 check[consumed_product.product_id.id] += consumed_product.product_qty
682 consumed = check[consumed_product.product_id.id]
683 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
684 consumed_products[consumed_product.product_id.id] = rest_consumed
686 for raw_product in production.move_lines:
687 for f in production.product_lines:
688 if f.product_id.id == raw_product.product_id.id:
689 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
690 if consumed_qty == 0:
691 consumed_qty = production_qty * f.product_qty / production.product_qty
693 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
695 if production_mode == 'consume_produce':
696 # To produce remaining qty of final product
697 vals = {'state':'confirmed'}
698 final_product_todo = [x.id for x in production.move_created_ids]
699 stock_mov_obj.write(cr, uid, final_product_todo, vals)
700 produced_products = {}
701 for produced_product in production.move_created_ids2:
702 if produced_product.scrapped:
704 if not produced_products.get(produced_product.product_id.id, False):
705 produced_products[produced_product.product_id.id] = 0
706 produced_products[produced_product.product_id.id] += produced_product.product_qty
708 for produce_product in production.move_created_ids:
709 produced_qty = produced_products.get(produce_product.product_id.id, 0)
710 rest_qty = production.product_qty - produced_qty
711 if rest_qty <= production_qty:
712 production_qty = rest_qty
714 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
716 for raw_product in production.move_lines2:
718 parent_move_ids = [x.id for x in raw_product.move_history_ids]
719 for final_product in production.move_created_ids2:
720 if final_product.id not in parent_move_ids:
721 new_parent_ids.append(final_product.id)
722 for new_parent_id in new_parent_ids:
723 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
725 wf_service = netsvc.LocalService("workflow")
726 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
729 def _costs_generate(self, cr, uid, production):
730 """ Calculates total costs at the end of the production.
731 @param production: Id of production order.
732 @return: Calculated amount.
735 analytic_line_obj = self.pool.get('account.analytic.line')
736 for wc_line in production.workcenter_lines:
737 wc = wc_line.workcenter_id
738 if wc.costs_journal_id and wc.costs_general_account_id:
739 value = wc_line.hour * wc.costs_hour
740 account = wc.costs_hour_account_id.id
741 if value and account:
743 analytic_line_obj.create(cr, uid, {
744 'name': wc_line.name + ' (H)',
746 'account_id': account,
747 'general_account_id': wc.costs_general_account_id.id,
748 'journal_id': wc.costs_journal_id.id,
751 if wc.costs_journal_id and wc.costs_general_account_id:
752 value = wc_line.cycle * wc.costs_cycle
753 account = wc.costs_cycle_account_id.id
754 if value and account:
756 analytic_line_obj.create(cr, uid, {
757 'name': wc_line.name+' (C)',
759 'account_id': account,
760 'general_account_id': wc.costs_general_account_id.id,
761 'journal_id': wc.costs_journal_id.id,
766 def action_in_production(self, cr, uid, ids):
767 """ Changes state to In Production and writes starting date.
770 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
773 def test_if_product(self, cr, uid, ids):
775 @return: True or False
778 for production in self.browse(cr, uid, ids):
779 if not production.product_lines:
780 if not self.action_compute(cr, uid, [production.id]):
784 def _get_auto_picking(self, cr, uid, production):
787 def action_confirm(self, cr, uid, ids):
788 """ Confirms production order.
789 @return: Newly generated picking Id.
793 seq_obj = self.pool.get('ir.sequence')
794 pick_obj = self.pool.get('stock.picking')
795 move_obj = self.pool.get('stock.move')
796 proc_obj = self.pool.get('procurement.order')
797 wf_service = netsvc.LocalService("workflow")
798 for production in self.browse(cr, uid, ids):
799 if not production.product_lines:
800 self.action_compute(cr, uid, [production.id])
801 production = self.browse(cr, uid, [production.id])[0]
803 pick_type = 'internal'
805 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
806 routing_loc = production.bom_id.routing_id.location_id
807 if routing_loc.usage <> 'internal':
809 address_id = routing_loc.address_id and routing_loc.address_id.id or False
810 routing_loc = routing_loc.id
811 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
812 picking_id = pick_obj.create(cr, uid, {
814 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
818 'address_id': address_id,
819 'auto_picking': self._get_auto_picking(cr, uid, production),
820 'company_id': production.company_id.id,
823 source = production.product_id.product_tmpl_id.property_stock_production.id
825 'name':'PROD:' + production.name,
826 'date': production.date_planned,
827 'product_id': production.product_id.id,
828 'product_qty': production.product_qty,
829 'product_uom': production.product_uom.id,
830 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
831 'product_uos': production.product_uos and production.product_uos.id or False,
832 'location_id': source,
833 'location_dest_id': production.location_dest_id.id,
834 'move_dest_id': production.move_prod_id.id,
836 'company_id': production.company_id.id,
838 res_final_id = move_obj.create(cr, uid, data)
840 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
842 for line in production.product_lines:
844 newdate = production.date_planned
845 if line.product_id.type in ('product', 'consu'):
846 res_dest_id = move_obj.create(cr, uid, {
847 'name':'PROD:' + production.name,
848 'date': production.date_planned,
849 'product_id': line.product_id.id,
850 'product_qty': line.product_qty,
851 'product_uom': line.product_uom.id,
852 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
853 'product_uos': line.product_uos and line.product_uos.id or False,
854 'location_id': routing_loc or production.location_src_id.id,
855 'location_dest_id': source,
856 'move_dest_id': res_final_id,
858 'company_id': production.company_id.id,
860 moves.append(res_dest_id)
861 move_id = move_obj.create(cr, uid, {
862 'name':'PROD:' + production.name,
863 'picking_id':picking_id,
864 'product_id': line.product_id.id,
865 'product_qty': line.product_qty,
866 'product_uom': line.product_uom.id,
867 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
868 'product_uos': line.product_uos and line.product_uos.id or False,
870 'move_dest_id': res_dest_id,
871 'location_id': production.location_src_id.id,
872 'location_dest_id': routing_loc or production.location_src_id.id,
874 'company_id': production.company_id.id,
876 proc_id = proc_obj.create(cr, uid, {
877 'name': (production.origin or '').split(':')[0] + ':' + production.name,
878 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
879 'date_planned': newdate,
880 'product_id': line.product_id.id,
881 'product_qty': line.product_qty,
882 'product_uom': line.product_uom.id,
883 'product_uos_qty': line.product_uos and line.product_qty or False,
884 'product_uos': line.product_uos and line.product_uos.id or False,
885 'location_id': production.location_src_id.id,
886 'procure_method': line.product_id.procure_method,
888 'company_id': production.company_id.id,
890 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
891 proc_ids.append(proc_id)
892 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
893 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
894 message = _("Manufacturing order '%s' is scheduled for the %s.") % (
896 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
898 self.log(cr, uid, production.id, message)
901 def force_production(self, cr, uid, ids, *args):
902 """ Assigns products.
903 @param *args: Arguments
906 pick_obj = self.pool.get('stock.picking')
907 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
912 class mrp_production_workcenter_line(osv.osv):
913 _name = 'mrp.production.workcenter.line'
914 _description = 'Work Order'
918 'name': fields.char('Work Order', size=64, required=True),
919 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
920 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
921 'hour': fields.float('Nbr of hours', digits=(16,2)),
922 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
923 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
926 'sequence': lambda *a: 1,
927 'hour': lambda *a: 0,
928 'cycle': lambda *a: 0,
930 mrp_production_workcenter_line()
932 class mrp_production_product_line(osv.osv):
933 _name = 'mrp.production.product.line'
934 _description = 'Production Scheduled Product'
936 'name': fields.char('Name', size=64, required=True),
937 'product_id': fields.many2one('product.product', 'Product', required=True),
938 'product_qty': fields.float('Product Qty', required=True),
939 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
940 'product_uos_qty': fields.float('Product UOS Qty'),
941 'product_uos': fields.many2one('product.uom', 'Product UOS'),
942 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
944 mrp_production_product_line()
946 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: