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 Work Center. Explain here what's a cycle according to this Work Center."),
43 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this Work Center can do in parallel. If this Work Center represents a team of 5 workers, the capacity per cycle is 5."),
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 Work Center 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 Work Center per cycle."),
51 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
52 help="Complete this only if you want automatic analytic accounting entries on production orders."),
53 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
54 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
55 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
56 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to track easily your production costs in the analytic accounting."),
59 'capacity_per_cycle': 1.0,
60 'resource_type': 'material',
63 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
69 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
70 value = {'costs_hour': cost.standard_price}
71 return {'value': value}
76 class mrp_routing(osv.osv):
78 For specifying the routings of Work Centers.
81 _description = 'Routing'
83 'name': fields.char('Name', size=64, required=True),
84 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
85 'code': fields.char('Code', size=8),
87 'note': fields.text('Description'),
88 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
90 'location_id': fields.many2one('stock.location', 'Production Location',
91 help="Keep empty if you produce at the location where the finished products are needed." \
92 "Set a location if you produce at a fixed location. This can be a partner location " \
93 "if you subcontract the manufacturing operations."
95 'company_id': fields.many2one('res.company', 'Company'),
98 'active': lambda *a: 1,
99 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
103 class mrp_routing_workcenter(osv.osv):
105 Defines working cycles and hours of a Work Center using routings.
107 _name = 'mrp.routing.workcenter'
108 _description = 'Work Center Usage'
110 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
111 'name': fields.char('Name', size=64, required=True),
112 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
113 'cycle_nbr': fields.float('Number of Cycles', required=True,
114 help="Number of iterations this work center has to do in the specified operation of the routing."),
115 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for this Work Center to achieve the operation of the specified routing."),
116 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
117 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
118 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
119 'note': fields.text('Description'),
120 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
123 'cycle_nbr': lambda *a: 1.0,
124 'hour_nbr': lambda *a: 0.0,
126 mrp_routing_workcenter()
128 class mrp_bom(osv.osv):
130 Defines bills of material for a product.
133 _description = 'Bill of Material'
135 def _child_compute(self, cr, uid, ids, name, arg, context=None):
137 @param self: The object pointer
138 @param cr: The current row, from the database cursor,
139 @param uid: The current user ID for security checks
140 @param ids: List of selected IDs
141 @param name: Name of the field
142 @param arg: User defined argument
143 @param context: A standard dictionary for contextual values
144 @return: Dictionary of values
149 bom_obj = self.pool.get('mrp.bom')
150 bom_id = context and context.get('active_id', False) or False
151 cr.execute('select id from mrp_bom')
152 if all(bom_id != r[0] for r in cr.fetchall()):
155 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
156 for bom in self.browse(cr, uid, ids, context=context):
157 if (bom_parent) or (bom.id == bom_id):
158 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
163 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
164 if (bom.type=='phantom' or ok):
165 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
167 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
168 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
172 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
173 """ Sets particular method for the selected bom type.
174 @param field_name: Name of the field
175 @param arg: User defined argument
176 @return: Dictionary of values
178 res = dict(map(lambda x: (x,''), ids))
179 for line in self.browse(cr, uid, ids, context=context):
180 if line.type == 'phantom' and not line.bom_id:
183 if line.bom_lines or line.type == 'phantom':
185 if line.product_id.supply_method == 'produce':
186 if line.product_id.procure_method == 'make_to_stock':
187 res[line.id] = 'stock'
189 res[line.id] = 'order'
193 'name': fields.char('Name', size=64, required=True),
194 'code': fields.char('Reference', size=16),
195 '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."),
196 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
197 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
198 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
199 "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."),
200 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
201 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
202 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
203 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
204 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
205 'product_id': fields.many2one('product.product', 'Product', required=True),
206 'product_uos_qty': fields.float('Product UOS Qty'),
207 '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."),
208 'product_qty': fields.float('Product Qty', required=True),
209 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
210 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
211 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
212 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
213 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
214 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
215 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
216 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
217 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
218 'company_id': fields.many2one('res.company','Company',required=True),
221 'active': lambda *a: 1,
222 'product_efficiency': lambda *a: 1.0,
223 'product_qty': lambda *a: 1.0,
224 'product_rounding': lambda *a: 0.0,
225 'type': lambda *a: 'normal',
226 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
230 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
231 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
234 def _check_recursion(self, cr, uid, ids, context=None):
237 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
238 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
244 def _check_product(self, cr, uid, ids, context=None):
246 boms = self.browse(cr, uid, ids, context=context)
250 if bom.product_id.id in all_prod:
252 all_prod.append(bom.product_id.id)
253 lines = bom.bom_lines
255 newboms = [a for a in lines if a not in boms]
256 res = res and check_bom(newboms)
258 return check_bom(boms)
261 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id']),
262 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
265 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
266 """ Changes UoM and name if product_id changes.
267 @param name: Name of the field
268 @param product_id: Changed product_id
269 @return: Dictionary of changed values
273 context['lang'] = self.pool.get('res.users').browse(cr,uid,uid).context_lang
276 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
277 v = {'product_uom': prod.uom_id.id}
279 v['name'] = prod.name
283 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
284 """ Finds BoM for particular product and product uom.
285 @param product_id: Selected product.
286 @param product_uom: Unit of measure of a product.
287 @param properties: List of related properties.
288 @return: False or BoM id.
290 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
291 ids = map(lambda x: x[0], cr.fetchall())
294 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
296 for prop_id in bom.property_ids:
297 if prop_id.id in properties:
299 if (prop > max_prop) or ((max_prop == 0) and not result):
304 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
305 """ Finds Products and Work Centers for related BoM for manufacturing order.
306 @param bom: BoM of particular product.
307 @param factor: Factor of product UoM.
308 @param properties: A List of properties Ids.
309 @param addthis: If BoM found then True else False.
310 @param level: Depth level to find BoM lines starts from 10.
311 @return: result: List of dictionaries containing product details.
312 result2: List of dictionaries containing Work Center details.
314 factor = factor / (bom.product_efficiency or 1.0)
315 factor = rounding(factor, bom.product_rounding)
316 if factor < bom.product_rounding:
317 factor = bom.product_rounding
321 if bom.type == 'phantom' and not bom.bom_lines:
322 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
324 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
325 result = result + res[0]
326 result2 = result2 + res[1]
331 if addthis and not bom.bom_lines:
334 'name': bom.product_id.name,
335 'product_id': bom.product_id.id,
336 'product_qty': bom.product_qty * factor,
337 'product_uom': bom.product_uom.id,
338 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
339 'product_uos': bom.product_uos and bom.product_uos.id or False,
342 for wc_use in bom.routing_id.workcenter_lines:
343 wc = wc_use.workcenter_id
344 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
345 mult = (d + (m and 1.0 or 0.0))
346 cycle = mult * wc_use.cycle_nbr
348 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
349 'workcenter_id': wc.id,
350 'sequence': level+(wc_use.sequence or 0),
352 '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)),
354 for bom2 in bom.bom_lines:
355 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
356 result = result + res[0]
357 result2 = result2 + res[1]
358 return result, result2
360 def copy_data(self, cr, uid, id, default=None, context=None):
365 bom_data = self.read(cr, uid, id, [], context=context)
366 default.update({'name': bom_data['name'] + ' ' + _('Copy'), 'bom_id':False})
367 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
371 class mrp_bom_revision(osv.osv):
372 _name = 'mrp.bom.revision'
373 _description = 'Bill of Material Revision'
375 'name': fields.char('Modification name', size=64, required=True),
376 'description': fields.text('Description'),
377 'date': fields.date('Modification Date'),
378 'indice': fields.char('Revision', size=16),
379 'last_indice': fields.char('last indice', size=64),
380 'author_id': fields.many2one('res.users', 'Author'),
381 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
385 'author_id': lambda x, y, z, c: z,
386 'date': lambda *a: time.strftime('%Y-%m-%d'),
395 return math.ceil(f / r) * r
397 class mrp_production(osv.osv):
399 Production Orders / Manufacturing Orders
401 _name = 'mrp.production'
402 _description = 'Manufacturing Order'
403 _date_name = 'date_planned'
405 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
406 """ Calculates total hours and total no. of cycles for a production order.
407 @param prop: Name of field.
409 @return: Dictionary of values.
412 for prod in self.browse(cr, uid, ids, context=context):
417 for wc in prod.workcenter_lines:
418 result[prod.id]['hour_total'] += wc.hour
419 result[prod.id]['cycle_total'] += wc.cycle
422 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context=None):
423 """ Finds production end date.
424 @param prop: Name of field.
426 @return: Dictionary of values.
429 for prod in self.browse(cr, uid, ids, context=context):
430 result[prod.id] = prod.date_planned
433 def _production_date(self, cr, uid, ids, prop, unknow_none, context=None):
434 """ Finds production planned date.
435 @param prop: Name of field.
437 @return: Dictionary of values.
440 for prod in self.browse(cr, uid, ids, context=context):
441 result[prod.id] = prod.date_planned[:10]
445 'name': fields.char('Reference', size=64, required=True),
446 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
447 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
449 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft':[('readonly',False)]}),
450 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
451 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
452 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
453 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
455 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
456 readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will look for components."),
457 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
458 readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will stock the finished products."),
460 'date_planned_end': fields.function(_production_date_end, type='date', string='Scheduled End Date'),
461 'date_planned_date': fields.function(_production_date, type='date', string='Scheduled Date'),
462 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
463 'date_start': fields.datetime('Start Date', select=True),
464 'date_finished': fields.datetime('End Date', select=True),
466 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]}),
467 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]}, help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production plannification."),
469 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True, ondelete="restrict",
470 help="This is the internal picking list that brings the finished product to the production plan"),
471 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
472 '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)]}),
473 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
474 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
475 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
476 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
477 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
478 '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,
479 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\'.\
480 \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\'.'),
481 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
482 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
483 'company_id': fields.many2one('res.company','Company',required=True),
486 'priority': lambda *a: '1',
487 'state': lambda *a: 'draft',
488 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
489 'product_qty': lambda *a: 1.0,
490 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
491 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
493 _order = 'priority desc, date_planned asc';
495 def _check_qty(self, cr, uid, ids, context=None):
496 orders = self.browse(cr, uid, ids, context=context)
498 if order.product_qty <= 0:
503 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
506 def unlink(self, cr, uid, ids, context=None):
507 productions = self.read(cr, uid, ids, ['state'])
509 for s in productions:
510 if s['state'] in ['draft','cancel']:
511 unlink_ids.append(s['id'])
513 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!') % s['state'])
514 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
516 def copy(self, cr, uid, id, default=None, context=None):
520 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
523 'move_created_ids' : [],
524 'move_created_ids2' : [],
525 'product_lines' : [],
528 return super(mrp_production, self).copy(cr, uid, id, default, context)
530 def location_id_change(self, cr, uid, ids, src, dest, context=None):
531 """ Changes destination location if source location is changed.
532 @param src: Source location id.
533 @param dest: Destination location id.
534 @return: Dictionary of values.
539 return {'value': {'location_dest_id': src}}
542 def product_id_change(self, cr, uid, ids, product_id, context=None):
543 """ Finds UoM of changed product.
544 @param product_id: Id of changed product.
545 @return: Dictionary of values.
549 'product_uom': False,
553 bom_obj = self.pool.get('mrp.bom')
554 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
555 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
558 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
559 routing_id = bom_point.routing_id.id or False
561 'product_uom': product.uom_id and product.uom_id.id or False,
563 'routing_id': routing_id
565 return {'value': result}
567 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
568 """ Finds routing for changed BoM.
569 @param product: Id of product.
570 @return: Dictionary of values.
576 bom_pool = self.pool.get('mrp.bom')
577 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
578 routing_id = bom_point.routing_id.id or False
580 'routing_id': routing_id
582 return {'value': result}
584 def action_picking_except(self, cr, uid, ids):
585 """ Changes the state to Exception.
588 self.write(cr, uid, ids, {'state': 'picking_except'})
591 def action_compute(self, cr, uid, ids, properties=[]):
592 """ Computes bills of material of a product.
593 @param properties: List containing dictionaries of properties.
594 @return: No. of products.
597 bom_obj = self.pool.get('mrp.bom')
598 uom_obj = self.pool.get('product.uom')
599 prod_line_obj = self.pool.get('mrp.production.product.line')
600 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
601 for production in self.browse(cr, uid, ids):
602 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
603 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
604 bom_point = production.bom_id
605 bom_id = production.bom_id.id
607 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
609 bom_point = bom_obj.browse(cr, uid, bom_id)
610 routing_id = bom_point.routing_id.id or False
611 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
614 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
616 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
617 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
621 line['production_id'] = production.id
622 prod_line_obj.create(cr, uid, line)
623 for line in results2:
624 line['production_id'] = production.id
625 workcenter_line_obj.create(cr, uid, line)
628 def action_cancel(self, cr, uid, ids, context=None):
629 """ Cancels the production order and related stock moves.
634 move_obj = self.pool.get('stock.move')
635 for production in self.browse(cr, uid, ids, context=context):
636 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
637 raise osv.except_osv(
638 _('Could not cancel manufacturing order !'),
639 _('You must first cancel related internal picking attached to this manufacturing order.'))
640 if production.move_created_ids:
641 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
642 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
643 self.write(cr, uid, ids, {'state': 'cancel'})
646 def action_ready(self, cr, uid, ids):
647 """ Changes the production state to Ready and location id of stock move.
650 move_obj = self.pool.get('stock.move')
651 self.write(cr, uid, ids, {'state': 'ready'})
653 for (production_id,name) in self.name_get(cr, uid, ids):
654 production = self.browse(cr, uid, production_id)
655 if production.move_prod_id:
656 move_obj.write(cr, uid, [production.move_prod_id.id],
657 {'location_id': production.location_dest_id.id})
659 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
660 self.log(cr, uid, production_id, message)
663 def action_production_end(self, cr, uid, ids):
664 """ Changes production state to Finish and writes finished date.
667 for production in self.browse(cr, uid, ids):
668 self._costs_generate(cr, uid, production)
669 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
671 def test_production_done(self, cr, uid, ids):
672 """ Tests whether production is done or not.
673 @return: True or False
676 for production in self.browse(cr, uid, ids):
677 if production.move_lines:
680 if production.move_created_ids:
684 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
685 """ To produce final product based on production mode (consume/consume&produce).
686 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
687 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
688 and stock move lines of final product will be also done/produced.
689 @param production_id: the ID of mrp.production object
690 @param production_qty: specify qty to produce
691 @param production_mode: specify production mode (consume/consume&produce).
694 stock_mov_obj = self.pool.get('stock.move')
695 production = self.browse(cr, uid, production_id, context=context)
699 if production_mode == 'consume_produce':
700 produced_qty = production_qty
702 for produced_product in production.move_created_ids2:
703 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
705 produced_qty += produced_product.product_qty
707 if production_mode in ['consume','consume_produce']:
708 consumed_products = {}
710 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
712 for consumed_product in production.move_lines2:
713 consumed = consumed_product.product_qty
714 if consumed_product.scrapped:
716 if not consumed_products.get(consumed_product.product_id.id, False):
717 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
718 check[consumed_product.product_id.id] = 0
719 for f in production.product_lines:
720 if f.product_id.id == consumed_product.product_id.id:
721 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
722 check[consumed_product.product_id.id] += consumed_product.product_qty
723 consumed = check[consumed_product.product_id.id]
724 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
725 consumed_products[consumed_product.product_id.id] = rest_consumed
727 for raw_product in production.move_lines:
728 for f in production.product_lines:
729 if f.product_id.id == raw_product.product_id.id:
730 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
731 if consumed_qty == 0:
732 consumed_qty = production_qty * f.product_qty / production.product_qty
734 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, raw_product.location_id.id, context=context)
736 if production_mode == 'consume_produce':
737 # To produce remaining qty of final product
738 #vals = {'state':'confirmed'}
739 #final_product_todo = [x.id for x in production.move_created_ids]
740 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
741 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
742 produced_products = {}
743 for produced_product in production.move_created_ids2:
744 if produced_product.scrapped:
746 if not produced_products.get(produced_product.product_id.id, False):
747 produced_products[produced_product.product_id.id] = 0
748 produced_products[produced_product.product_id.id] += produced_product.product_qty
750 for produce_product in production.move_created_ids:
751 produced_qty = produced_products.get(produce_product.product_id.id, 0)
752 rest_qty = production.product_qty - produced_qty
753 if rest_qty <= production_qty:
754 production_qty = rest_qty
756 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
758 for raw_product in production.move_lines2:
760 parent_move_ids = [x.id for x in raw_product.move_history_ids]
761 for final_product in production.move_created_ids2:
762 if final_product.id not in parent_move_ids:
763 new_parent_ids.append(final_product.id)
764 for new_parent_id in new_parent_ids:
765 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
767 wf_service = netsvc.LocalService("workflow")
768 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
771 def _costs_generate(self, cr, uid, production):
772 """ Calculates total costs at the end of the production.
773 @param production: Id of production order.
774 @return: Calculated amount.
777 analytic_line_obj = self.pool.get('account.analytic.line')
778 for wc_line in production.workcenter_lines:
779 wc = wc_line.workcenter_id
780 if wc.costs_journal_id and wc.costs_general_account_id:
781 value = wc_line.hour * wc.costs_hour
782 account = wc.costs_hour_account_id.id
783 if value and account:
785 analytic_line_obj.create(cr, uid, {
786 'name': wc_line.name + ' (H)',
788 'account_id': account,
789 'general_account_id': wc.costs_general_account_id.id,
790 'journal_id': wc.costs_journal_id.id,
792 'product_id': wc.product_id.id,
793 'unit_amount': wc_line.hour,
794 'product_uom_id': wc.product_id.uom_id.id
796 if wc.costs_journal_id and wc.costs_general_account_id:
797 value = wc_line.cycle * wc.costs_cycle
798 account = wc.costs_cycle_account_id.id
799 if value and account:
801 analytic_line_obj.create(cr, uid, {
802 'name': wc_line.name+' (C)',
804 'account_id': account,
805 'general_account_id': wc.costs_general_account_id.id,
806 'journal_id': wc.costs_journal_id.id,
808 'product_id': wc.product_id.id,
809 'unit_amount': wc_line.cycle,
810 'product_uom_id': wc.product_id.uom_id.id
814 def action_in_production(self, cr, uid, ids):
815 """ Changes state to In Production and writes starting date.
818 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
821 def test_if_product(self, cr, uid, ids):
823 @return: True or False
826 for production in self.browse(cr, uid, ids):
827 if not production.product_lines:
828 if not self.action_compute(cr, uid, [production.id]):
832 def _get_auto_picking(self, cr, uid, production):
835 def action_confirm(self, cr, uid, ids):
836 """ Confirms production order.
837 @return: Newly generated picking Id.
841 seq_obj = self.pool.get('ir.sequence')
842 pick_obj = self.pool.get('stock.picking')
843 move_obj = self.pool.get('stock.move')
844 proc_obj = self.pool.get('procurement.order')
845 wf_service = netsvc.LocalService("workflow")
846 for production in self.browse(cr, uid, ids):
847 if not production.product_lines:
848 self.action_compute(cr, uid, [production.id])
849 production = self.browse(cr, uid, [production.id])[0]
851 pick_type = 'internal'
853 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
854 routing_loc = production.bom_id.routing_id.location_id
855 if routing_loc.usage <> 'internal':
857 address_id = routing_loc.address_id and routing_loc.address_id.id or False
858 routing_loc = routing_loc.id
859 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
860 picking_id = pick_obj.create(cr, uid, {
862 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
866 'address_id': address_id,
867 'auto_picking': self._get_auto_picking(cr, uid, production),
868 'company_id': production.company_id.id,
871 source = production.product_id.product_tmpl_id.property_stock_production.id
873 'name':'PROD:' + production.name,
874 'date': production.date_planned,
875 'product_id': production.product_id.id,
876 'product_qty': production.product_qty,
877 'product_uom': production.product_uom.id,
878 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
879 'product_uos': production.product_uos and production.product_uos.id or False,
880 'location_id': source,
881 'location_dest_id': production.location_dest_id.id,
882 'move_dest_id': production.move_prod_id.id,
884 'company_id': production.company_id.id,
886 res_final_id = move_obj.create(cr, uid, data)
888 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
890 for line in production.product_lines:
892 newdate = production.date_planned
893 if line.product_id.type in ('product', 'consu'):
894 res_dest_id = move_obj.create(cr, uid, {
895 'name':'PROD:' + production.name,
896 'date': production.date_planned,
897 'product_id': line.product_id.id,
898 'product_qty': line.product_qty,
899 'product_uom': line.product_uom.id,
900 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
901 'product_uos': line.product_uos and line.product_uos.id or False,
902 'location_id': routing_loc or production.location_src_id.id,
903 'location_dest_id': source,
904 'move_dest_id': res_final_id,
906 'company_id': production.company_id.id,
908 moves.append(res_dest_id)
909 move_id = move_obj.create(cr, uid, {
910 'name':'PROD:' + production.name,
911 'picking_id':picking_id,
912 'product_id': line.product_id.id,
913 'product_qty': line.product_qty,
914 'product_uom': line.product_uom.id,
915 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
916 'product_uos': line.product_uos and line.product_uos.id or False,
918 'move_dest_id': res_dest_id,
919 'location_id': production.location_src_id.id,
920 'location_dest_id': routing_loc or production.location_src_id.id,
922 'company_id': production.company_id.id,
924 proc_id = proc_obj.create(cr, uid, {
925 'name': (production.origin or '').split(':')[0] + ':' + production.name,
926 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
927 'date_planned': newdate,
928 'product_id': line.product_id.id,
929 'product_qty': line.product_qty,
930 'product_uom': line.product_uom.id,
931 'product_uos_qty': line.product_uos and line.product_qty or False,
932 'product_uos': line.product_uos and line.product_uos.id or False,
933 'location_id': production.location_src_id.id,
934 'procure_method': line.product_id.procure_method,
936 'company_id': production.company_id.id,
938 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
939 proc_ids.append(proc_id)
940 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
941 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
942 message = _("Manufacturing order '%s' is scheduled for the %s.") % (
944 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
946 self.log(cr, uid, production.id, message)
949 def force_production(self, cr, uid, ids, *args):
950 """ Assigns products.
951 @param *args: Arguments
954 pick_obj = self.pool.get('stock.picking')
955 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
960 class mrp_production_workcenter_line(osv.osv):
961 _name = 'mrp.production.workcenter.line'
962 _description = 'Work Order'
966 'name': fields.char('Work Order', size=64, required=True),
967 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
968 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
969 'hour': fields.float('Nbr of hours', digits=(16,2)),
970 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
971 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
974 'sequence': lambda *a: 1,
975 'hour': lambda *a: 0,
976 'cycle': lambda *a: 0,
978 mrp_production_workcenter_line()
980 class mrp_production_product_line(osv.osv):
981 _name = 'mrp.production.product.line'
982 _description = 'Production Scheduled Product'
984 'name': fields.char('Name', size=64, required=True),
985 'product_id': fields.many2one('product.product', 'Product', required=True),
986 'product_qty': fields.float('Product Qty', required=True),
987 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
988 'product_uos_qty': fields.float('Product UOS Qty'),
989 'product_uos': fields.many2one('product.uom', 'Product UOS'),
990 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
992 mrp_production_product_line()
994 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: