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 ##############################################################################
23 from datetime import datetime
25 import openerp.addons.decimal_precision as dp
26 from openerp.osv import fields, osv
27 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
28 from openerp.tools import float_compare
29 from openerp.tools.translate import _
30 from openerp import tools
32 #----------------------------------------------------------
34 #----------------------------------------------------------
35 # capacity_hour : capacity per hour. default: 1.0.
36 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
37 # unit_per_cycle : how many units are produced for one cycle
39 class mrp_workcenter(osv.osv):
40 _name = 'mrp.workcenter'
41 _description = 'Work Center'
42 _inherits = {'resource.resource':"resource_id"}
44 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
45 '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."),
46 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
47 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
48 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
49 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
50 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
51 help="Fill this only if you want automatic analytic accounting entries on production orders."),
52 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
53 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
54 help="Fill this only if you want automatic analytic accounting entries on production orders."),
55 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
56 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
57 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
58 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
61 'capacity_per_cycle': 1.0,
62 'resource_type': 'material',
65 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
69 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
70 value = {'costs_hour': cost.standard_price}
71 return {'value': value}
76 class mrp_routing(osv.osv):
78 For specifying the routings of Work Centers.
81 _description = 'Routing'
83 'name': fields.char('Name', size=64, required=True),
84 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
85 'code': fields.char('Code', size=8),
87 'note': fields.text('Description'),
88 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
90 'location_id': fields.many2one('stock.location', 'Production Location',
91 help="Keep empty if you produce at the location where the finished products are needed." \
92 "Set a location if you produce at a fixed location. This can be a partner location " \
93 "if you subcontract the manufacturing operations."
95 'company_id': fields.many2one('res.company', 'Company'),
98 'active': lambda *a: 1,
99 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
103 class mrp_routing_workcenter(osv.osv):
105 Defines working cycles and hours of a Work Center using routings.
107 _name = 'mrp.routing.workcenter'
108 _description = 'Work Center Usage'
111 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
112 'name': fields.char('Name', size=64, required=True),
113 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
114 'cycle_nbr': fields.float('Number of Cycles', required=True,
115 help="Number of iterations this work center has to do in the specified operation of the routing."),
116 '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."),
117 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
118 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
119 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
120 'note': fields.text('Description'),
121 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
124 'cycle_nbr': lambda *a: 1.0,
125 'hour_nbr': lambda *a: 0.0,
127 mrp_routing_workcenter()
129 class mrp_bom(osv.osv):
131 Defines bills of material for a product.
134 _description = 'Bill of Material'
135 _inherit = ['mail.thread']
137 def _child_compute(self, cr, uid, ids, name, arg, context=None):
139 @param self: The object pointer
140 @param cr: The current row, from the database cursor,
141 @param uid: The current user ID for security checks
142 @param ids: List of selected IDs
143 @param name: Name of the field
144 @param arg: User defined argument
145 @param context: A standard dictionary for contextual values
146 @return: Dictionary of values
151 bom_obj = self.pool.get('mrp.bom')
152 bom_id = context and context.get('active_id', False) or False
153 cr.execute('select id from mrp_bom')
154 if all(bom_id != r[0] for r in cr.fetchall()):
157 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
158 for bom in self.browse(cr, uid, ids, context=context):
159 if (bom_parent) or (bom.id == bom_id):
160 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
165 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
166 if (bom.type=='phantom' or ok):
167 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
169 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
170 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
174 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
175 """ Sets particular method for the selected bom type.
176 @param field_name: Name of the field
177 @param arg: User defined argument
178 @return: Dictionary of values
180 res = dict.fromkeys(ids, False)
181 for line in self.browse(cr, uid, ids, context=context):
182 if line.type == 'phantom' and not line.bom_id:
185 if line.bom_lines or line.type == 'phantom':
187 if line.product_id.supply_method == 'produce':
188 if line.product_id.procure_method == 'make_to_stock':
189 res[line.id] = 'stock'
191 res[line.id] = 'order'
195 'name': fields.char('Name', size=64),
196 'code': fields.char('Reference', size=16),
197 '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."),
198 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
199 help= "If a by-product is used in several products, it can be useful to create its own BoM. "\
200 "Though if you don't want separated production orders for this by-product, select Set/Phantom as BoM type. "\
201 "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."),
202 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
203 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
204 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
205 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
206 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
207 'product_id': fields.many2one('product.product', 'Product', required=True),
208 'product_uos_qty': fields.float('Product UOS Qty'),
209 '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."),
210 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
211 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"),
212 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
213 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
214 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
215 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
216 '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."),
217 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
218 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
219 'company_id': fields.many2one('res.company','Company',required=True),
222 'active': lambda *a: 1,
223 'product_efficiency': lambda *a: 1.0,
224 'product_qty': lambda *a: 1.0,
225 'product_rounding': lambda *a: 0.0,
226 'type': lambda *a: 'normal',
227 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
230 _parent_name = "bom_id"
232 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
233 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
236 def _check_recursion(self, cr, uid, ids, context=None):
239 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
240 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
246 def _check_product(self, cr, uid, ids, context=None):
248 boms = self.browse(cr, uid, ids, context=context)
252 if bom.product_id.id in all_prod:
254 all_prod.append(bom.product_id.id)
255 lines = bom.bom_lines
257 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
259 return check_bom(boms)
262 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
263 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
266 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
267 """ Changes UoM and name if product_id changes.
268 @param name: Name of the field
269 @param product_id: Changed product_id
270 @return: Dictionary of changed values
273 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
274 return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}}
277 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
279 if not product_uom or not product_id:
281 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
282 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
283 if uom.category_id.id != product.uom_id.category_id.id:
284 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
285 res['value'].update({'product_uom': product.uom_id.id})
288 def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
289 """ Finds BoM for particular product and product uom.
290 @param product_id: Selected product.
291 @param product_uom: Unit of measure of a product.
292 @param properties: List of related properties.
293 @return: False or BoM id.
295 if properties is None:
297 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
298 ids = map(lambda x: x[0], cr.fetchall())
301 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
303 for prop_id in bom.property_ids:
304 if prop_id.id in properties:
306 if (prop > max_prop) or ((max_prop == 0) and not result):
311 def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
312 """ Finds Products and Work Centers for related BoM for manufacturing order.
313 @param bom: BoM of particular product.
314 @param factor: Factor of product UoM.
315 @param properties: A List of properties Ids.
316 @param addthis: If BoM found then True else False.
317 @param level: Depth level to find BoM lines starts from 10.
318 @return: result: List of dictionaries containing product details.
319 result2: List of dictionaries containing Work Center details.
321 routing_obj = self.pool.get('mrp.routing')
322 factor = factor / (bom.product_efficiency or 1.0)
323 factor = rounding(factor, bom.product_rounding)
324 if factor < bom.product_rounding:
325 factor = bom.product_rounding
329 if bom.type == 'phantom' and not bom.bom_lines:
330 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
333 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
334 result = result + res[0]
335 result2 = result2 + res[1]
340 if addthis and not bom.bom_lines:
343 'name': bom.product_id.name,
344 'product_id': bom.product_id.id,
345 'product_qty': bom.product_qty * factor,
346 'product_uom': bom.product_uom.id,
347 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
348 'product_uos': bom.product_uos and bom.product_uos.id or False,
350 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
352 for wc_use in routing.workcenter_lines:
353 wc = wc_use.workcenter_id
354 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
355 mult = (d + (m and 1.0 or 0.0))
356 cycle = mult * wc_use.cycle_nbr
358 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
359 'workcenter_id': wc.id,
360 'sequence': level+(wc_use.sequence or 0),
362 '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)),
364 for bom2 in bom.bom_lines:
365 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
366 result = result + res[0]
367 result2 = result2 + res[1]
368 return result, result2
370 def copy_data(self, cr, uid, id, default=None, context=None):
373 bom_data = self.read(cr, uid, id, [], context=context)
374 default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
375 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
382 return math.ceil(f / r) * r
384 class mrp_production(osv.osv):
386 Production Orders / Manufacturing Orders
388 _name = 'mrp.production'
389 _description = 'Manufacturing Order'
390 _date_name = 'date_planned'
391 _inherit = ['mail.thread', 'ir.needaction_mixin']
393 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
394 """ Calculates total hours and total no. of cycles for a production order.
395 @param prop: Name of field.
397 @return: Dictionary of values.
400 for prod in self.browse(cr, uid, ids, context=context):
405 for wc in prod.workcenter_lines:
406 result[prod.id]['hour_total'] += wc.hour
407 result[prod.id]['cycle_total'] += wc.cycle
410 def _src_id_default(self, cr, uid, ids, context=None):
411 src_location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
412 return src_location_id.id
414 def _dest_id_default(self, cr, uid, ids, context=None):
415 dest_location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
416 return dest_location_id.id
418 def _get_progress(self, cr, uid, ids, name, arg, context=None):
419 """ Return product quantity percentage """
420 result = dict.fromkeys(ids, 100)
421 for mrp_production in self.browse(cr, uid, ids, context=context):
422 if mrp_production.product_qty:
424 for move in mrp_production.move_created_ids2:
425 if not move.scrapped and move.product_id == mrp_production.product_id:
426 done += move.product_qty
427 result[mrp_production.id] = done / mrp_production.product_qty * 100
431 'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
432 'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
433 help="Reference of the document that generated this production order request."),
434 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority',
435 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
437 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
438 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
439 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
440 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
441 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
442 'progress': fields.function(_get_progress, type='float',
443 string='Production progress'),
445 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
446 readonly=True, states={'draft':[('readonly',False)]},
447 help="Location where the system will look for components."),
448 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
449 readonly=True, states={'draft':[('readonly',False)]},
450 help="Location where the system will stock the finished products."),
451 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft':[('readonly',False)]}),
452 'date_start': fields.datetime('Start Date', select=True, readonly=True),
453 'date_finished': fields.datetime('End Date', select=True, readonly=True),
454 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]},
455 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
456 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]},
457 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."),
458 'picking_id': fields.many2one('stock.picking', 'Picking List', readonly=True, ondelete="restrict",
459 help="This is the Internal Picking List that brings the finished product to the production plan"),
460 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
461 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume',
462 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
463 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products',
464 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
465 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
466 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
467 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
468 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
469 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
470 readonly=True, states={'draft':[('readonly',False)]}),
471 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
472 readonly=True, states={'draft':[('readonly',False)]}),
473 'state': fields.selection(
474 [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
475 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
476 string='Status', readonly=True,
477 track_visibility='onchange',
478 help="When the production order is created the status is set to 'Draft'.\n\
479 If the order is confirmed the status is set to 'Waiting Goods'.\n\
480 If any exceptions are there, the status is set to 'Picking Exception'.\n\
481 If the stock is available then the status is set to 'Ready to Produce'.\n\
482 When the production gets started then the status is set to 'In Production'.\n\
483 When the production is over, the status is set to 'Done'."),
484 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
485 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
486 'user_id':fields.many2one('res.users', 'Responsible'),
487 'company_id': fields.many2one('res.company','Company',required=True),
490 'priority': lambda *a: '1',
491 'state': lambda *a: 'draft',
492 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
493 'product_qty': lambda *a: 1.0,
494 'user_id': lambda self, cr, uid, c: uid,
495 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
496 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
497 'location_src_id': _src_id_default,
498 'location_dest_id': _dest_id_default
501 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
503 _order = 'priority desc, date_planned asc';
505 def _check_qty(self, cr, uid, ids, context=None):
506 for order in self.browse(cr, uid, ids, context=context):
507 if order.product_qty <= 0:
512 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
515 def unlink(self, cr, uid, ids, context=None):
516 for production in self.browse(cr, uid, ids, context=context):
517 if production.state not in ('draft', 'cancel'):
518 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
519 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
521 def copy(self, cr, uid, id, default=None, context=None):
525 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
528 'move_created_ids' : [],
529 'move_created_ids2' : [],
530 'product_lines' : [],
531 'move_prod_id' : False,
534 return super(mrp_production, self).copy(cr, uid, id, default, context)
536 def location_id_change(self, cr, uid, ids, src, dest, context=None):
537 """ Changes destination location if source location is changed.
538 @param src: Source location id.
539 @param dest: Destination location id.
540 @return: Dictionary of values.
545 return {'value': {'location_dest_id': src}}
548 def product_id_change(self, cr, uid, ids, product_id, context=None):
549 """ Finds UoM of changed product.
550 @param product_id: Id of changed product.
551 @return: Dictionary of values.
555 'product_uom': False,
559 bom_obj = self.pool.get('mrp.bom')
560 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
561 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
564 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
565 routing_id = bom_point.routing_id.id or False
567 product_uom_id = product.uom_id and product.uom_id.id or False
569 'product_uom': product_uom_id,
571 'routing_id': routing_id,
573 return {'value': result}
575 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
576 """ Finds routing for changed BoM.
577 @param product: Id of product.
578 @return: Dictionary of values.
584 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
585 routing_id = bom_point.routing_id.id or False
587 'routing_id': routing_id
589 return {'value': result}
591 def action_picking_except(self, cr, uid, ids):
592 """ Changes the state to Exception.
595 self.write(cr, uid, ids, {'state': 'picking_except'})
598 def action_compute(self, cr, uid, ids, properties=None, context=None):
599 """ Computes bills of material of a product.
600 @param properties: List containing dictionaries of properties.
601 @return: No. of products.
603 if properties is None:
606 bom_obj = self.pool.get('mrp.bom')
607 uom_obj = self.pool.get('product.uom')
608 prod_line_obj = self.pool.get('mrp.production.product.line')
609 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
610 for production in self.browse(cr, uid, ids):
611 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
612 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
613 bom_point = production.bom_id
614 bom_id = production.bom_id.id
616 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
618 bom_point = bom_obj.browse(cr, uid, bom_id)
619 routing_id = bom_point.routing_id.id or False
620 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
623 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
624 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
625 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
629 line['production_id'] = production.id
630 prod_line_obj.create(cr, uid, line)
631 for line in results2:
632 line['production_id'] = production.id
633 workcenter_line_obj.create(cr, uid, line)
636 def action_cancel(self, cr, uid, ids, context=None):
637 """ Cancels the production order and related stock moves.
642 move_obj = self.pool.get('stock.move')
643 for production in self.browse(cr, uid, ids, context=context):
644 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
645 raise osv.except_osv(
646 _('Cannot cancel manufacturing order!'),
647 _('You must first cancel related internal picking attached to this manufacturing order.'))
648 if production.move_created_ids:
649 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
650 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
651 self.write(cr, uid, ids, {'state': 'cancel'})
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})
668 def action_production_end(self, cr, uid, ids, context=None):
669 """ Changes production state to Finish and writes finished date.
672 for production in self.browse(cr, uid, ids):
673 self._costs_generate(cr, uid, production)
674 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
677 def test_production_done(self, cr, uid, ids):
678 """ Tests whether production is done or not.
679 @return: True or False
682 for production in self.browse(cr, uid, ids):
683 if production.move_lines:
686 if production.move_created_ids:
690 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
691 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
692 it's always equal to the quantity encoded in the production order or the production wizard, but if the
693 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
695 :param production_id: ID of the mrp.order
696 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
697 :return: The factor to apply to the quantity that we should produce for the given production order.
701 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
702 """ To produce final product based on production mode (consume/consume&produce).
703 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
704 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
705 and stock move lines of final product will be also done/produced.
706 @param production_id: the ID of mrp.production object
707 @param production_qty: specify qty to produce
708 @param production_mode: specify production mode (consume/consume&produce).
711 stock_mov_obj = self.pool.get('stock.move')
712 production = self.browse(cr, uid, production_id, context=context)
715 for produced_product in production.move_created_ids2:
716 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
718 produced_qty += produced_product.product_qty
719 if production_mode in ['consume','consume_produce']:
722 # Calculate already consumed qtys
723 for consumed in production.move_lines2:
724 if consumed.scrapped:
726 if not consumed_data.get(consumed.product_id.id, False):
727 consumed_data[consumed.product_id.id] = 0
728 consumed_data[consumed.product_id.id] += consumed.product_qty
730 # Find product qty to be consumed and consume it
731 for scheduled in production.product_lines:
733 # total qty of consumed product we need after this consumption
734 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
736 # qty available for consume and produce
737 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
740 # there will be nothing to consume for this raw material
743 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
745 # qtys we have to consume
746 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
747 if float_compare(qty, qty_avail, precision_rounding=scheduled.product_id.uom_id.rounding) == 1:
748 # if qtys we have to consume is more than qtys available to consume
749 prod_name = scheduled.product_id.name_get()[0][1]
750 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))
752 # we already have more qtys consumed than we need
755 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
757 if production_mode == 'consume_produce':
758 # To produce remaining qty of final product
759 #vals = {'state':'confirmed'}
760 #final_product_todo = [x.id for x in production.move_created_ids]
761 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
762 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
763 produced_products = {}
764 for produced_product in production.move_created_ids2:
765 if produced_product.scrapped:
767 if not produced_products.get(produced_product.product_id.id, False):
768 produced_products[produced_product.product_id.id] = 0
769 produced_products[produced_product.product_id.id] += produced_product.product_qty
771 for produce_product in production.move_created_ids:
772 produced_qty = produced_products.get(produce_product.product_id.id, 0)
773 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
774 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
776 if rest_qty < production_qty:
777 prod_name = produce_product.product_id.name_get()[0][1]
778 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))
780 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
782 for raw_product in production.move_lines2:
784 parent_move_ids = [x.id for x in raw_product.move_history_ids]
785 for final_product in production.move_created_ids2:
786 if final_product.id not in parent_move_ids:
787 new_parent_ids.append(final_product.id)
788 for new_parent_id in new_parent_ids:
789 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
790 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
791 self.signal_button_produce_done(cr, uid, [production_id])
794 def _costs_generate(self, cr, uid, production):
795 """ Calculates total costs at the end of the production.
796 @param production: Id of production order.
797 @return: Calculated amount.
800 analytic_line_obj = self.pool.get('account.analytic.line')
801 for wc_line in production.workcenter_lines:
802 wc = wc_line.workcenter_id
803 if wc.costs_journal_id and wc.costs_general_account_id:
805 value = wc_line.hour * wc.costs_hour
806 account = wc.costs_hour_account_id.id
807 if value and account:
809 analytic_line_obj.create(cr, uid, {
810 'name': wc_line.name + ' (H)',
812 'account_id': account,
813 'general_account_id': wc.costs_general_account_id.id,
814 'journal_id': wc.costs_journal_id.id,
816 'product_id': wc.product_id.id,
817 'unit_amount': wc_line.hour,
818 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
821 value = wc_line.cycle * wc.costs_cycle
822 account = wc.costs_cycle_account_id.id
823 if value and account:
825 analytic_line_obj.create(cr, uid, {
826 'name': wc_line.name+' (C)',
828 'account_id': account,
829 'general_account_id': wc.costs_general_account_id.id,
830 'journal_id': wc.costs_journal_id.id,
832 'product_id': wc.product_id.id,
833 'unit_amount': wc_line.cycle,
834 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
838 def action_in_production(self, cr, uid, ids, context=None):
839 """ Changes state to In Production and writes starting date.
842 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
844 def test_if_product(self, cr, uid, ids):
846 @return: True or False
849 for production in self.browse(cr, uid, ids):
850 if not production.product_lines:
851 if not self.action_compute(cr, uid, [production.id]):
855 def _get_auto_picking(self, cr, uid, production):
858 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
859 procurement_order = self.pool.get('procurement.order')
860 production = production_line.production_id
861 location_id = production.location_src_id.id
862 date_planned = production.date_planned
863 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
864 procurement_id = procurement_order.create(cr, uid, {
865 'name': procurement_name,
866 'origin': procurement_name,
867 'date_planned': date_planned,
868 'product_id': production_line.product_id.id,
869 'product_qty': production_line.product_qty,
870 'product_uom': production_line.product_uom.id,
871 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
872 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
873 'location_id': location_id,
874 'procure_method': production_line.product_id.procure_method,
875 'move_id': shipment_move_id,
876 'company_id': production.company_id.id,
878 self.signal_button_confirm(cr, uid, [procurement_id])
879 return procurement_id
881 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
882 stock_move = self.pool.get('stock.move')
883 production = production_line.production_id
884 date_planned = production.date_planned
885 # Internal shipment is created for Stockable and Consumer Products
886 if production_line.product_id.type not in ('product', 'consu'):
888 source_location_id = production.location_src_id.id
889 if not destination_location_id:
890 destination_location_id = source_location_id
891 return stock_move.create(cr, uid, {
892 'name': production.name,
893 'picking_id': shipment_id,
894 'product_id': production_line.product_id.id,
895 'product_qty': production_line.product_qty,
896 'product_uom': production_line.product_uom.id,
897 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
898 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
899 'date': date_planned,
900 'move_dest_id': parent_move_id,
901 'location_id': source_location_id,
902 'location_dest_id': destination_location_id,
904 'company_id': production.company_id.id,
907 def _make_production_internal_shipment(self, cr, uid, production, context=None):
908 ir_sequence = self.pool.get('ir.sequence')
909 stock_picking = self.pool.get('stock.picking')
911 pick_type = 'internal'
914 # Take routing address as a Shipment Address.
915 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
916 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
917 routing_loc = production.bom_id.routing_id.location_id
918 if routing_loc.usage != 'internal':
920 partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
922 # Take next Sequence number of shipment base on type
923 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
925 picking_id = stock_picking.create(cr, uid, {
927 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
931 'partner_id': partner_id,
932 'auto_picking': self._get_auto_picking(cr, uid, production),
933 'company_id': production.company_id.id,
935 production.write({'picking_id': picking_id}, context=context)
938 def _make_production_produce_line(self, cr, uid, production, context=None):
939 stock_move = self.pool.get('stock.move')
940 source_location_id = production.product_id.property_stock_production.id
941 destination_location_id = production.location_dest_id.id
943 'name': production.name,
944 'date': production.date_planned,
945 'product_id': production.product_id.id,
946 'product_qty': production.product_qty,
947 'product_uom': production.product_uom.id,
948 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
949 'product_uos': production.product_uos and production.product_uos.id or False,
950 'location_id': source_location_id,
951 'location_dest_id': destination_location_id,
952 'move_dest_id': production.move_prod_id.id,
954 'company_id': production.company_id.id,
956 move_id = stock_move.create(cr, uid, data, context=context)
957 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
960 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
961 stock_move = self.pool.get('stock.move')
962 production = production_line.production_id
963 # Internal shipment is created for Stockable and Consumer Products
964 if production_line.product_id.type not in ('product', 'consu'):
966 destination_location_id = production.product_id.property_stock_production.id
967 if not source_location_id:
968 source_location_id = production.location_src_id.id
969 move_id = stock_move.create(cr, uid, {
970 'name': production.name,
971 'date': production.date_planned,
972 'product_id': production_line.product_id.id,
973 'product_qty': production_line.product_qty,
974 'product_uom': production_line.product_uom.id,
975 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
976 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
977 'location_id': source_location_id,
978 'location_dest_id': destination_location_id,
979 'move_dest_id': parent_move_id,
981 'company_id': production.company_id.id,
983 production.write({'move_lines': [(4, move_id)]}, context=context)
986 def action_confirm(self, cr, uid, ids, context=None):
987 """ Confirms production order.
988 @return: Newly generated Shipment Id.
991 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)])
992 self.action_compute(cr, uid, uncompute_ids, context=context)
993 for production in self.browse(cr, uid, ids, context=context):
994 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
995 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
997 # Take routing location as a Source Location.
998 source_location_id = production.location_src_id.id
999 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1000 source_location_id = production.bom_id.routing_id.location_id.id
1002 for line in production.product_lines:
1003 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1004 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1005 destination_location_id=source_location_id, context=context)
1006 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1008 self.pool.get('stock.picking').signal_button_confirm(cr, uid, [shipment_id])
1009 production.write({'state':'confirmed'}, context=context)
1012 def force_production(self, cr, uid, ids, *args):
1013 """ Assigns products.
1014 @param *args: Arguments
1017 pick_obj = self.pool.get('stock.picking')
1018 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1022 class mrp_production_workcenter_line(osv.osv):
1023 _name = 'mrp.production.workcenter.line'
1024 _description = 'Work Order'
1026 _inherit = ['mail.thread']
1029 'name': fields.char('Work Order', size=64, required=True),
1030 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1031 'cycle': fields.float('Number of Cycles', digits=(16,2)),
1032 'hour': fields.float('Number of Hours', digits=(16,2)),
1033 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1034 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1035 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1038 'sequence': lambda *a: 1,
1039 'hour': lambda *a: 0,
1040 'cycle': lambda *a: 0,
1043 class mrp_production_product_line(osv.osv):
1044 _name = 'mrp.production.product.line'
1045 _description = 'Production Scheduled Product'
1047 'name': fields.char('Name', size=64, required=True),
1048 'product_id': fields.many2one('product.product', 'Product', required=True),
1049 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1050 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1051 'product_uos_qty': fields.float('Product UOS Quantity'),
1052 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1053 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1056 class product_product(osv.osv):
1057 _inherit = "product.product"
1059 'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1062 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: