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 _
29 #----------------------------------------------------------
31 #----------------------------------------------------------
32 # capacity_hour : capacity per hour. default: 1.0.
33 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
34 # unit_per_cycle : how many units are produced for one cycle
36 class mrp_workcenter(osv.osv):
37 _name = 'mrp.workcenter'
38 _description = 'Work Center'
39 _inherits = {'resource.resource':"resource_id"}
41 'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
42 '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."),
43 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
44 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
45 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
46 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Workcenter per hour."),
47 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
48 help="Complete this only if you want automatic analytic accounting entries on production orders."),
49 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Workcenter per cycle."),
50 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
51 help="Complete this only if you want automatic analytic accounting entries on production orders."),
52 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
53 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
54 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
57 'capacity_per_cycle': 1.0,
58 'resource_type': 'material',
63 class mrp_routing(osv.osv):
65 For specifying the routings of workcenters.
68 _description = 'Routing'
70 'name': fields.char('Name', size=64, required=True),
71 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the routing without removing it."),
72 'code': fields.char('Code', size=8),
74 'note': fields.text('Description'),
75 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
77 'location_id': fields.many2one('stock.location', 'Production Location',
78 help="Keep empty if you produce at the location where the finished products are needed." \
79 "Set a location if you produce at a fixed location. This can be a partner location " \
80 "if you subcontract the manufacturing operations."
82 'company_id': fields.many2one('res.company', 'Company'),
85 'active': lambda *a: 1,
86 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
90 class mrp_routing_workcenter(osv.osv):
92 Defines working cycles and hours of a workcenter using routings.
94 _name = 'mrp.routing.workcenter'
95 _description = 'Workcenter Usage'
97 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
98 'name': fields.char('Name', size=64, required=True),
99 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing workcenters."),
100 'cycle_nbr': fields.float('Number of Cycles', required=True,
101 help="Number of operations this workcenter can do."),
102 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for doing one cycle."),
103 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
104 help="Routing indicates all the workcenters used, for how long and/or cycles." \
105 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
106 'note': fields.text('Description'),
107 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company'),
110 'cycle_nbr': lambda *a: 1.0,
111 'hour_nbr': lambda *a: 0.0,
113 mrp_routing_workcenter()
115 class mrp_bom(osv.osv):
117 Defines bills of material for a product.
120 _description = 'Bill of Material'
122 def _child_compute(self, cr, uid, ids, name, arg, context={}):
124 @param self: The object pointer
125 @param cr: The current row, from the database cursor,
126 @param uid: The current user ID for security checks
127 @param ids: List of selected IDs
128 @param name: Name of the field
129 @param arg: User defined argument
130 @param context: A standard dictionary for contextual values
131 @return: Dictionary of values
134 bom_obj = self.pool.get('mrp.bom')
135 bom_id = context and context.get('active_id', False) or False
136 cr.execute('select id from mrp_bom')
137 if all(bom_id != r[0] for r in cr.fetchall()):
140 bom_parent = bom_obj.browse(cr, uid, bom_id)
141 for bom in self.browse(cr, uid, ids, context=context):
142 if (bom_parent) or (bom.id == bom_id):
143 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
148 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
149 if (bom.type=='phantom' or ok):
150 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
152 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
153 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
157 def _compute_type(self, cr, uid, ids, field_name, arg, context):
158 """ Sets particular method for the selected bom type.
159 @param field_name: Name of the field
160 @param arg: User defined argument
161 @return: Dictionary of values
163 res = dict(map(lambda x: (x,''), ids))
164 for line in self.browse(cr, uid, ids):
165 if line.type == 'phantom' and not line.bom_id:
168 if line.bom_lines or line.type == 'phantom':
170 if line.product_id.supply_method == 'produce':
171 if line.product_id.procure_method == 'make_to_stock':
172 res[line.id] = 'stock'
174 res[line.id] = 'order'
178 'name': fields.char('Name', size=64, required=True),
179 'code': fields.char('Reference', size=16),
180 '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."),
181 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
182 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
183 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
184 "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."),
185 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
186 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
187 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
188 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
189 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
190 'product_id': fields.many2one('product.product', 'Product', required=True),
191 'product_uos_qty': fields.float('Product UOS Qty'),
192 '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."),
193 'product_qty': fields.float('Product Qty', required=True),
194 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
195 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
196 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
197 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
198 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
199 '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."),
200 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
201 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
202 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
203 'company_id': fields.many2one('res.company','Company',required=True),
206 'active': lambda *a: 1,
207 'product_efficiency': lambda *a: 1.0,
208 'product_qty': lambda *a: 1.0,
209 'product_rounding': lambda *a: 0.0,
210 'type': lambda *a: 'normal',
211 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
215 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
216 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
219 def _check_recursion(self, cr, uid, ids):
222 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
223 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
229 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
233 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
234 """ Changes UoM and name if product_id changes.
235 @param name: Name of the field
236 @param product_id: Changed product_id
237 @return: Dictionary of changed values
240 prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
241 v = {'product_uom': prod.uom_id.id}
243 v['name'] = prod.name
247 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
248 """ Finds BoM for particular product and product uom.
249 @param product_id: Selected product.
250 @param product_uom: Unit of measure of a product.
251 @param properties: List of related properties.
252 @return: False or BoM id.
254 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
255 ids = map(lambda x: x[0], cr.fetchall())
258 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
260 for prop_id in bom.property_ids:
261 if prop_id.id in properties:
263 if (prop > max_prop) or ((max_prop == 0) and not result):
268 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
269 """ Finds Products and Workcenters for related BoM for manufacturing order.
270 @param bom: BoM of particular product.
271 @param factor: Factor of product UoM.
272 @param properties: A List of properties Ids.
273 @param addthis: If BoM found then True else False.
274 @param level: Depth level to find BoM lines starts from 10.
275 @return: result: List of dictionaries containing product details.
276 result2: List of dictionaries containing workcenter details.
278 factor = factor / (bom.product_efficiency or 1.0)
279 factor = rounding(factor, bom.product_rounding)
280 if factor < bom.product_rounding:
281 factor = bom.product_rounding
285 if bom.type == 'phantom' and not bom.bom_lines:
286 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
288 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
289 result = result + res[0]
290 result2 = result2 + res[1]
295 if addthis and not bom.bom_lines:
298 'name': bom.product_id.name,
299 'product_id': bom.product_id.id,
300 'product_qty': bom.product_qty * factor,
301 'product_uom': bom.product_uom.id,
302 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
303 'product_uos': bom.product_uos and bom.product_uos.id or False,
306 for wc_use in bom.routing_id.workcenter_lines:
307 wc = wc_use.workcenter_id
308 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
309 mult = (d + (m and 1.0 or 0.0))
310 cycle = mult * wc_use.cycle_nbr
312 'name': bom.routing_id.name,
313 'workcenter_id': wc.id,
314 'sequence': level+(wc_use.sequence or 0),
316 '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)),
318 for bom2 in bom.bom_lines:
319 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
320 result = result + res[0]
321 result2 = result2 + res[1]
322 return result, result2
326 class mrp_bom_revision(osv.osv):
327 _name = 'mrp.bom.revision'
328 _description = 'Bill of Material Revision'
330 'name': fields.char('Modification name', size=64, required=True),
331 'description': fields.text('Description'),
332 'date': fields.date('Modification Date'),
333 'indice': fields.char('Revision', size=16),
334 'last_indice': fields.char('last indice', size=64),
335 'author_id': fields.many2one('res.users', 'Author'),
336 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
340 'author_id': lambda x, y, z, c: z,
341 'date': lambda *a: time.strftime('%Y-%m-%d'),
349 return round(f / r) * r
351 class mrp_production(osv.osv):
353 Production Orders / Manufacturing Orders
355 _name = 'mrp.production'
356 _description = 'Manufacturing Order'
357 _date_name = 'date_planned'
359 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
360 """ Calculates total hours and total no. of cycles for a production order.
361 @param prop: Name of field.
363 @return: Dictionary of values.
366 for prod in self.browse(cr, uid, ids, context=context):
371 for wc in prod.workcenter_lines:
372 result[prod.id]['hour_total'] += wc.hour
373 result[prod.id]['cycle_total'] += wc.cycle
376 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
377 """ Finds production end date.
378 @param prop: Name of field.
380 @return: Dictionary of values.
383 for prod in self.browse(cr, uid, ids, context=context):
384 result[prod.id] = prod.date_planned
387 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
388 """ Finds production planned date.
389 @param prop: Name of field.
391 @return: Dictionary of values.
394 for prod in self.browse(cr, uid, ids, context=context):
395 result[prod.id] = prod.date_planned[:10]
399 'name': fields.char('Reference', size=64, required=True),
400 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
401 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
403 'product_id': fields.many2one('product.product', 'Product', required=True, ),
404 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
405 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
406 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
407 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
409 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
410 help="Location where the system will look for components."),
411 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
412 help="Location where the system will stock the finished products."),
414 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
415 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
416 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
417 'date_start': fields.datetime('Start Date'),
418 'date_finished': fields.datetime('End Date'),
420 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
421 '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."),
423 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
424 help="This is the internal picking list that brings the finished product to the production plan"),
425 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
426 '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)]}),
427 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
428 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
429 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
430 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
431 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
432 '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,
433 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\'.\
434 \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\'.'),
435 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
436 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
437 'company_id': fields.many2one('res.company','Company',required=True),
440 'priority': lambda *a: '1',
441 'state': lambda *a: 'draft',
442 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
443 'product_qty': lambda *a: 1.0,
444 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
445 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
447 _order = 'priority desc, date_planned asc';
449 def _check_qty(self, cr, uid, ids):
450 orders = self.browse(cr, uid, ids)
452 if order.product_qty <= 0:
457 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
460 def unlink(self, cr, uid, ids, context=None):
461 productions = self.read(cr, uid, ids, ['state'])
463 for s in productions:
464 if s['state'] in ['draft','cancel']:
465 unlink_ids.append(s['id'])
467 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
468 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
470 def copy(self, cr, uid, id, default=None, context=None):
474 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
477 'move_created_ids' : [],
478 'move_created_ids2' : [],
479 'product_lines' : [],
482 return super(mrp_production, self).copy(cr, uid, id, default, context)
484 def location_id_change(self, cr, uid, ids, src, dest, context={}):
485 """ Changes destination location if source location is changed.
486 @param src: Source location id.
487 @param dest: Destination location id.
488 @return: Dictionary of values.
493 return {'value': {'location_dest_id': src}}
496 def product_id_change(self, cr, uid, ids, product_id, context=None):
497 """ Finds UoM of changed product.
498 @param product_id: Id of changed product.
499 @return: Dictionary of values.
503 'product_uom': False,
507 bom_obj = self.pool.get('mrp.bom')
508 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
509 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
512 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
513 routing_id = bom_point.routing_id.id or False
515 'product_uom': product.uom_id and product.uom_id.id or False,
517 'routing_id': routing_id
519 return {'value': result}
521 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
522 """ Finds routing for changed BoM.
523 @param product: Id of product.
524 @return: Dictionary of values.
530 bom_pool = self.pool.get('mrp.bom')
531 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
532 routing_id = bom_point.routing_id.id or False
534 'routing_id': routing_id
536 return {'value': result}
538 def action_picking_except(self, cr, uid, ids):
539 """ Changes the state to Exception.
542 self.write(cr, uid, ids, {'state': 'picking_except'})
545 def action_compute(self, cr, uid, ids, properties=[]):
546 """ Computes bills of material of a product.
547 @param properties: List containing dictionaries of properties.
548 @return: No. of products.
551 bom_obj = self.pool.get('mrp.bom')
552 prod_line_obj = self.pool.get('mrp.production.product.line')
553 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
554 for production in self.browse(cr, uid, ids):
555 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
556 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
557 bom_point = production.bom_id
558 bom_id = production.bom_id.id
560 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
562 bom_point = bom_obj.browse(cr, uid, bom_id)
563 routing_id = bom_point.routing_id.id or False
564 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
567 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
569 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
570 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
574 line['production_id'] = production.id
575 prod_line_obj.create(cr, uid, line)
576 for line in results2:
577 line['production_id'] = production.id
578 workcenter_line_obj.create(cr, uid, line)
581 def action_cancel(self, cr, uid, ids):
582 """ Cancels the production order and related stock moves.
585 move_obj = self.pool.get('stock.move')
586 for production in self.browse(cr, uid, ids):
587 if production.move_created_ids:
588 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
589 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
590 self.write(cr, uid, ids, {'state': 'cancel'})
593 def action_ready(self, cr, uid, ids):
594 """ Changes the production state to Ready and location id of stock move.
597 move_obj = self.pool.get('stock.move')
598 self.write(cr, uid, ids, {'state': 'ready'})
600 for (production_id,name) in self.name_get(cr, uid, ids):
601 production = self.browse(cr, uid, production_id)
602 if production.move_prod_id:
603 move_obj.write(cr, uid, [production.move_prod_id.id],
604 {'location_id': production.location_dest_id.id})
606 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
607 self.log(cr, uid, production_id, message)
610 def action_production_end(self, cr, uid, ids):
611 """ Changes production state to Finish and writes finished date.
614 for production in self.browse(cr, uid, ids):
615 self._costs_generate(cr, uid, production)
616 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
618 def test_production_done(self, cr, uid, ids):
619 """ Tests whether production is done or not.
620 @return: True or False
623 for production in self.browse(cr, uid, ids):
624 if production.move_lines:
627 if production.move_created_ids:
631 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
632 """ To produce final product based on production mode (consume/consume&produce).
633 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
634 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
635 and stock move lines of final product will be also done/produced.
636 @param production_id: the ID of mrp.production object
637 @param production_qty: specify qty to produce
638 @param production_mode: specify production mode (consume/consume&produce).
642 stock_mov_obj = self.pool.get('stock.move')
643 production = self.browse(cr, uid, production_id)
645 final_product_todo = []
648 if production_mode == 'consume_produce':
649 produced_qty = production_qty
651 for produced_product in production.move_created_ids2:
652 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
654 produced_qty += produced_product.product_qty
656 if production_mode in ['consume','consume_produce']:
657 consumed_products = {}
659 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
661 for consumed_product in production.move_lines2:
662 consumed = consumed_product.product_qty
663 if consumed_product.scrapped:
665 if not consumed_products.get(consumed_product.product_id.id, False):
666 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
667 check[consumed_product.product_id.id] = 0
668 for f in production.product_lines:
669 if f.product_id.id == consumed_product.product_id.id:
670 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
671 check[consumed_product.product_id.id] += consumed_product.product_qty
672 consumed = check[consumed_product.product_id.id]
673 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
674 consumed_products[consumed_product.product_id.id] = rest_consumed
676 for raw_product in production.move_lines:
677 for f in production.product_lines:
678 if f.product_id.id == raw_product.product_id.id:
679 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
680 if consumed_qty == 0:
681 consumed_qty = production_qty * f.product_qty / production.product_qty
683 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
685 if production_mode == 'consume_produce':
686 # To produce remaining qty of final product
687 vals = {'state':'confirmed'}
688 final_product_todo = [x.id for x in production.move_created_ids]
689 stock_mov_obj.write(cr, uid, final_product_todo, vals)
690 produced_products = {}
691 for produced_product in production.move_created_ids2:
692 if produced_product.scrapped:
694 if not produced_products.get(produced_product.product_id.id, False):
695 produced_products[produced_product.product_id.id] = 0
696 produced_products[produced_product.product_id.id] += produced_product.product_qty
698 for produce_product in production.move_created_ids:
699 produced_qty = produced_products.get(produce_product.product_id.id, 0)
700 rest_qty = production.product_qty - produced_qty
701 if rest_qty <= production_qty:
702 production_qty = rest_qty
704 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
706 for raw_product in production.move_lines2:
708 parent_move_ids = [x.id for x in raw_product.move_history_ids]
709 for final_product in production.move_created_ids2:
710 if final_product.id not in parent_move_ids:
711 new_parent_ids.append(final_product.id)
712 for new_parent_id in new_parent_ids:
713 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
715 wf_service = netsvc.LocalService("workflow")
716 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
719 def _costs_generate(self, cr, uid, production):
720 """ Calculates total costs at the end of the production.
721 @param production: Id of production order.
722 @return: Calculated amount.
725 analytic_line_obj = self.pool.get('account.analytic.line')
726 for wc_line in production.workcenter_lines:
727 wc = wc_line.workcenter_id
728 if wc.costs_journal_id and wc.costs_general_account_id:
729 value = wc_line.hour * wc.costs_hour
730 account = wc.costs_hour_account_id.id
731 if value and account:
733 analytic_line_obj.create(cr, uid, {
734 'name': wc_line.name + ' (H)',
736 'account_id': account,
737 'general_account_id': wc.costs_general_account_id.id,
738 'journal_id': wc.costs_journal_id.id,
741 if wc.costs_journal_id and wc.costs_general_account_id:
742 value = wc_line.cycle * wc.costs_cycle
743 account = wc.costs_cycle_account_id.id
744 if value and account:
746 analytic_line_obj.create(cr, uid, {
747 'name': wc_line.name+' (C)',
749 'account_id': account,
750 'general_account_id': wc.costs_general_account_id.id,
751 'journal_id': wc.costs_journal_id.id,
756 def action_in_production(self, cr, uid, ids):
757 """ Changes state to In Production and writes starting date.
760 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
763 def test_if_product(self, cr, uid, ids):
765 @return: True or False
768 for production in self.browse(cr, uid, ids):
769 if not production.product_lines:
770 if not self.action_compute(cr, uid, [production.id]):
774 def _get_auto_picking(self, cr, uid, production):
777 def action_confirm(self, cr, uid, ids):
778 """ Confirms production order.
779 @return: Newly generated picking Id.
783 seq_obj = self.pool.get('ir.sequence')
784 pick_obj = self.pool.get('stock.picking')
785 move_obj = self.pool.get('stock.move')
786 proc_obj = self.pool.get('procurement.order')
787 wf_service = netsvc.LocalService("workflow")
788 for production in self.browse(cr, uid, ids):
789 if not production.product_lines:
790 self.action_compute(cr, uid, [production.id])
791 production = self.browse(cr, uid, [production.id])[0]
793 pick_type = 'internal'
795 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
796 routing_loc = production.bom_id.routing_id.location_id
797 if routing_loc.usage <> 'internal':
799 address_id = routing_loc.address_id and routing_loc.address_id.id or False
800 routing_loc = routing_loc.id
801 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
802 picking_id = pick_obj.create(cr, uid, {
804 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
808 'address_id': address_id,
809 'auto_picking': self._get_auto_picking(cr, uid, production),
810 'company_id': production.company_id.id,
813 source = production.product_id.product_tmpl_id.property_stock_production.id
815 'name':'PROD:' + production.name,
816 'date': production.date_planned,
817 'product_id': production.product_id.id,
818 'product_qty': production.product_qty,
819 'product_uom': production.product_uom.id,
820 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
821 'product_uos': production.product_uos and production.product_uos.id or False,
822 'location_id': source,
823 'location_dest_id': production.location_dest_id.id,
824 'move_dest_id': production.move_prod_id.id,
826 'company_id': production.company_id.id,
828 res_final_id = move_obj.create(cr, uid, data)
830 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
832 for line in production.product_lines:
834 newdate = production.date_planned
835 if line.product_id.type in ('product', 'consu'):
836 res_dest_id = move_obj.create(cr, uid, {
837 'name':'PROD:' + production.name,
838 'date': production.date_planned,
839 'product_id': line.product_id.id,
840 'product_qty': line.product_qty,
841 'product_uom': line.product_uom.id,
842 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
843 'product_uos': line.product_uos and line.product_uos.id or False,
844 'location_id': routing_loc or production.location_src_id.id,
845 'location_dest_id': source,
846 'move_dest_id': res_final_id,
848 'company_id': production.company_id.id,
850 moves.append(res_dest_id)
851 move_id = move_obj.create(cr, uid, {
852 'name':'PROD:' + production.name,
853 'picking_id':picking_id,
854 'product_id': line.product_id.id,
855 'product_qty': line.product_qty,
856 'product_uom': line.product_uom.id,
857 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
858 'product_uos': line.product_uos and line.product_uos.id or False,
860 'move_dest_id': res_dest_id,
861 'location_id': production.location_src_id.id,
862 'location_dest_id': routing_loc or production.location_src_id.id,
864 'company_id': production.company_id.id,
866 proc_id = proc_obj.create(cr, uid, {
867 'name': (production.origin or '').split(':')[0] + ':' + production.name,
868 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
869 'date_planned': newdate,
870 'product_id': line.product_id.id,
871 'product_qty': line.product_qty,
872 'product_uom': line.product_uom.id,
873 'product_uos_qty': line.product_uos and line.product_qty or False,
874 'product_uos': line.product_uos and line.product_uos.id or False,
875 'location_id': production.location_src_id.id,
876 'procure_method': line.product_id.procure_method,
878 'company_id': production.company_id.id,
880 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
881 proc_ids.append(proc_id)
882 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
883 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
884 message = _("Manufacturing order '%s' is scheduled for the %s.") % (
886 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
888 self.log(cr, uid, production.id, message)
891 def force_production(self, cr, uid, ids, *args):
892 """ Assigns products.
893 @param *args: Arguments
896 pick_obj = self.pool.get('stock.picking')
897 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
902 class mrp_production_workcenter_line(osv.osv):
903 _name = 'mrp.production.workcenter.line'
904 _description = 'Work Order'
908 'name': fields.char('Work Order', size=64, required=True),
909 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
910 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
911 'hour': fields.float('Nbr of hours', digits=(16,2)),
912 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
913 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
916 'sequence': lambda *a: 1,
917 'hour': lambda *a: 0,
918 'cycle': lambda *a: 0,
920 mrp_production_workcenter_line()
922 class mrp_production_product_line(osv.osv):
923 _name = 'mrp.production.product.line'
924 _description = 'Production Scheduled Product'
926 'name': fields.char('Name', size=64, required=True),
927 'product_id': fields.many2one('product.product', 'Product', required=True),
928 'product_qty': fields.float('Product Qty', required=True),
929 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
930 'product_uos_qty': fields.float('Product UOS Qty'),
931 'product_uos': fields.many2one('product.uom', 'Product UOS'),
932 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
934 mrp_production_product_line()
936 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: