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 import decimal_precision as dp
25 from tools.translate import _
31 #----------------------------------------------------------
33 #----------------------------------------------------------
34 # capacity_hour : capacity per hour. default: 1.0.
35 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
36 # unit_per_cycle : how many units are produced for one cycle
38 class mrp_workcenter(osv.osv):
39 _name = 'mrp.workcenter'
40 _description = 'Work Center'
41 _inherits = {'resource.resource':"resource_id"}
43 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
44 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this Work Center can do in parallel. If this Work Center represents a team of 5 workers, the capacity per cycle is 5."),
45 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
46 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
47 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
48 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
49 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
50 help="Complete this only if you want automatic analytic accounting entries on production orders."),
51 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
52 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
53 help="Complete this only if you want automatic analytic accounting entries on production orders."),
54 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
55 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
56 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
57 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to track easily your production costs in the analytic accounting."),
60 'capacity_per_cycle': 1.0,
61 'resource_type': 'material',
64 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
68 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
69 value = {'costs_hour': cost.standard_price}
70 return {'value': value}
75 class mrp_routing(osv.osv):
77 For specifying the routings of Work Centers.
80 _description = 'Routing'
82 'name': fields.char('Name', size=64, required=True),
83 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
84 'code': fields.char('Code', size=8),
86 'note': fields.text('Description'),
87 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
89 'location_id': fields.many2one('stock.location', 'Production Location',
90 help="Keep empty if you produce at the location where the finished products are needed." \
91 "Set a location if you produce at a fixed location. This can be a partner location " \
92 "if you subcontract the manufacturing operations."
94 'company_id': fields.many2one('res.company', 'Company'),
97 'active': lambda *a: 1,
98 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
102 class mrp_routing_workcenter(osv.osv):
104 Defines working cycles and hours of a Work Center using routings.
106 _name = 'mrp.routing.workcenter'
107 _description = 'Work Center Usage'
109 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
110 'name': fields.char('Name', size=64, required=True),
111 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
112 'cycle_nbr': fields.float('Number of Cycles', required=True,
113 help="Number of iterations this work center has to do in the specified operation of the routing."),
114 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for this Work Center to achieve the operation of the specified routing."),
115 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
116 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
117 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
118 'note': fields.text('Description'),
119 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
122 'cycle_nbr': lambda *a: 1.0,
123 'hour_nbr': lambda *a: 0.0,
125 mrp_routing_workcenter()
127 class mrp_bom(osv.osv):
129 Defines bills of material for a product.
132 _description = 'Bill of Material'
133 _inherit = ['mail.thread']
135 def _child_compute(self, cr, uid, ids, name, arg, context=None):
137 @param self: The object pointer
138 @param cr: The current row, from the database cursor,
139 @param uid: The current user ID for security checks
140 @param ids: List of selected IDs
141 @param name: Name of the field
142 @param arg: User defined argument
143 @param context: A standard dictionary for contextual values
144 @return: Dictionary of values
149 bom_obj = self.pool.get('mrp.bom')
150 bom_id = context and context.get('active_id', False) or False
151 cr.execute('select id from mrp_bom')
152 if all(bom_id != r[0] for r in cr.fetchall()):
155 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
156 for bom in self.browse(cr, uid, ids, context=context):
157 if (bom_parent) or (bom.id == bom_id):
158 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
163 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
164 if (bom.type=='phantom' or ok):
165 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
167 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
168 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
172 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
173 """ Sets particular method for the selected bom type.
174 @param field_name: Name of the field
175 @param arg: User defined argument
176 @return: Dictionary of values
178 res = dict.fromkeys(ids, False)
179 for line in self.browse(cr, uid, ids, context=context):
180 if line.type == 'phantom' and not line.bom_id:
183 if line.bom_lines or line.type == 'phantom':
185 if line.product_id.supply_method == 'produce':
186 if line.product_id.procure_method == 'make_to_stock':
187 res[line.id] = 'stock'
189 res[line.id] = 'order'
193 'name': fields.char('Name', size=64, required=True),
194 'code': fields.char('Reference', size=16),
195 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the bills of material without removing it."),
196 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
197 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
198 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
199 "If a Phantom BoM is used for a root product, it will be sold and shipped as a set of components, instead of being produced."),
200 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
201 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
202 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
203 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
204 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
205 'product_id': fields.many2one('product.product', 'Product', required=True),
206 'product_uos_qty': fields.float('Product UOS Qty'),
207 'product_uos': fields.many2one('product.uom', 'Product UOS', help="Product UOS (Unit of Sale) is the unit of measurement for the invoicing and promotion of stock."),
208 'product_qty': fields.float('Product Qty', required=True, digits_compute=dp.get_precision('Product UoM')),
209 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
210 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
211 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
212 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
213 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
214 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
215 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
216 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
217 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
218 'company_id': fields.many2one('res.company','Company',required=True),
221 'active': lambda *a: 1,
222 'product_efficiency': lambda *a: 1.0,
223 'product_qty': lambda *a: 1.0,
224 'product_rounding': lambda *a: 0.0,
225 'type': lambda *a: 'normal',
226 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
230 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
231 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
234 def _check_recursion(self, cr, uid, ids, context=None):
237 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
238 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
244 def _check_product(self, cr, uid, ids, context=None):
246 boms = self.browse(cr, uid, ids, context=context)
250 if bom.product_id.id in all_prod:
252 all_prod.append(bom.product_id.id)
253 lines = bom.bom_lines
255 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
257 return check_bom(boms)
260 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
261 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
264 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
265 """ Changes UoM and name if product_id changes.
266 @param name: Name of the field
267 @param product_id: Changed product_id
268 @return: Dictionary of changed values
271 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
272 return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}}
275 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
276 """ Finds BoM for particular product and product uom.
277 @param product_id: Selected product.
278 @param product_uom: Unit of measure of a product.
279 @param properties: List of related properties.
280 @return: False or BoM id.
282 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
283 ids = map(lambda x: x[0], cr.fetchall())
286 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
288 for prop_id in bom.property_ids:
289 if prop_id.id in properties:
291 if (prop > max_prop) or ((max_prop == 0) and not result):
296 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0, routing_id=False):
297 """ Finds Products and Work Centers for related BoM for manufacturing order.
298 @param bom: BoM of particular product.
299 @param factor: Factor of product UoM.
300 @param properties: A List of properties Ids.
301 @param addthis: If BoM found then True else False.
302 @param level: Depth level to find BoM lines starts from 10.
303 @return: result: List of dictionaries containing product details.
304 result2: List of dictionaries containing Work Center details.
306 routing_obj = self.pool.get('mrp.routing')
307 factor = factor / (bom.product_efficiency or 1.0)
308 factor = rounding(factor, bom.product_rounding)
309 if factor < bom.product_rounding:
310 factor = bom.product_rounding
314 if bom.type == 'phantom' and not bom.bom_lines:
315 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
318 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
319 result = result + res[0]
320 result2 = result2 + res[1]
325 if addthis and not bom.bom_lines:
328 'name': bom.product_id.name,
329 'product_id': bom.product_id.id,
330 'product_qty': bom.product_qty * factor,
331 'product_uom': bom.product_uom.id,
332 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
333 'product_uos': bom.product_uos and bom.product_uos.id or False,
335 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
337 for wc_use in routing.workcenter_lines:
338 wc = wc_use.workcenter_id
339 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
340 mult = (d + (m and 1.0 or 0.0))
341 cycle = mult * wc_use.cycle_nbr
343 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
344 'workcenter_id': wc.id,
345 'sequence': level+(wc_use.sequence or 0),
347 '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)),
349 for bom2 in bom.bom_lines:
350 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
351 result = result + res[0]
352 result2 = result2 + res[1]
353 return result, result2
355 def copy_data(self, cr, uid, id, default=None, context=None):
358 bom_data = self.read(cr, uid, id, [], context=context)
359 default.update({'name': bom_data['name'] + ' ' + _('Copy'), 'bom_id':False})
360 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
362 def create(self, cr, uid, vals, context=None):
363 obj_id = super(mrp_bom, self).create(cr, uid, vals, context=context)
364 self.create_send_note(cr, uid, [obj_id], context=context)
367 def create_send_note(self, cr, uid, ids, context=None):
368 prod_obj = self.pool.get('product.product')
369 for obj in self.browse(cr, uid, ids, context=context):
370 for prod in prod_obj.browse(cr, uid, [obj.product_id], context=context):
371 self.message_append_note(cr, uid, [obj.id], body=_("Bill of Material has been <b>created</b> for <em>%s</em> product.") % (prod.id.name_template), context=context)
376 class mrp_bom_revision(osv.osv):
377 _name = 'mrp.bom.revision'
378 _description = 'Bill of Material Revision'
380 'name': fields.char('Modification name', size=64, required=True),
381 'description': fields.text('Description'),
382 'date': fields.date('Modification Date'),
383 'indice': fields.char('Revision', size=16),
384 'last_indice': fields.char('last indice', size=64),
385 'author_id': fields.many2one('res.users', 'Author'),
386 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
390 'author_id': lambda x, y, z, c: z,
391 'date': fields.date.context_today,
400 return math.ceil(f / r) * r
402 class mrp_production(osv.osv):
404 Production Orders / Manufacturing Orders
406 _name = 'mrp.production'
407 _description = 'Manufacturing Order'
408 _date_name = 'date_planned'
409 _inherit = ['ir.needaction_mixin', 'mail.thread']
411 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
412 """ Calculates total hours and total no. of cycles for a production order.
413 @param prop: Name of field.
415 @return: Dictionary of values.
418 for prod in self.browse(cr, uid, ids, context=context):
423 for wc in prod.workcenter_lines:
424 result[prod.id]['hour_total'] += wc.hour
425 result[prod.id]['cycle_total'] += wc.cycle
428 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context=None):
429 """ Finds production end date.
430 @param prop: Name of field.
432 @return: Dictionary of values.
435 for prod in self.browse(cr, uid, ids, context=context):
436 result[prod.id] = prod.date_planned
439 def _production_date(self, cr, uid, ids, prop, unknow_none, context=None):
440 """ Finds production planned date.
441 @param prop: Name of field.
443 @return: Dictionary of values.
446 for prod in self.browse(cr, uid, ids, context=context):
447 result[prod.id] = prod.date_planned[:10]
451 'name': fields.char('Reference', size=64, required=True),
452 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
453 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', select=True),
455 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft':[('readonly',False)]}),
456 'product_qty': fields.float('Product Qty', digits_compute=dp.get_precision('Product UoM'), required=True, states={'draft':[('readonly',False)]}, readonly=True),
457 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
458 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
459 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
461 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
462 readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will look for components."),
463 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
464 readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will stock the finished products."),
466 'date_planned_end': fields.function(_production_date_end, type='date', string='Scheduled End Date'),
467 'date_planned_date': fields.function(_production_date, type='date', string='Scheduled Date'),
468 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
469 'date_start': fields.datetime('Start Date', select=True),
470 'date_finished': fields.datetime('End Date', select=True),
472 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]}),
473 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]}, help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production plannification."),
474 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True, ondelete="restrict",
475 help="This is the Internal Picking List that brings the finished product to the production plan"),
476 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
477 '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)]}),
478 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
479 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
480 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products', domain=[('state','in', ('done', 'cancel'))]),
481 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
482 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
483 'state': fields.selection([('draft','New'),('picking_except', 'Picking Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','Production Started'),('cancel','Cancelled'),('done','Done')],'State', readonly=True,
484 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\'.\
485 \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\'.'),
486 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
487 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
488 'user_id':fields.many2one('res.users', 'Responsible'),
489 'company_id': fields.many2one('res.company','Company',required=True),
492 'priority': lambda *a: '1',
493 'state': lambda *a: 'draft',
494 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
495 'product_qty': lambda *a: 1.0,
496 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
497 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
500 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
502 _order = 'priority desc, date_planned asc';
504 def _check_qty(self, cr, uid, ids, context=None):
505 for order in self.browse(cr, uid, ids, context=context):
506 if order.product_qty <= 0:
511 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
514 def create(self, cr, uid, vals, context=None):
515 obj_id = super(mrp_production, self).create(cr, uid, vals, context=context)
516 self.create_send_note(cr, uid, [obj_id], context=context)
519 def unlink(self, cr, uid, ids, context=None):
520 for production in self.browse(cr, uid, ids, context=context):
521 if production.state not in ('draft', 'cancel'):
522 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a manufacturing order in state \'%s\'') % production.state)
523 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
525 def copy(self, cr, uid, id, default=None, context=None):
529 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
532 'move_created_ids' : [],
533 'move_created_ids2' : [],
534 'product_lines' : [],
537 return super(mrp_production, self).copy(cr, uid, id, default, context)
539 def location_id_change(self, cr, uid, ids, src, dest, context=None):
540 """ Changes destination location if source location is changed.
541 @param src: Source location id.
542 @param dest: Destination location id.
543 @return: Dictionary of values.
548 return {'value': {'location_dest_id': src}}
551 def product_id_change(self, cr, uid, ids, product_id, context=None):
552 """ Finds UoM of changed product.
553 @param product_id: Id of changed product.
554 @return: Dictionary of values.
558 'product_uom': False,
562 bom_obj = self.pool.get('mrp.bom')
563 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
564 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
567 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
568 routing_id = bom_point.routing_id.id or False
570 'product_uom': product.uom_id and product.uom_id.id or False,
572 'routing_id': routing_id
574 return {'value': result}
576 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
577 """ Finds routing for changed BoM.
578 @param product: Id of product.
579 @return: Dictionary of values.
585 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
586 routing_id = bom_point.routing_id.id or False
588 'routing_id': routing_id
590 return {'value': result}
592 def action_picking_except(self, cr, uid, ids):
593 """ Changes the state to Exception.
596 self.write(cr, uid, ids, {'state': 'picking_except'})
599 def action_compute(self, cr, uid, ids, properties=[], context=None):
600 """ Computes bills of material of a product.
601 @param properties: List containing dictionaries of properties.
602 @return: No. of products.
605 bom_obj = self.pool.get('mrp.bom')
606 uom_obj = self.pool.get('product.uom')
607 prod_line_obj = self.pool.get('mrp.production.product.line')
608 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
609 for production in self.browse(cr, uid, ids):
610 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
611 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
612 bom_point = production.bom_id
613 bom_id = production.bom_id.id
615 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
617 bom_point = bom_obj.browse(cr, uid, bom_id)
618 routing_id = bom_point.routing_id.id or False
619 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
622 raise osv.except_osv(_('Error'), _("Couldn't find a bill of material for this product."))
623 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
624 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
628 line['production_id'] = production.id
629 prod_line_obj.create(cr, uid, line)
630 for line in results2:
631 line['production_id'] = production.id
632 workcenter_line_obj.create(cr, uid, line)
635 def action_cancel(self, cr, uid, ids, context=None):
636 """ Cancels the production order and related stock moves.
641 move_obj = self.pool.get('stock.move')
642 for production in self.browse(cr, uid, ids, context=context):
643 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
644 raise osv.except_osv(
645 _('Could not cancel manufacturing order !'),
646 _('You must first cancel related internal picking attached to this manufacturing order.'))
647 if production.move_created_ids:
648 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
649 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
650 self.write(cr, uid, ids, {'state': 'cancel'})
651 self.action_cancel_send_note(cr, uid, ids, context)
654 def action_ready(self, cr, uid, ids, context=None):
655 """ Changes the production state to Ready and location id of stock move.
658 move_obj = self.pool.get('stock.move')
659 self.write(cr, uid, ids, {'state': 'ready'})
661 for (production_id,name) in self.name_get(cr, uid, ids):
662 production = self.browse(cr, uid, production_id)
663 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
664 move_obj.write(cr, uid, [production.move_prod_id.id],
665 {'location_id': production.location_dest_id.id})
666 self.action_ready_send_note(cr, uid, [production_id], context)
669 def action_production_end(self, cr, uid, ids, context=None):
670 """ Changes production state to Finish and writes finished date.
673 for production in self.browse(cr, uid, ids):
674 self._costs_generate(cr, uid, production)
675 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
676 self.action_done_send_note(cr, uid, ids, context)
679 def test_production_done(self, cr, uid, ids):
680 """ Tests whether production is done or not.
681 @return: True or False
684 for production in self.browse(cr, uid, ids):
685 if production.move_lines:
688 if production.move_created_ids:
692 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
693 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
694 it's always equal to the quantity encoded in the production order or the production wizard, but if the
695 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
697 :param production_id: ID of the mrp.order
698 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
699 :return: The factor to apply to the quantity that we should produce for the given production order.
703 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
704 """ To produce final product based on production mode (consume/consume&produce).
705 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
706 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
707 and stock move lines of final product will be also done/produced.
708 @param production_id: the ID of mrp.production object
709 @param production_qty: specify qty to produce
710 @param production_mode: specify production mode (consume/consume&produce).
713 stock_mov_obj = self.pool.get('stock.move')
714 production = self.browse(cr, uid, production_id, context=context)
717 for produced_product in production.move_created_ids2:
718 if (produced_product.scrapped) or (produced_product.product_id.id <> production.product_id.id):
720 produced_qty += produced_product.product_qty
721 if production_mode in ['consume','consume_produce']:
724 # Calculate already consumed qtys
725 for consumed in production.move_lines2:
726 if consumed.scrapped:
728 if not consumed_data.get(consumed.product_id.id, False):
729 consumed_data[consumed.product_id.id] = 0
730 consumed_data[consumed.product_id.id] += consumed.product_qty
732 # Find product qty to be consumed and consume it
733 for scheduled in production.product_lines:
735 # total qty of consumed product we need after this consumption
736 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
738 # qty available for consume and produce
739 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
742 # there will be nothing to consume for this raw material
745 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
747 # qtys we have to consume
748 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
751 # if qtys we have to consume is more than qtys available to consume
752 prod_name = scheduled.product_id.name_get()[0][1]
753 raise osv.except_osv(_('Warning!'), _('You are going to consume total %s quantities of "%s".\nBut you can only consume up to total %s quantities.') % (qty, prod_name, qty_avail))
755 # we already have more qtys consumed than we need
758 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
760 if production_mode == 'consume_produce':
761 # To produce remaining qty of final product
762 #vals = {'state':'confirmed'}
763 #final_product_todo = [x.id for x in production.move_created_ids]
764 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
765 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
766 produced_products = {}
767 for produced_product in production.move_created_ids2:
768 if produced_product.scrapped:
770 if not produced_products.get(produced_product.product_id.id, False):
771 produced_products[produced_product.product_id.id] = 0
772 produced_products[produced_product.product_id.id] += produced_product.product_qty
774 for produce_product in production.move_created_ids:
775 produced_qty = produced_products.get(produce_product.product_id.id, 0)
776 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
777 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
779 if rest_qty < production_qty:
780 prod_name = produce_product.product_id.name_get()[0][1]
781 raise osv.except_osv(_('Warning!'), _('You are going to produce total %s quantities of "%s".\nBut you can only produce up to total %s quantities.') % (production_qty, prod_name, rest_qty))
783 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
785 for raw_product in production.move_lines2:
787 parent_move_ids = [x.id for x in raw_product.move_history_ids]
788 for final_product in production.move_created_ids2:
789 if final_product.id not in parent_move_ids:
790 new_parent_ids.append(final_product.id)
791 for new_parent_id in new_parent_ids:
792 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
794 wf_service = netsvc.LocalService("workflow")
795 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
798 def _costs_generate(self, cr, uid, production):
799 """ Calculates total costs at the end of the production.
800 @param production: Id of production order.
801 @return: Calculated amount.
804 analytic_line_obj = self.pool.get('account.analytic.line')
805 for wc_line in production.workcenter_lines:
806 wc = wc_line.workcenter_id
807 if wc.costs_journal_id and wc.costs_general_account_id:
808 value = wc_line.hour * wc.costs_hour
809 account = wc.costs_hour_account_id.id
810 if value and account:
812 analytic_line_obj.create(cr, uid, {
813 'name': wc_line.name + ' (H)',
815 'account_id': account,
816 'general_account_id': wc.costs_general_account_id.id,
817 'journal_id': wc.costs_journal_id.id,
819 'product_id': wc.product_id.id,
820 'unit_amount': wc_line.hour,
821 'product_uom_id': wc.product_id.uom_id.id
823 if wc.costs_journal_id and wc.costs_general_account_id:
824 value = wc_line.cycle * wc.costs_cycle
825 account = wc.costs_cycle_account_id.id
826 if value and account:
828 analytic_line_obj.create(cr, uid, {
829 'name': wc_line.name+' (C)',
831 'account_id': account,
832 'general_account_id': wc.costs_general_account_id.id,
833 'journal_id': wc.costs_journal_id.id,
835 'product_id': wc.product_id.id,
836 'unit_amount': wc_line.cycle,
837 'product_uom_id': wc.product_id.uom_id.id
841 def action_in_production(self, cr, uid, ids, context=None):
842 """ Changes state to In Production and writes starting date.
845 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
846 self.action_in_production_send_note(cr, uid, ids, context)
849 def test_if_product(self, cr, uid, ids):
851 @return: True or False
854 for production in self.browse(cr, uid, ids):
855 if not production.product_lines:
856 if not self.action_compute(cr, uid, [production.id]):
860 def _get_auto_picking(self, cr, uid, production):
863 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
864 wf_service = netsvc.LocalService("workflow")
865 procurement_order = self.pool.get('procurement.order')
866 production = production_line.production_id
867 location_id = production.location_src_id.id
868 date_planned = production.date_planned
869 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
870 procurement_id = procurement_order.create(cr, uid, {
871 'name': procurement_name,
872 'origin': procurement_name,
873 'date_planned': date_planned,
874 'product_id': production_line.product_id.id,
875 'product_qty': production_line.product_qty,
876 'product_uom': production_line.product_uom.id,
877 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
878 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
879 'location_id': location_id,
880 'procure_method': production_line.product_id.procure_method,
881 'move_id': shipment_move_id,
882 'company_id': production.company_id.id,
884 wf_service.trg_validate(uid, procurement_order._name, procurement_id, 'button_confirm', cr)
885 return procurement_id
887 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
888 stock_move = self.pool.get('stock.move')
889 production = production_line.production_id
890 date_planned = production.date_planned
891 # Internal shipment is created for Stockable and Consumer Products
892 if production_line.product_id.type not in ('product', 'consu'):
894 move_name = _('PROD: %s') % production.name
895 source_location_id = production.location_src_id.id
896 if not destination_location_id:
897 destination_location_id = source_location_id
898 return stock_move.create(cr, uid, {
900 'picking_id': shipment_id,
901 'product_id': production_line.product_id.id,
902 'product_qty': production_line.product_qty,
903 'product_uom': production_line.product_uom.id,
904 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
905 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
906 'date': date_planned,
907 'move_dest_id': parent_move_id,
908 'location_id': source_location_id,
909 'location_dest_id': destination_location_id,
911 'company_id': production.company_id.id,
914 def _make_production_internal_shipment(self, cr, uid, production, context=None):
915 ir_sequence = self.pool.get('ir.sequence')
916 stock_picking = self.pool.get('stock.picking')
918 pick_type = 'internal'
921 # Take routing address as a Shipment Address.
922 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
923 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
924 routing_loc = production.bom_id.routing_id.location_id
925 if routing_loc.usage <> 'internal':
927 partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
929 # Take next Sequence number of shipment base on type
930 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
932 picking_id = stock_picking.create(cr, uid, {
934 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
938 'partner_id': partner_id,
939 'auto_picking': self._get_auto_picking(cr, uid, production),
940 'company_id': production.company_id.id,
942 production.write({'picking_id': picking_id}, context=context)
945 def _make_production_produce_line(self, cr, uid, production, context=None):
946 stock_move = self.pool.get('stock.move')
947 source_location_id = production.product_id.product_tmpl_id.property_stock_production.id
948 destination_location_id = production.location_dest_id.id
949 move_name = _('PROD: %s') + production.name
952 'date': production.date_planned,
953 'product_id': production.product_id.id,
954 'product_qty': production.product_qty,
955 'product_uom': production.product_uom.id,
956 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
957 'product_uos': production.product_uos and production.product_uos.id or False,
958 'location_id': source_location_id,
959 'location_dest_id': destination_location_id,
960 'move_dest_id': production.move_prod_id.id,
962 'company_id': production.company_id.id,
964 move_id = stock_move.create(cr, uid, data, context=context)
965 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
968 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
969 stock_move = self.pool.get('stock.move')
970 production = production_line.production_id
971 # Internal shipment is created for Stockable and Consumer Products
972 if production_line.product_id.type not in ('product', 'consu'):
974 move_name = _('PROD: %s') % production.name
975 destination_location_id = production.product_id.product_tmpl_id.property_stock_production.id
976 if not source_location_id:
977 source_location_id = production.location_src_id.id
978 move_id = stock_move.create(cr, uid, {
980 'date': production.date_planned,
981 'product_id': production_line.product_id.id,
982 'product_qty': production_line.product_qty,
983 'product_uom': production_line.product_uom.id,
984 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
985 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
986 'location_id': source_location_id,
987 'location_dest_id': destination_location_id,
988 'move_dest_id': parent_move_id,
990 'company_id': production.company_id.id,
992 production.write({'move_lines': [(4, move_id)]}, context=context)
995 def action_confirm(self, cr, uid, ids, context=None):
996 """ Confirms production order.
997 @return: Newly generated Shipment Id.
1000 wf_service = netsvc.LocalService("workflow")
1001 uncompute_ids = filter(lambda x:x, [not x.product_lines and x.id or False for x in self.browse(cr, uid, ids, context=context)])
1002 self.action_compute(cr, uid, uncompute_ids, context=context)
1003 for production in self.browse(cr, uid, ids, context=context):
1004 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
1005 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
1007 # Take routing location as a Source Location.
1008 source_location_id = production.location_src_id.id
1009 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1010 source_location_id = production.bom_id.routing_id.location_id.id
1012 for line in production.product_lines:
1013 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1014 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1015 destination_location_id=source_location_id, context=context)
1016 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1018 wf_service.trg_validate(uid, 'stock.picking', shipment_id, 'button_confirm', cr)
1019 production.write({'state':'confirmed'}, context=context)
1020 self.action_confirm_send_note(cr, uid, [production.id], context);
1023 def force_production(self, cr, uid, ids, *args):
1024 """ Assigns products.
1025 @param *args: Arguments
1028 pick_obj = self.pool.get('stock.picking')
1029 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1032 # ---------------------------------------------------
1033 # OpenChatter methods and notifications
1034 # ---------------------------------------------------
1036 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1037 result = dict.fromkeys(ids, [])
1038 for obj in self.browse(cr, uid, ids, context=context):
1039 if obj.state == 'draft' and obj.user_id:
1040 result[obj.id] = [obj.user_id.id]
1043 def message_get_subscribers(self, cr, uid, ids, context=None):
1044 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1045 for obj in self.browse(cr, uid, ids, context=context):
1047 sub_ids.append(obj.user_id.id)
1048 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1050 def create_send_note(self, cr, uid, ids, context=None):
1051 self.message_append_note(cr, uid, ids, body=_("Manufacturing order has been <b>created</b>."), context=context)
1054 def action_cancel_send_note(self, cr, uid, ids, context=None):
1055 message = _("Manufacturing order has been <b>canceled</b>.")
1056 self.message_append_note(cr, uid, ids, body=message, context=context)
1059 def action_ready_send_note(self, cr, uid, ids, context=None):
1060 message = _("Manufacturing order is <b>ready to produce</b>.")
1061 self.message_append_note(cr, uid, ids, body=message, context=context)
1064 def action_in_production_send_note(self, cr, uid, ids, context=None):
1065 message = _("Manufacturing order is <b>in production</b>.")
1066 self.message_append_note(cr, uid, ids, body=message, context=context)
1069 def action_done_send_note(self, cr, uid, ids, context=None):
1070 message = _("Manufacturing order has been <b>done</b>.")
1071 self.message_append_note(cr, uid, ids, body=message, context=context)
1074 def action_confirm_send_note(self, cr, uid, ids, context=None):
1075 for obj in self.browse(cr, uid, ids, context=context):
1076 message = _("Manufacturing order has been <b>confirmed</b> and is <b>scheduled</b> for the <em>%s</em>.") % (obj.date_planned)
1077 self.message_append_note(cr, uid, [obj.id], body=message, context=context)
1083 class mrp_production_workcenter_line(osv.osv):
1084 _name = 'mrp.production.workcenter.line'
1085 _description = 'Work Order'
1087 _inherit = ['mail.thread']
1090 'name': fields.char('Work Order', size=64, required=True),
1091 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1092 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
1093 'hour': fields.float('Nbr of hours', digits=(16,2)),
1094 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1095 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
1098 'sequence': lambda *a: 1,
1099 'hour': lambda *a: 0,
1100 'cycle': lambda *a: 0,
1102 mrp_production_workcenter_line()
1104 class mrp_production_product_line(osv.osv):
1105 _name = 'mrp.production.product.line'
1106 _description = 'Production Scheduled Product'
1108 'name': fields.char('Name', size=64, required=True),
1109 'product_id': fields.many2one('product.product', 'Product', required=True),
1110 'product_qty': fields.float('Product Qty', digits_compute=dp.get_precision('Product UoM'), required=True),
1111 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1112 'product_uos_qty': fields.float('Product UOS Qty'),
1113 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1114 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1116 mrp_production_product_line()
1118 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: