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 true, 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=None):
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
137 bom_obj = self.pool.get('mrp.bom')
138 bom_id = context and context.get('active_id', False) or False
139 cr.execute('select id from mrp_bom')
140 if all(bom_id != r[0] for r in cr.fetchall()):
143 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
144 for bom in self.browse(cr, uid, ids, context=context):
145 if (bom_parent) or (bom.id == bom_id):
146 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
151 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
152 if (bom.type=='phantom' or ok):
153 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
155 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
156 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
160 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
161 """ Sets particular method for the selected bom type.
162 @param field_name: Name of the field
163 @param arg: User defined argument
164 @return: Dictionary of values
166 res = dict(map(lambda x: (x,''), ids))
169 for line in self.browse(cr, uid, ids, context=context):
170 if line.type == 'phantom' and not line.bom_id:
173 if line.bom_lines or line.type == 'phantom':
175 if line.product_id.supply_method == 'produce':
176 if line.product_id.procure_method == 'make_to_stock':
177 res[line.id] = 'stock'
179 res[line.id] = 'order'
183 'name': fields.char('Name', size=64, required=True),
184 'code': fields.char('Reference', size=16),
185 '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."),
186 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
187 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
188 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
189 "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."),
190 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
191 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
192 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
193 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
194 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
195 'product_id': fields.many2one('product.product', 'Product', required=True),
196 'product_uos_qty': fields.float('Product UOS Qty'),
197 '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."),
198 'product_qty': fields.float('Product Qty', required=True),
199 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
200 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
201 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
202 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
203 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
204 '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."),
205 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
206 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
207 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
208 'company_id': fields.many2one('res.company','Company',required=True),
211 'active': lambda *a: 1,
212 'product_efficiency': lambda *a: 1.0,
213 'product_qty': lambda *a: 1.0,
214 'product_rounding': lambda *a: 0.0,
215 'type': lambda *a: 'normal',
216 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
220 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
221 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
224 def _check_recursion(self, cr, uid, ids, context=None):
229 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
230 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
236 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
240 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
241 """ Changes UoM and name if product_id changes.
242 @param name: Name of the field
243 @param product_id: Changed product_id
244 @return: Dictionary of changed values
247 prod = self.pool.get('product.product').browse(cr, uid, [product_id], context=context)[0]
248 v = {'product_uom': prod.uom_id.id}
250 v['name'] = prod.name
254 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
255 """ Finds BoM for particular product and product uom.
256 @param product_id: Selected product.
257 @param product_uom: Unit of measure of a product.
258 @param properties: List of related properties.
259 @return: False or BoM id.
261 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
262 ids = map(lambda x: x[0], cr.fetchall())
265 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
267 for prop_id in bom.property_ids:
268 if prop_id.id in properties:
270 if (prop > max_prop) or ((max_prop == 0) and not result):
275 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
276 """ Finds Products and Workcenters for related BoM for manufacturing order.
277 @param bom: BoM of particular product.
278 @param factor: Factor of product UoM.
279 @param properties: A List of properties Ids.
280 @param addthis: If BoM found then True else False.
281 @param level: Depth level to find BoM lines starts from 10.
282 @return: result: List of dictionaries containing product details.
283 result2: List of dictionaries containing workcenter details.
285 factor = factor / (bom.product_efficiency or 1.0)
286 factor = rounding(factor, bom.product_rounding)
287 if factor < bom.product_rounding:
288 factor = bom.product_rounding
292 if bom.type == 'phantom' and not bom.bom_lines:
293 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
295 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
296 result = result + res[0]
297 result2 = result2 + res[1]
302 if addthis and not bom.bom_lines:
305 'name': bom.product_id.name,
306 'product_id': bom.product_id.id,
307 'product_qty': bom.product_qty * factor,
308 'product_uom': bom.product_uom.id,
309 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
310 'product_uos': bom.product_uos and bom.product_uos.id or False,
313 for wc_use in bom.routing_id.workcenter_lines:
314 wc = wc_use.workcenter_id
315 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
316 mult = (d + (m and 1.0 or 0.0))
317 cycle = mult * wc_use.cycle_nbr
319 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
320 'workcenter_id': wc.id,
321 'sequence': level+(wc_use.sequence or 0),
323 '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)),
325 for bom2 in bom.bom_lines:
326 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
327 result = result + res[0]
328 result2 = result2 + res[1]
329 return result, result2
333 class mrp_bom_revision(osv.osv):
334 _name = 'mrp.bom.revision'
335 _description = 'Bill of Material Revision'
337 'name': fields.char('Modification name', size=64, required=True),
338 'description': fields.text('Description'),
339 'date': fields.date('Modification Date'),
340 'indice': fields.char('Revision', size=16),
341 'last_indice': fields.char('last indice', size=64),
342 'author_id': fields.many2one('res.users', 'Author'),
343 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
347 'author_id': lambda x, y, z, c: z,
348 'date': lambda *a: time.strftime('%Y-%m-%d'),
356 return round(f / r) * r
358 class mrp_production(osv.osv):
360 Production Orders / Manufacturing Orders
362 _name = 'mrp.production'
363 _description = 'Manufacturing Order'
364 _date_name = 'date_planned'
366 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
367 """ Calculates total hours and total no. of cycles for a production order.
368 @param prop: Name of field.
370 @return: Dictionary of values.
375 for prod in self.browse(cr, uid, ids, context=context):
380 for wc in prod.workcenter_lines:
381 result[prod.id]['hour_total'] += wc.hour
382 result[prod.id]['cycle_total'] += wc.cycle
385 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context=None):
386 """ Finds production end date.
387 @param prop: Name of field.
389 @return: Dictionary of values.
394 for prod in self.browse(cr, uid, ids, context=context):
395 result[prod.id] = prod.date_planned
398 def _production_date(self, cr, uid, ids, prop, unknow_none, context=None):
399 """ Finds production planned date.
400 @param prop: Name of field.
402 @return: Dictionary of values.
407 for prod in self.browse(cr, uid, ids, context=context):
408 result[prod.id] = prod.date_planned[:10]
412 'name': fields.char('Reference', size=64, required=True),
413 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
414 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
416 'product_id': fields.many2one('product.product', 'Product', required=True, ),
417 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
418 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
419 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
420 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
422 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
423 help="Location where the system will look for components."),
424 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
425 help="Location where the system will stock the finished products."),
427 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
428 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
429 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
430 'date_start': fields.datetime('Start Date'),
431 'date_finished': fields.datetime('End Date'),
433 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
434 '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."),
436 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
437 help="This is the internal picking list that brings the finished product to the production plan"),
438 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
439 '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)]}),
440 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
441 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
442 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
443 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
444 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
445 '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,
446 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\'.\
447 \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\'.'),
448 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
449 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
450 'company_id': fields.many2one('res.company','Company',required=True),
453 'priority': lambda *a: '1',
454 'state': lambda *a: 'draft',
455 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
456 'product_qty': lambda *a: 1.0,
457 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
458 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
460 _order = 'priority desc, date_planned asc';
462 def _check_qty(self, cr, uid, ids, context=None):
465 orders = self.browse(cr, uid, ids, context=context)
467 if order.product_qty <= 0:
472 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
475 def unlink(self, cr, uid, ids, context=None):
476 productions = self.read(cr, uid, ids, ['state'])
478 for s in productions:
479 if s['state'] in ['draft','cancel']:
480 unlink_ids.append(s['id'])
482 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!') % s['state'])
483 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
485 def copy(self, cr, uid, id, default=None, context=None):
489 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
492 'move_created_ids' : [],
493 'move_created_ids2' : [],
494 'product_lines' : [],
497 return super(mrp_production, self).copy(cr, uid, id, default, context)
499 def location_id_change(self, cr, uid, ids, src, dest, context=None):
500 """ Changes destination location if source location is changed.
501 @param src: Source location id.
502 @param dest: Destination location id.
503 @return: Dictionary of values.
508 return {'value': {'location_dest_id': src}}
511 def product_id_change(self, cr, uid, ids, product_id, context=None):
512 """ Finds UoM of changed product.
513 @param product_id: Id of changed product.
514 @return: Dictionary of values.
518 'product_uom': False,
524 bom_obj = self.pool.get('mrp.bom')
525 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
526 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
529 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
530 routing_id = bom_point.routing_id.id or False
532 'product_uom': product.uom_id and product.uom_id.id or False,
534 'routing_id': routing_id
536 return {'value': result}
538 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
539 """ Finds routing for changed BoM.
540 @param product: Id of product.
541 @return: Dictionary of values.
549 bom_pool = self.pool.get('mrp.bom')
550 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
551 routing_id = bom_point.routing_id.id or False
553 'routing_id': routing_id
555 return {'value': result}
557 def action_picking_except(self, cr, uid, ids):
558 """ Changes the state to Exception.
561 self.write(cr, uid, ids, {'state': 'picking_except'})
564 def action_compute(self, cr, uid, ids, properties=[]):
565 """ Computes bills of material of a product.
566 @param properties: List containing dictionaries of properties.
567 @return: No. of products.
570 bom_obj = self.pool.get('mrp.bom')
571 prod_line_obj = self.pool.get('mrp.production.product.line')
572 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
573 for production in self.browse(cr, uid, ids):
574 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
575 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
576 bom_point = production.bom_id
577 bom_id = production.bom_id.id
579 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
581 bom_point = bom_obj.browse(cr, uid, bom_id)
582 routing_id = bom_point.routing_id.id or False
583 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
586 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
588 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
589 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
593 line['production_id'] = production.id
594 prod_line_obj.create(cr, uid, line)
595 for line in results2:
596 line['production_id'] = production.id
597 workcenter_line_obj.create(cr, uid, line)
600 def action_cancel(self, cr, uid, ids):
601 """ Cancels the production order and related stock moves.
604 move_obj = self.pool.get('stock.move')
605 for production in self.browse(cr, uid, ids):
606 if production.move_created_ids:
607 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
608 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
609 self.write(cr, uid, ids, {'state': 'cancel'})
612 def action_ready(self, cr, uid, ids):
613 """ Changes the production state to Ready and location id of stock move.
616 move_obj = self.pool.get('stock.move')
617 self.write(cr, uid, ids, {'state': 'ready'})
619 for (production_id,name) in self.name_get(cr, uid, ids):
620 production = self.browse(cr, uid, production_id)
621 if production.move_prod_id:
622 move_obj.write(cr, uid, [production.move_prod_id.id],
623 {'location_id': production.location_dest_id.id})
625 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
626 self.log(cr, uid, production_id, message)
629 def action_production_end(self, cr, uid, ids):
630 """ Changes production state to Finish and writes finished date.
633 for production in self.browse(cr, uid, ids):
634 self._costs_generate(cr, uid, production)
635 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
637 def test_production_done(self, cr, uid, ids):
638 """ Tests whether production is done or not.
639 @return: True or False
642 for production in self.browse(cr, uid, ids):
643 if production.move_lines:
646 if production.move_created_ids:
650 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
651 """ To produce final product based on production mode (consume/consume&produce).
652 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
653 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
654 and stock move lines of final product will be also done/produced.
655 @param production_id: the ID of mrp.production object
656 @param production_qty: specify qty to produce
657 @param production_mode: specify production mode (consume/consume&produce).
662 stock_mov_obj = self.pool.get('stock.move')
663 production = self.browse(cr, uid, production_id, context=context)
665 final_product_todo = []
668 if production_mode == 'consume_produce':
669 produced_qty = production_qty
671 for produced_product in production.move_created_ids2:
672 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
674 produced_qty += produced_product.product_qty
676 if production_mode in ['consume','consume_produce']:
677 consumed_products = {}
679 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
681 for consumed_product in production.move_lines2:
682 consumed = consumed_product.product_qty
683 if consumed_product.scrapped:
685 if not consumed_products.get(consumed_product.product_id.id, False):
686 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
687 check[consumed_product.product_id.id] = 0
688 for f in production.product_lines:
689 if f.product_id.id == consumed_product.product_id.id:
690 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
691 check[consumed_product.product_id.id] += consumed_product.product_qty
692 consumed = check[consumed_product.product_id.id]
693 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
694 consumed_products[consumed_product.product_id.id] = rest_consumed
696 for raw_product in production.move_lines:
697 for f in production.product_lines:
698 if f.product_id.id == raw_product.product_id.id:
699 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
700 if consumed_qty == 0:
701 consumed_qty = production_qty * f.product_qty / production.product_qty
703 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
705 if production_mode == 'consume_produce':
706 # To produce remaining qty of final product
707 vals = {'state':'confirmed'}
708 final_product_todo = [x.id for x in production.move_created_ids]
709 stock_mov_obj.write(cr, uid, final_product_todo, vals)
710 produced_products = {}
711 for produced_product in production.move_created_ids2:
712 if produced_product.scrapped:
714 if not produced_products.get(produced_product.product_id.id, False):
715 produced_products[produced_product.product_id.id] = 0
716 produced_products[produced_product.product_id.id] += produced_product.product_qty
718 for produce_product in production.move_created_ids:
719 produced_qty = produced_products.get(produce_product.product_id.id, 0)
720 rest_qty = production.product_qty - produced_qty
721 if rest_qty <= production_qty:
722 production_qty = rest_qty
724 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
726 for raw_product in production.move_lines2:
728 parent_move_ids = [x.id for x in raw_product.move_history_ids]
729 for final_product in production.move_created_ids2:
730 if final_product.id not in parent_move_ids:
731 new_parent_ids.append(final_product.id)
732 for new_parent_id in new_parent_ids:
733 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
735 wf_service = netsvc.LocalService("workflow")
736 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
739 def _costs_generate(self, cr, uid, production):
740 """ Calculates total costs at the end of the production.
741 @param production: Id of production order.
742 @return: Calculated amount.
745 analytic_line_obj = self.pool.get('account.analytic.line')
746 for wc_line in production.workcenter_lines:
747 wc = wc_line.workcenter_id
748 if wc.costs_journal_id and wc.costs_general_account_id:
749 value = wc_line.hour * wc.costs_hour
750 account = wc.costs_hour_account_id.id
751 if value and account:
753 analytic_line_obj.create(cr, uid, {
754 'name': wc_line.name + ' (H)',
756 'account_id': account,
757 'general_account_id': wc.costs_general_account_id.id,
758 'journal_id': wc.costs_journal_id.id,
761 if wc.costs_journal_id and wc.costs_general_account_id:
762 value = wc_line.cycle * wc.costs_cycle
763 account = wc.costs_cycle_account_id.id
764 if value and account:
766 analytic_line_obj.create(cr, uid, {
767 'name': wc_line.name+' (C)',
769 'account_id': account,
770 'general_account_id': wc.costs_general_account_id.id,
771 'journal_id': wc.costs_journal_id.id,
776 def action_in_production(self, cr, uid, ids):
777 """ Changes state to In Production and writes starting date.
780 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
783 def test_if_product(self, cr, uid, ids):
785 @return: True or False
788 for production in self.browse(cr, uid, ids):
789 if not production.product_lines:
790 if not self.action_compute(cr, uid, [production.id]):
794 def _get_auto_picking(self, cr, uid, production):
797 def action_confirm(self, cr, uid, ids):
798 """ Confirms production order.
799 @return: Newly generated picking Id.
803 seq_obj = self.pool.get('ir.sequence')
804 pick_obj = self.pool.get('stock.picking')
805 move_obj = self.pool.get('stock.move')
806 proc_obj = self.pool.get('procurement.order')
807 wf_service = netsvc.LocalService("workflow")
808 for production in self.browse(cr, uid, ids):
809 if not production.product_lines:
810 self.action_compute(cr, uid, [production.id])
811 production = self.browse(cr, uid, [production.id])[0]
813 pick_type = 'internal'
815 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
816 routing_loc = production.bom_id.routing_id.location_id
817 if routing_loc.usage <> 'internal':
819 address_id = routing_loc.address_id and routing_loc.address_id.id or False
820 routing_loc = routing_loc.id
821 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
822 picking_id = pick_obj.create(cr, uid, {
824 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
828 'address_id': address_id,
829 'auto_picking': self._get_auto_picking(cr, uid, production),
830 'company_id': production.company_id.id,
833 source = production.product_id.product_tmpl_id.property_stock_production.id
835 'name':'PROD:' + production.name,
836 'date': production.date_planned,
837 'product_id': production.product_id.id,
838 'product_qty': production.product_qty,
839 'product_uom': production.product_uom.id,
840 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
841 'product_uos': production.product_uos and production.product_uos.id or False,
842 'location_id': source,
843 'location_dest_id': production.location_dest_id.id,
844 'move_dest_id': production.move_prod_id.id,
846 'company_id': production.company_id.id,
848 res_final_id = move_obj.create(cr, uid, data)
850 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
852 for line in production.product_lines:
854 newdate = production.date_planned
855 if line.product_id.type in ('product', 'consu'):
856 res_dest_id = move_obj.create(cr, uid, {
857 'name':'PROD:' + production.name,
858 'date': production.date_planned,
859 'product_id': line.product_id.id,
860 'product_qty': line.product_qty,
861 'product_uom': line.product_uom.id,
862 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
863 'product_uos': line.product_uos and line.product_uos.id or False,
864 'location_id': routing_loc or production.location_src_id.id,
865 'location_dest_id': source,
866 'move_dest_id': res_final_id,
868 'company_id': production.company_id.id,
870 moves.append(res_dest_id)
871 move_id = move_obj.create(cr, uid, {
872 'name':'PROD:' + production.name,
873 'picking_id':picking_id,
874 'product_id': line.product_id.id,
875 'product_qty': line.product_qty,
876 'product_uom': line.product_uom.id,
877 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
878 'product_uos': line.product_uos and line.product_uos.id or False,
880 'move_dest_id': res_dest_id,
881 'location_id': production.location_src_id.id,
882 'location_dest_id': routing_loc or production.location_src_id.id,
884 'company_id': production.company_id.id,
886 proc_id = proc_obj.create(cr, uid, {
887 'name': (production.origin or '').split(':')[0] + ':' + production.name,
888 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
889 'date_planned': newdate,
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_qty or False,
894 'product_uos': line.product_uos and line.product_uos.id or False,
895 'location_id': production.location_src_id.id,
896 'procure_method': line.product_id.procure_method,
898 'company_id': production.company_id.id,
900 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
901 proc_ids.append(proc_id)
902 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
903 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
904 message = _("Manufacturing order '%s' is scheduled for the %s.") % (
906 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
908 self.log(cr, uid, production.id, message)
911 def force_production(self, cr, uid, ids, *args):
912 """ Assigns products.
913 @param *args: Arguments
916 pick_obj = self.pool.get('stock.picking')
917 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
922 class mrp_production_workcenter_line(osv.osv):
923 _name = 'mrp.production.workcenter.line'
924 _description = 'Work Order'
928 'name': fields.char('Work Order', size=64, required=True),
929 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
930 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
931 'hour': fields.float('Nbr of hours', digits=(16,2)),
932 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
933 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
936 'sequence': lambda *a: 1,
937 'hour': lambda *a: 0,
938 'cycle': lambda *a: 0,
940 mrp_production_workcenter_line()
942 class mrp_production_product_line(osv.osv):
943 _name = 'mrp.production.product.line'
944 _description = 'Production Scheduled Product'
946 'name': fields.char('Name', size=64, required=True),
947 'product_id': fields.many2one('product.product', 'Product', required=True),
948 'product_qty': fields.float('Product Qty', required=True),
949 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
950 'product_uos_qty': fields.float('Product UOS Qty'),
951 'product_uos': fields.many2one('product.uom', 'Product UOS'),
952 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
954 mrp_production_product_line()
956 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: