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={}):
125 @param self: The object pointer
126 @param cr: The current row, from the database cursor,
127 @param uid: The current user ID for security checks
128 @param ids: List of selected IDs
129 @param name: Name of the field
130 @param arg: User defined argument
131 @param context: A standard dictionary for contextual values
132 @return: Dictionary of values
135 bom_obj = self.pool.get('mrp.bom')
136 bom_id = context and context.get('active_id', False) or False
137 cr.execute('select id from mrp_bom')
138 if all(bom_id != r[0] for r in cr.fetchall()):
141 bom_parent = bom_obj.browse(cr, uid, bom_id)
142 for bom in self.browse(cr, uid, ids, context=context):
143 if (bom_parent) or (bom.id == bom_id):
144 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
149 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
150 if (bom.type=='phantom' or ok):
151 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
153 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
154 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
158 def _compute_type(self, cr, uid, ids, field_name, arg, context):
159 """ Sets particular method for the selected bom type.
160 @param field_name: Name of the field
161 @param arg: User defined argument
162 @return: Dictionary of values
164 res = dict(map(lambda x: (x,''), ids))
165 for line in self.browse(cr, uid, ids):
166 if line.type == 'phantom' and not line.bom_id:
169 if line.bom_lines or line.type == 'phantom':
171 if line.product_id.supply_method == 'produce':
172 if line.product_id.procure_method == 'make_to_stock':
173 res[line.id] = 'stock'
175 res[line.id] = 'order'
179 'name': fields.char('Name', size=64, required=True),
180 'code': fields.char('Reference', size=16),
181 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the bills of material without removing it."),
182 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
183 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
184 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
185 "If a Phantom BoM is used for a root product, it will be sold and shipped as a set of components, instead of being produced."),
186 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
187 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
188 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
189 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
190 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
191 'product_id': fields.many2one('product.product', 'Product', required=True),
192 'product_uos_qty': fields.float('Product UOS Qty'),
193 'product_uos': fields.many2one('product.uom', 'Product UOS', help="Product UOS (Unit of Sale) is the unit of measurement for the invoicing and promotion of stock."),
194 'product_qty': fields.float('Product Qty', required=True),
195 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
196 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
197 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
198 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
199 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
200 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of workcenters) to produce the finished product. The routing is mainly used to compute workcenter costs during operations and to plan future loads on workcenters based on production planning."),
201 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
202 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
203 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
204 'company_id': fields.many2one('res.company','Company',required=True),
207 'active': lambda *a: 1,
208 'product_efficiency': lambda *a: 1.0,
209 'product_qty': lambda *a: 1.0,
210 'product_rounding': lambda *a: 0.0,
211 'type': lambda *a: 'normal',
212 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
216 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
217 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
220 def _check_recursion(self, cr, uid, ids):
223 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
224 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
230 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
234 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
235 """ Changes UoM and name if product_id changes.
236 @param name: Name of the field
237 @param product_id: Changed product_id
238 @return: Dictionary of changed values
241 prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
242 v = {'product_uom': prod.uom_id.id}
244 v['name'] = prod.name
248 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
249 """ Finds BoM for particular product and product uom.
250 @param product_id: Selected product.
251 @param product_uom: Unit of measure of a product.
252 @param properties: List of related properties.
253 @return: False or BoM id.
255 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
256 ids = map(lambda x: x[0], cr.fetchall())
259 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
261 for prop_id in bom.property_ids:
262 if prop_id.id in properties:
264 if (prop > max_prop) or ((max_prop == 0) and not result):
269 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
270 """ Finds Products and Workcenters for related BoM for manufacturing order.
271 @param bom: BoM of particular product.
272 @param factor: Factor of product UoM.
273 @param properties: A List of properties Ids.
274 @param addthis: If BoM found then True else False.
275 @param level: Depth level to find BoM lines starts from 10.
276 @return: result: List of dictionaries containing product details.
277 result2: List of dictionaries containing workcenter details.
279 factor = factor / (bom.product_efficiency or 1.0)
280 factor = rounding(factor, bom.product_rounding)
281 if factor < bom.product_rounding:
282 factor = bom.product_rounding
286 if bom.type == 'phantom' and not bom.bom_lines:
287 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
289 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
290 result = result + res[0]
291 result2 = result2 + res[1]
296 if addthis and not bom.bom_lines:
299 'name': bom.product_id.name,
300 'product_id': bom.product_id.id,
301 'product_qty': bom.product_qty * factor,
302 'product_uom': bom.product_uom.id,
303 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
304 'product_uos': bom.product_uos and bom.product_uos.id or False,
307 for wc_use in bom.routing_id.workcenter_lines:
308 wc = wc_use.workcenter_id
309 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
310 mult = (d + (m and 1.0 or 0.0))
311 cycle = mult * wc_use.cycle_nbr
313 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
314 'workcenter_id': wc.id,
315 'sequence': level+(wc_use.sequence or 0),
317 'hour': float(wc_use.hour_nbr*mult + ((wc.time_start or 0.0)+(wc.time_stop or 0.0)+cycle*(wc.time_cycle or 0.0)) * (wc.time_efficiency or 1.0)),
319 for bom2 in bom.bom_lines:
320 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
321 result = result + res[0]
322 result2 = result2 + res[1]
323 return result, result2
327 class mrp_bom_revision(osv.osv):
328 _name = 'mrp.bom.revision'
329 _description = 'Bill of Material Revision'
331 'name': fields.char('Modification name', size=64, required=True),
332 'description': fields.text('Description'),
333 'date': fields.date('Modification Date'),
334 'indice': fields.char('Revision', size=16),
335 'last_indice': fields.char('last indice', size=64),
336 'author_id': fields.many2one('res.users', 'Author'),
337 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
341 'author_id': lambda x, y, z, c: z,
342 'date': lambda *a: time.strftime('%Y-%m-%d'),
350 return round(f / r) * r
352 class mrp_production(osv.osv):
354 Production Orders / Manufacturing Orders
356 _name = 'mrp.production'
357 _description = 'Manufacturing Order'
358 _date_name = 'date_planned'
360 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
361 """ Calculates total hours and total no. of cycles for a production order.
362 @param prop: Name of field.
364 @return: Dictionary of values.
367 for prod in self.browse(cr, uid, ids, context=context):
372 for wc in prod.workcenter_lines:
373 result[prod.id]['hour_total'] += wc.hour
374 result[prod.id]['cycle_total'] += wc.cycle
377 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
378 """ Finds production end date.
379 @param prop: Name of field.
381 @return: Dictionary of values.
384 for prod in self.browse(cr, uid, ids, context=context):
385 result[prod.id] = prod.date_planned
388 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
389 """ Finds production planned date.
390 @param prop: Name of field.
392 @return: Dictionary of values.
395 for prod in self.browse(cr, uid, ids, context=context):
396 result[prod.id] = prod.date_planned[:10]
400 'name': fields.char('Reference', size=64, required=True),
401 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
402 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
404 'product_id': fields.many2one('product.product', 'Product', required=True, ),
405 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
406 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
407 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
408 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
410 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
411 help="Location where the system will look for components."),
412 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
413 help="Location where the system will stock the finished products."),
415 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
416 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
417 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
418 'date_start': fields.datetime('Start Date'),
419 'date_finished': fields.datetime('End Date'),
421 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
422 '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."),
424 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
425 help="This is the internal picking list that brings the finished product to the production plan"),
426 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
427 '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)]}),
428 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
429 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
430 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
431 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
432 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
433 '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,
434 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\'.\
435 \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\'.'),
436 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
437 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
438 'company_id': fields.many2one('res.company','Company',required=True),
441 'priority': lambda *a: '1',
442 'state': lambda *a: 'draft',
443 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
444 'product_qty': lambda *a: 1.0,
445 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
446 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
448 _order = 'priority desc, date_planned asc';
450 def _check_qty(self, cr, uid, ids):
451 orders = self.browse(cr, uid, ids)
453 if order.product_qty <= 0:
458 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
461 def unlink(self, cr, uid, ids, context=None):
462 productions = self.read(cr, uid, ids, ['state'])
464 for s in productions:
465 if s['state'] in ['draft','cancel']:
466 unlink_ids.append(s['id'])
468 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
469 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
471 def copy(self, cr, uid, id, default=None, context=None):
475 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
478 'move_created_ids' : [],
479 'move_created_ids2' : [],
480 'product_lines' : [],
483 return super(mrp_production, self).copy(cr, uid, id, default, context)
485 def location_id_change(self, cr, uid, ids, src, dest, context={}):
486 """ Changes destination location if source location is changed.
487 @param src: Source location id.
488 @param dest: Destination location id.
489 @return: Dictionary of values.
494 return {'value': {'location_dest_id': src}}
497 def product_id_change(self, cr, uid, ids, product_id, context=None):
498 """ Finds UoM of changed product.
499 @param product_id: Id of changed product.
500 @return: Dictionary of values.
504 'product_uom': False,
508 bom_obj = self.pool.get('mrp.bom')
509 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
510 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
513 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
514 routing_id = bom_point.routing_id.id or False
516 'product_uom': product.uom_id and product.uom_id.id or False,
518 'routing_id': routing_id
520 return {'value': result}
522 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
523 """ Finds routing for changed BoM.
524 @param product: Id of product.
525 @return: Dictionary of values.
531 bom_pool = self.pool.get('mrp.bom')
532 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
533 routing_id = bom_point.routing_id.id or False
535 'routing_id': routing_id
537 return {'value': result}
539 def action_picking_except(self, cr, uid, ids):
540 """ Changes the state to Exception.
543 self.write(cr, uid, ids, {'state': 'picking_except'})
546 def action_compute(self, cr, uid, ids, properties=[]):
547 """ Computes bills of material of a product.
548 @param properties: List containing dictionaries of properties.
549 @return: No. of products.
552 bom_obj = self.pool.get('mrp.bom')
553 prod_line_obj = self.pool.get('mrp.production.product.line')
554 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
555 for production in self.browse(cr, uid, ids):
556 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
557 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
558 bom_point = production.bom_id
559 bom_id = production.bom_id.id
561 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
563 bom_point = bom_obj.browse(cr, uid, bom_id)
564 routing_id = bom_point.routing_id.id or False
565 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
568 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
570 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
571 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
575 line['production_id'] = production.id
576 prod_line_obj.create(cr, uid, line)
577 for line in results2:
578 line['production_id'] = production.id
579 workcenter_line_obj.create(cr, uid, line)
582 def action_cancel(self, cr, uid, ids):
583 """ Cancels the production order and related stock moves.
586 move_obj = self.pool.get('stock.move')
587 for production in self.browse(cr, uid, ids):
588 if production.move_created_ids:
589 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
590 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
591 self.write(cr, uid, ids, {'state': 'cancel'})
594 def action_ready(self, cr, uid, ids):
595 """ Changes the production state to Ready and location id of stock move.
598 move_obj = self.pool.get('stock.move')
599 self.write(cr, uid, ids, {'state': 'ready'})
601 for (production_id,name) in self.name_get(cr, uid, ids):
602 production = self.browse(cr, uid, production_id)
603 if production.move_prod_id:
604 move_obj.write(cr, uid, [production.move_prod_id.id],
605 {'location_id': production.location_dest_id.id})
607 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
608 self.log(cr, uid, production_id, message)
611 def action_production_end(self, cr, uid, ids):
612 """ Changes production state to Finish and writes finished date.
615 for production in self.browse(cr, uid, ids):
616 self._costs_generate(cr, uid, production)
617 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
619 def test_production_done(self, cr, uid, ids):
620 """ Tests whether production is done or not.
621 @return: True or False
624 for production in self.browse(cr, uid, ids):
625 if production.move_lines:
628 if production.move_created_ids:
632 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
633 """ To produce final product based on production mode (consume/consume&produce).
634 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
635 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
636 and stock move lines of final product will be also done/produced.
637 @param production_id: the ID of mrp.production object
638 @param production_qty: specify qty to produce
639 @param production_mode: specify production mode (consume/consume&produce).
643 stock_mov_obj = self.pool.get('stock.move')
644 production = self.browse(cr, uid, production_id)
646 final_product_todo = []
649 if production_mode == 'consume_produce':
650 produced_qty = production_qty
652 for produced_product in production.move_created_ids2:
653 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
655 produced_qty += produced_product.product_qty
657 if production_mode in ['consume','consume_produce']:
658 consumed_products = {}
660 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
662 for consumed_product in production.move_lines2:
663 consumed = consumed_product.product_qty
664 if consumed_product.scrapped:
666 if not consumed_products.get(consumed_product.product_id.id, False):
667 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
668 check[consumed_product.product_id.id] = 0
669 for f in production.product_lines:
670 if f.product_id.id == consumed_product.product_id.id:
671 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
672 check[consumed_product.product_id.id] += consumed_product.product_qty
673 consumed = check[consumed_product.product_id.id]
674 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
675 consumed_products[consumed_product.product_id.id] = rest_consumed
677 for raw_product in production.move_lines:
678 for f in production.product_lines:
679 if f.product_id.id == raw_product.product_id.id:
680 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
681 if consumed_qty == 0:
682 consumed_qty = production_qty * f.product_qty / production.product_qty
684 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
686 if production_mode == 'consume_produce':
687 # To produce remaining qty of final product
688 vals = {'state':'confirmed'}
689 final_product_todo = [x.id for x in production.move_created_ids]
690 stock_mov_obj.write(cr, uid, final_product_todo, vals)
691 produced_products = {}
692 for produced_product in production.move_created_ids2:
693 if produced_product.scrapped:
695 if not produced_products.get(produced_product.product_id.id, False):
696 produced_products[produced_product.product_id.id] = 0
697 produced_products[produced_product.product_id.id] += produced_product.product_qty
699 for produce_product in production.move_created_ids:
700 produced_qty = produced_products.get(produce_product.product_id.id, 0)
701 rest_qty = production.product_qty - produced_qty
702 if rest_qty <= production_qty:
703 production_qty = rest_qty
705 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
707 for raw_product in production.move_lines2:
709 parent_move_ids = [x.id for x in raw_product.move_history_ids]
710 for final_product in production.move_created_ids2:
711 if final_product.id not in parent_move_ids:
712 new_parent_ids.append(final_product.id)
713 for new_parent_id in new_parent_ids:
714 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
716 wf_service = netsvc.LocalService("workflow")
717 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
720 def _costs_generate(self, cr, uid, production):
721 """ Calculates total costs at the end of the production.
722 @param production: Id of production order.
723 @return: Calculated amount.
726 analytic_line_obj = self.pool.get('account.analytic.line')
727 for wc_line in production.workcenter_lines:
728 wc = wc_line.workcenter_id
729 if wc.costs_journal_id and wc.costs_general_account_id:
730 value = wc_line.hour * wc.costs_hour
731 account = wc.costs_hour_account_id.id
732 if value and account:
734 analytic_line_obj.create(cr, uid, {
735 'name': wc_line.name + ' (H)',
737 'account_id': account,
738 'general_account_id': wc.costs_general_account_id.id,
739 'journal_id': wc.costs_journal_id.id,
742 if wc.costs_journal_id and wc.costs_general_account_id:
743 value = wc_line.cycle * wc.costs_cycle
744 account = wc.costs_cycle_account_id.id
745 if value and account:
747 analytic_line_obj.create(cr, uid, {
748 'name': wc_line.name+' (C)',
750 'account_id': account,
751 'general_account_id': wc.costs_general_account_id.id,
752 'journal_id': wc.costs_journal_id.id,
754 'product_id': production.product_id.id
758 def action_in_production(self, cr, uid, ids):
759 """ Changes state to In Production and writes starting date.
762 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
765 def test_if_product(self, cr, uid, ids):
767 @return: True or False
770 for production in self.browse(cr, uid, ids):
771 if not production.product_lines:
772 if not self.action_compute(cr, uid, [production.id]):
776 def _get_auto_picking(self, cr, uid, production):
779 def action_confirm(self, cr, uid, ids):
780 """ Confirms production order.
781 @return: Newly generated picking Id.
785 seq_obj = self.pool.get('ir.sequence')
786 pick_obj = self.pool.get('stock.picking')
787 move_obj = self.pool.get('stock.move')
788 proc_obj = self.pool.get('procurement.order')
789 wf_service = netsvc.LocalService("workflow")
790 for production in self.browse(cr, uid, ids):
791 if not production.product_lines:
792 self.action_compute(cr, uid, [production.id])
793 production = self.browse(cr, uid, [production.id])[0]
795 pick_type = 'internal'
797 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
798 routing_loc = production.bom_id.routing_id.location_id
799 if routing_loc.usage <> 'internal':
801 address_id = routing_loc.address_id and routing_loc.address_id.id or False
802 routing_loc = routing_loc.id
803 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
804 picking_id = pick_obj.create(cr, uid, {
806 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
810 'address_id': address_id,
811 'auto_picking': self._get_auto_picking(cr, uid, production),
812 'company_id': production.company_id.id,
815 source = production.product_id.product_tmpl_id.property_stock_production.id
817 'name':'PROD:' + production.name,
818 'date': production.date_planned,
819 'product_id': production.product_id.id,
820 'product_qty': production.product_qty,
821 'product_uom': production.product_uom.id,
822 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
823 'product_uos': production.product_uos and production.product_uos.id or False,
824 'location_id': source,
825 'location_dest_id': production.location_dest_id.id,
826 'move_dest_id': production.move_prod_id.id,
828 'company_id': production.company_id.id,
830 res_final_id = move_obj.create(cr, uid, data)
832 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
834 for line in production.product_lines:
836 newdate = production.date_planned
837 if line.product_id.type in ('product', 'consu'):
838 res_dest_id = move_obj.create(cr, uid, {
839 'name':'PROD:' + production.name,
840 'date': production.date_planned,
841 'product_id': line.product_id.id,
842 'product_qty': line.product_qty,
843 'product_uom': line.product_uom.id,
844 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
845 'product_uos': line.product_uos and line.product_uos.id or False,
846 'location_id': routing_loc or production.location_src_id.id,
847 'location_dest_id': source,
848 'move_dest_id': res_final_id,
850 'company_id': production.company_id.id,
852 moves.append(res_dest_id)
853 move_id = move_obj.create(cr, uid, {
854 'name':'PROD:' + production.name,
855 'picking_id':picking_id,
856 'product_id': line.product_id.id,
857 'product_qty': line.product_qty,
858 'product_uom': line.product_uom.id,
859 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
860 'product_uos': line.product_uos and line.product_uos.id or False,
862 'move_dest_id': res_dest_id,
863 'location_id': production.location_src_id.id,
864 'location_dest_id': routing_loc or production.location_src_id.id,
866 'company_id': production.company_id.id,
868 proc_id = proc_obj.create(cr, uid, {
869 'name': (production.origin or '').split(':')[0] + ':' + production.name,
870 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
871 'date_planned': newdate,
872 'product_id': line.product_id.id,
873 'product_qty': line.product_qty,
874 'product_uom': line.product_uom.id,
875 'product_uos_qty': line.product_uos and line.product_qty or False,
876 'product_uos': line.product_uos and line.product_uos.id or False,
877 'location_id': production.location_src_id.id,
878 'procure_method': line.product_id.procure_method,
880 'company_id': production.company_id.id,
882 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
883 proc_ids.append(proc_id)
884 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
885 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
886 message = _("Manufacturing order '%s' is scheduled for the %s.") % (
888 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
890 self.log(cr, uid, production.id, message)
893 def force_production(self, cr, uid, ids, *args):
894 """ Assigns products.
895 @param *args: Arguments
898 pick_obj = self.pool.get('stock.picking')
899 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
904 class mrp_production_workcenter_line(osv.osv):
905 _name = 'mrp.production.workcenter.line'
906 _description = 'Work Order'
910 'name': fields.char('Work Order', size=64, required=True),
911 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
912 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
913 'hour': fields.float('Nbr of hours', digits=(16,2)),
914 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
915 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
918 'sequence': lambda *a: 1,
919 'hour': lambda *a: 0,
920 'cycle': lambda *a: 0,
922 mrp_production_workcenter_line()
924 class mrp_production_product_line(osv.osv):
925 _name = 'mrp.production.product.line'
926 _description = 'Production Scheduled Product'
928 'name': fields.char('Name', size=64, required=True),
929 'product_id': fields.many2one('product.product', 'Product', required=True),
930 'product_qty': fields.float('Product Qty', required=True),
931 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
932 'product_uos_qty': fields.float('Product UOS Qty'),
933 'product_uos': fields.many2one('product.uom', 'Product UOS'),
934 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
936 mrp_production_product_line()
938 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: