1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import datetime
23 from osv import osv, fields
24 from tools.translate import _
30 #----------------------------------------------------------
32 #----------------------------------------------------------
33 # capacity_hour : capacity per hour. default: 1.0.
34 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
35 # unit_per_cycle : how many units are produced for one cycle
37 class mrp_workcenter(osv.osv):
38 _name = 'mrp.workcenter'
39 _description = 'Work Center'
40 _inherits = {'resource.resource':"resource_id"}
42 'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
43 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this workcenter can do in parallel. If this workcenter represents a team of 5 workers, the capacity per cycle is 5."),
44 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
45 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
46 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
47 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Workcenter per hour."),
48 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
49 help="Complete this only if you want automatic analytic accounting entries on production orders."),
50 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Workcenter per cycle."),
51 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
52 help="Complete this only if you want automatic analytic accounting entries on production orders."),
53 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
54 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
55 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
58 'capacity_per_cycle': 1.0,
59 'resource_type': 'material',
64 class mrp_routing(osv.osv):
66 For specifying the routings of workcenters.
69 _description = 'Routing'
71 'name': fields.char('Name', size=64, required=True),
72 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
73 'code': fields.char('Code', size=8),
75 'note': fields.text('Description'),
76 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
78 'location_id': fields.many2one('stock.location', 'Production Location',
79 help="Keep empty if you produce at the location where the finished products are needed." \
80 "Set a location if you produce at a fixed location. This can be a partner location " \
81 "if you subcontract the manufacturing operations."
83 'company_id': fields.many2one('res.company', 'Company'),
86 'active': lambda *a: 1,
87 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
91 class mrp_routing_workcenter(osv.osv):
93 Defines working cycles and hours of a workcenter using routings.
95 _name = 'mrp.routing.workcenter'
96 _description = 'Workcenter Usage'
98 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
99 'name': fields.char('Name', size=64, required=True),
100 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing workcenters."),
101 'cycle_nbr': fields.float('Number of Cycles', required=True,
102 help="Number of operations this workcenter can do."),
103 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for doing one cycle."),
104 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
105 help="Routing indicates all the workcenters used, for how long and/or cycles." \
106 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
107 'note': fields.text('Description'),
108 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company'),
111 'cycle_nbr': lambda *a: 1.0,
112 'hour_nbr': lambda *a: 0.0,
114 mrp_routing_workcenter()
116 class mrp_bom(osv.osv):
118 Defines bills of material for a product.
121 _description = 'Bill of Material'
123 def _child_compute(self, cr, uid, ids, name, arg, context=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))
167 for line in self.browse(cr, uid, ids, context=context):
168 if line.type == 'phantom' and not line.bom_id:
171 if line.bom_lines or line.type == 'phantom':
173 if line.product_id.supply_method == 'produce':
174 if line.product_id.procure_method == 'make_to_stock':
175 res[line.id] = 'stock'
177 res[line.id] = 'order'
181 'name': fields.char('Name', size=64, required=True),
182 'code': fields.char('Reference', size=16),
183 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the bills of material without removing it."),
184 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
185 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
186 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
187 "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."),
188 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
189 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
190 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
191 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
192 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
193 'product_id': fields.many2one('product.product', 'Product', required=True),
194 'product_uos_qty': fields.float('Product UOS Qty'),
195 '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."),
196 'product_qty': fields.float('Product Qty', required=True),
197 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
198 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
199 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
200 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
201 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
202 '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."),
203 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
204 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
205 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
206 'company_id': fields.many2one('res.company','Company',required=True),
209 'active': lambda *a: 1,
210 'product_efficiency': lambda *a: 1.0,
211 'product_qty': lambda *a: 1.0,
212 'product_rounding': lambda *a: 0.0,
213 'type': lambda *a: 'normal',
214 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
218 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
219 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
222 def _check_recursion(self, cr, uid, ids, context=None):
225 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
226 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
232 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
236 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
237 """ Changes UoM and name if product_id changes.
238 @param name: Name of the field
239 @param product_id: Changed product_id
240 @return: Dictionary of changed values
243 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
244 v = {'product_uom': prod.uom_id.id}
246 v['name'] = prod.name
250 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
251 """ Finds BoM for particular product and product uom.
252 @param product_id: Selected product.
253 @param product_uom: Unit of measure of a product.
254 @param properties: List of related properties.
255 @return: False or BoM id.
257 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
258 ids = map(lambda x: x[0], cr.fetchall())
261 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
263 for prop_id in bom.property_ids:
264 if prop_id.id in properties:
266 if (prop > max_prop) or ((max_prop == 0) and not result):
271 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
272 """ Finds Products and Workcenters for related BoM for manufacturing order.
273 @param bom: BoM of particular product.
274 @param factor: Factor of product UoM.
275 @param properties: A List of properties Ids.
276 @param addthis: If BoM found then True else False.
277 @param level: Depth level to find BoM lines starts from 10.
278 @return: result: List of dictionaries containing product details.
279 result2: List of dictionaries containing workcenter details.
281 factor = factor / (bom.product_efficiency or 1.0)
282 factor = rounding(factor, bom.product_rounding)
283 if factor < bom.product_rounding:
284 factor = bom.product_rounding
288 if bom.type == 'phantom' and not bom.bom_lines:
289 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
291 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
292 result = result + res[0]
293 result2 = result2 + res[1]
298 if addthis and not bom.bom_lines:
301 'name': bom.product_id.name,
302 'product_id': bom.product_id.id,
303 'product_qty': bom.product_qty * factor,
304 'product_uom': bom.product_uom.id,
305 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
306 'product_uos': bom.product_uos and bom.product_uos.id or False,
309 for wc_use in bom.routing_id.workcenter_lines:
310 wc = wc_use.workcenter_id
311 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
312 mult = (d + (m and 1.0 or 0.0))
313 cycle = mult * wc_use.cycle_nbr
315 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
316 'workcenter_id': wc.id,
317 'sequence': level+(wc_use.sequence or 0),
319 '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)),
321 for bom2 in bom.bom_lines:
322 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
323 result = result + res[0]
324 result2 = result2 + res[1]
325 return result, result2
329 class mrp_bom_revision(osv.osv):
330 _name = 'mrp.bom.revision'
331 _description = 'Bill of Material Revision'
333 'name': fields.char('Modification name', size=64, required=True),
334 'description': fields.text('Description'),
335 'date': fields.date('Modification Date'),
336 'indice': fields.char('Revision', size=16),
337 'last_indice': fields.char('last indice', size=64),
338 'author_id': fields.many2one('res.users', 'Author'),
339 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
343 'author_id': lambda x, y, z, c: z,
344 'date': lambda *a: time.strftime('%Y-%m-%d'),
352 return round(f / r) * r
354 class mrp_production(osv.osv):
356 Production Orders / Manufacturing Orders
358 _name = 'mrp.production'
359 _description = 'Manufacturing Order'
360 _date_name = 'date_planned'
362 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
363 """ Calculates total hours and total no. of cycles for a production order.
364 @param prop: Name of field.
366 @return: Dictionary of values.
369 for prod in self.browse(cr, uid, ids, context=context):
374 for wc in prod.workcenter_lines:
375 result[prod.id]['hour_total'] += wc.hour
376 result[prod.id]['cycle_total'] += wc.cycle
379 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context=None):
380 """ Finds production end date.
381 @param prop: Name of field.
383 @return: Dictionary of values.
386 for prod in self.browse(cr, uid, ids, context=context):
387 result[prod.id] = prod.date_planned
390 def _production_date(self, cr, uid, ids, prop, unknow_none, context=None):
391 """ Finds production planned date.
392 @param prop: Name of field.
394 @return: Dictionary of values.
397 for prod in self.browse(cr, uid, ids, context=context):
398 result[prod.id] = prod.date_planned[:10]
402 'name': fields.char('Reference', size=64, required=True),
403 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
404 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
406 'product_id': fields.many2one('product.product', 'Product', required=True, ),
407 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
408 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
409 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
410 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
412 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
413 help="Location where the system will look for components."),
414 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
415 help="Location where the system will stock the finished products."),
417 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
418 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
419 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
420 'date_start': fields.datetime('Start Date'),
421 'date_finished': fields.datetime('End Date'),
423 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
424 '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."),
426 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
427 help="This is the internal picking list that brings the finished product to the production plan"),
428 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
429 '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)]}),
430 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
431 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
432 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
433 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
434 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
435 '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,
436 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\'.\
437 \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\'.'),
438 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
439 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
440 'company_id': fields.many2one('res.company','Company',required=True),
443 'priority': lambda *a: '1',
444 'state': lambda *a: 'draft',
445 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
446 'product_qty': lambda *a: 1.0,
447 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
448 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
450 _order = 'priority desc, date_planned asc';
452 def _check_qty(self, cr, uid, ids, context=None):
453 orders = self.browse(cr, uid, ids, context=context)
455 if order.product_qty <= 0:
460 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
463 def unlink(self, cr, uid, ids, context=None):
464 productions = self.read(cr, uid, ids, ['state'])
466 for s in productions:
467 if s['state'] in ['draft','cancel']:
468 unlink_ids.append(s['id'])
470 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!') % s['state'])
471 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
473 def copy(self, cr, uid, id, default=None, context=None):
477 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
480 'move_created_ids' : [],
481 'move_created_ids2' : [],
482 'product_lines' : [],
485 return super(mrp_production, self).copy(cr, uid, id, default, context)
487 def location_id_change(self, cr, uid, ids, src, dest, context=None):
488 """ Changes destination location if source location is changed.
489 @param src: Source location id.
490 @param dest: Destination location id.
491 @return: Dictionary of values.
496 return {'value': {'location_dest_id': src}}
499 def product_id_change(self, cr, uid, ids, product_id, context=None):
500 """ Finds UoM of changed product.
501 @param product_id: Id of changed product.
502 @return: Dictionary of values.
506 'product_uom': False,
510 bom_obj = self.pool.get('mrp.bom')
511 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
512 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
515 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
516 routing_id = bom_point.routing_id.id or False
518 'product_uom': product.uom_id and product.uom_id.id or False,
520 'routing_id': routing_id
522 return {'value': result}
524 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
525 """ Finds routing for changed BoM.
526 @param product: Id of product.
527 @return: Dictionary of values.
533 bom_pool = self.pool.get('mrp.bom')
534 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
535 routing_id = bom_point.routing_id.id or False
537 'routing_id': routing_id
539 return {'value': result}
541 def action_picking_except(self, cr, uid, ids):
542 """ Changes the state to Exception.
545 self.write(cr, uid, ids, {'state': 'picking_except'})
548 def action_compute(self, cr, uid, ids, properties=[]):
549 """ Computes bills of material of a product.
550 @param properties: List containing dictionaries of properties.
551 @return: No. of products.
554 bom_obj = self.pool.get('mrp.bom')
555 prod_line_obj = self.pool.get('mrp.production.product.line')
556 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
557 for production in self.browse(cr, uid, ids):
558 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
559 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
560 bom_point = production.bom_id
561 bom_id = production.bom_id.id
563 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
565 bom_point = bom_obj.browse(cr, uid, bom_id)
566 routing_id = bom_point.routing_id.id or False
567 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
570 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
572 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
573 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
577 line['production_id'] = production.id
578 prod_line_obj.create(cr, uid, line)
579 for line in results2:
580 line['production_id'] = production.id
581 workcenter_line_obj.create(cr, uid, line)
584 def action_cancel(self, cr, uid, ids):
585 """ Cancels the production order and related stock moves.
588 move_obj = self.pool.get('stock.move')
589 for production in self.browse(cr, uid, ids):
590 if production.move_created_ids:
591 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
592 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
593 self.write(cr, uid, ids, {'state': 'cancel'})
596 def action_ready(self, cr, uid, ids):
597 """ Changes the production state to Ready and location id of stock move.
600 move_obj = self.pool.get('stock.move')
601 self.write(cr, uid, ids, {'state': 'ready'})
603 for (production_id,name) in self.name_get(cr, uid, ids):
604 production = self.browse(cr, uid, production_id)
605 if production.move_prod_id:
606 move_obj.write(cr, uid, [production.move_prod_id.id],
607 {'location_id': production.location_dest_id.id})
609 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
610 self.log(cr, uid, production_id, message)
613 def action_production_end(self, cr, uid, ids):
614 """ Changes production state to Finish and writes finished date.
617 for production in self.browse(cr, uid, ids):
618 self._costs_generate(cr, uid, production)
619 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
621 def test_production_done(self, cr, uid, ids):
622 """ Tests whether production is done or not.
623 @return: True or False
626 for production in self.browse(cr, uid, ids):
627 if production.move_lines:
630 if production.move_created_ids:
634 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
635 """ To produce final product based on production mode (consume/consume&produce).
636 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
637 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
638 and stock move lines of final product will be also done/produced.
639 @param production_id: the ID of mrp.production object
640 @param production_qty: specify qty to produce
641 @param production_mode: specify production mode (consume/consume&produce).
644 stock_mov_obj = self.pool.get('stock.move')
645 production = self.browse(cr, uid, production_id, context=context)
647 final_product_todo = []
650 if production_mode == 'consume_produce':
651 produced_qty = production_qty
653 for produced_product in production.move_created_ids2:
654 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
656 produced_qty += produced_product.product_qty
658 if production_mode in ['consume','consume_produce']:
659 consumed_products = {}
661 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
663 for consumed_product in production.move_lines2:
664 consumed = consumed_product.product_qty
665 if consumed_product.scrapped:
667 if not consumed_products.get(consumed_product.product_id.id, False):
668 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
669 check[consumed_product.product_id.id] = 0
670 for f in production.product_lines:
671 if f.product_id.id == consumed_product.product_id.id:
672 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
673 check[consumed_product.product_id.id] += consumed_product.product_qty
674 consumed = check[consumed_product.product_id.id]
675 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
676 consumed_products[consumed_product.product_id.id] = rest_consumed
678 for raw_product in production.move_lines:
679 for f in production.product_lines:
680 if f.product_id.id == raw_product.product_id.id:
681 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
682 if consumed_qty == 0:
683 consumed_qty = production_qty * f.product_qty / production.product_qty
685 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
687 if production_mode == 'consume_produce':
688 # To produce remaining qty of final product
689 vals = {'state':'confirmed'}
690 final_product_todo = [x.id for x in production.move_created_ids]
691 stock_mov_obj.write(cr, uid, final_product_todo, vals)
692 produced_products = {}
693 for produced_product in production.move_created_ids2:
694 if produced_product.scrapped:
696 if not produced_products.get(produced_product.product_id.id, False):
697 produced_products[produced_product.product_id.id] = 0
698 produced_products[produced_product.product_id.id] += produced_product.product_qty
700 for produce_product in production.move_created_ids:
701 produced_qty = produced_products.get(produce_product.product_id.id, 0)
702 rest_qty = production.product_qty - produced_qty
703 if rest_qty <= production_qty:
704 production_qty = rest_qty
706 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
708 for raw_product in production.move_lines2:
710 parent_move_ids = [x.id for x in raw_product.move_history_ids]
711 for final_product in production.move_created_ids2:
712 if final_product.id not in parent_move_ids:
713 new_parent_ids.append(final_product.id)
714 for new_parent_id in new_parent_ids:
715 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
717 wf_service = netsvc.LocalService("workflow")
718 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
721 def _costs_generate(self, cr, uid, production):
722 """ Calculates total costs at the end of the production.
723 @param production: Id of production order.
724 @return: Calculated amount.
727 analytic_line_obj = self.pool.get('account.analytic.line')
728 for wc_line in production.workcenter_lines:
729 wc = wc_line.workcenter_id
730 if wc.costs_journal_id and wc.costs_general_account_id:
731 value = wc_line.hour * wc.costs_hour
732 account = wc.costs_hour_account_id.id
733 if value and account:
735 analytic_line_obj.create(cr, uid, {
736 'name': wc_line.name + ' (H)',
738 'account_id': account,
739 'general_account_id': wc.costs_general_account_id.id,
740 'journal_id': wc.costs_journal_id.id,
743 if wc.costs_journal_id and wc.costs_general_account_id:
744 value = wc_line.cycle * wc.costs_cycle
745 account = wc.costs_cycle_account_id.id
746 if value and account:
748 analytic_line_obj.create(cr, uid, {
749 'name': wc_line.name+' (C)',
751 'account_id': account,
752 'general_account_id': wc.costs_general_account_id.id,
753 'journal_id': wc.costs_journal_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: