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, orm
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, SUPERUSER_ID
31 from openerp import SUPERUSER_ID
32 from openerp.addons.product import _common
34 #----------------------------------------------------------
36 #----------------------------------------------------------
37 # capacity_hour : capacity per hour. default: 1.0.
38 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
39 # unit_per_cycle : how many units are produced for one cycle
41 class mrp_workcenter(osv.osv):
42 _name = 'mrp.workcenter'
43 _description = 'Work Center'
44 _inherits = {'resource.resource':"resource_id"}
46 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
47 '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."),
48 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
49 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
50 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
51 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
52 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
53 help="Fill this only if you want automatic analytic accounting entries on production orders."),
54 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
55 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
56 help="Fill this only if you want automatic analytic accounting entries on production orders."),
57 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
58 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
59 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
60 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
63 'capacity_per_cycle': 1.0,
64 'resource_type': 'material',
67 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
71 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
72 value = {'costs_hour': cost.standard_price}
73 return {'value': value}
77 class mrp_routing(osv.osv):
79 For specifying the routings of Work Centers.
82 _description = 'Routing'
84 'name': fields.char('Name', size=64, required=True),
85 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
86 'code': fields.char('Code', size=8),
88 'note': fields.text('Description'),
89 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
91 'location_id': fields.many2one('stock.location', 'Production Location',
92 help="Keep empty if you produce at the location where the finished products are needed." \
93 "Set a location if you produce at a fixed location. This can be a partner location " \
94 "if you subcontract the manufacturing operations."
96 'company_id': fields.many2one('res.company', 'Company'),
99 'active': lambda *a: 1,
100 '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,
128 class mrp_bom(osv.osv):
130 Defines bills of material for a product.
133 _description = 'Bill of Material'
134 _inherit = ['mail.thread']
136 def _child_compute(self, cr, uid, ids, name, arg, context=None):
138 @param self: The object pointer
139 @param cr: The current row, from the database cursor,
140 @param uid: The current user ID for security checks
141 @param ids: List of selected IDs
142 @param name: Name of the field
143 @param arg: User defined argument
144 @param context: A standard dictionary for contextual values
145 @return: Dictionary of values
150 bom_obj = self.pool.get('mrp.bom')
151 bom_id = context and context.get('active_id', False) or False
152 cr.execute('select id from mrp_bom')
153 if all(bom_id != r[0] for r in cr.fetchall()):
156 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
157 for bom in self.browse(cr, uid, ids, context=context):
158 if (bom_parent) or (bom.id == bom_id):
159 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
164 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
165 if (bom.type=='phantom' or ok):
166 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
168 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
169 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
173 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
174 """ Sets particular method for the selected bom type.
175 @param field_name: Name of the field
176 @param arg: User defined argument
177 @return: Dictionary of values
179 res = dict.fromkeys(ids, False)
180 for line in self.browse(cr, uid, ids, context=context):
181 if line.type == 'phantom' and not line.bom_id:
184 if line.bom_lines or line.type == 'phantom':
186 if line.product_id.supply_method == 'produce':
187 if line.product_id.procure_method == 'make_to_stock':
188 res[line.id] = 'stock'
190 res[line.id] = 'order'
194 'name': fields.char('Name', size=64),
195 'code': fields.char('Reference', size=16),
196 '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."),
197 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
198 help= "If a by-product is used in several products, it can be useful to create its own BoM. "\
199 "Though if you don't want separated production orders for this by-product, select Set/Phantom as BoM type. "\
200 "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."),
201 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
202 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
203 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
204 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
205 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
206 'product_id': fields.many2one('product.product', 'Product', required=True),
207 'product_uos_qty': fields.float('Product UOS Qty'),
208 '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."),
209 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
210 '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"),
211 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
212 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
213 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
214 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
215 '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."),
216 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
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),
229 _parent_name = "bom_id"
231 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
232 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
235 def _check_recursion(self, cr, uid, ids, context=None):
238 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
239 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
245 def _check_product(self, cr, uid, ids, context=None):
247 boms = self.browse(cr, uid, ids, context=context)
251 if bom.product_id.id in all_prod:
253 all_prod.append(bom.product_id.id)
254 lines = bom.bom_lines
256 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
258 return check_bom(boms)
261 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
262 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
265 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
266 """ Changes UoM and name if product_id changes.
267 @param name: Name of the field
268 @param product_id: Changed product_id
269 @return: Dictionary of changed values
272 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
273 return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}}
276 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
278 if not product_uom or not product_id:
280 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
281 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
282 if uom.category_id.id != product.uom_id.category_id.id:
283 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
284 res['value'].update({'product_uom': product.uom_id.id})
287 def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
288 """ Finds BoM for particular product and product uom.
289 @param product_id: Selected product.
290 @param product_uom: Unit of measure of a product.
291 @param properties: List of related properties.
292 @return: False or BoM id.
294 if properties is None:
296 domain = [('product_id', '=', product_id), ('bom_id', '=', False),
297 '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
298 '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
299 ids = self.search(cr, uid, domain)
302 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
304 for prop_id in bom.property_ids:
305 if prop_id.id in properties:
307 if (prop > max_prop) or ((max_prop == 0) and not result):
312 def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
313 """ Finds Products and Work Centers for related BoM for manufacturing order.
314 @param bom: BoM of particular product.
315 @param factor: Factor of product UoM.
316 @param properties: A List of properties Ids.
317 @param addthis: If BoM found then True else False.
318 @param level: Depth level to find BoM lines starts from 10.
319 @return: result: List of dictionaries containing product details.
320 result2: List of dictionaries containing Work Center details.
322 routing_obj = self.pool.get('mrp.routing')
323 factor = factor / (bom.product_efficiency or 1.0)
324 factor = _common.ceiling(factor, bom.product_rounding)
325 if factor < bom.product_rounding:
326 factor = bom.product_rounding
330 if bom.type == 'phantom' and not bom.bom_lines:
331 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
334 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
335 result = result + res[0]
336 result2 = result2 + res[1]
341 if addthis and not bom.bom_lines:
344 'name': bom.product_id.name,
345 'product_id': bom.product_id.id,
346 'product_qty': bom.product_qty * factor,
347 'product_uom': bom.product_uom.id,
348 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
349 'product_uos': bom.product_uos and bom.product_uos.id or False,
351 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
353 for wc_use in routing.workcenter_lines:
354 wc = wc_use.workcenter_id
355 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
356 mult = (d + (m and 1.0 or 0.0))
357 cycle = mult * wc_use.cycle_nbr
359 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
360 'workcenter_id': wc.id,
361 'sequence': level+(wc_use.sequence or 0),
363 '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)),
365 for bom2 in bom.bom_lines:
366 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
367 result = result + res[0]
368 result2 = result2 + res[1]
369 return result, result2
371 def copy_data(self, cr, uid, id, default=None, context=None):
374 bom_data = self.read(cr, uid, id, [], context=context)
375 default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
376 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
380 # TODO for trunk: log deprecation warning
381 # _logger.warning("Deprecated rounding method, please use tools.float_round to round floats.")
385 return math.ceil(f / r) * r
387 class mrp_production(osv.osv):
389 Production Orders / Manufacturing Orders
391 _name = 'mrp.production'
392 _description = 'Manufacturing Order'
393 _date_name = 'date_planned'
394 _inherit = ['mail.thread', 'ir.needaction_mixin']
396 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
397 """ Calculates total hours and total no. of cycles for a production order.
398 @param prop: Name of field.
400 @return: Dictionary of values.
403 for prod in self.browse(cr, uid, ids, context=context):
408 for wc in prod.workcenter_lines:
409 result[prod.id]['hour_total'] += wc.hour
410 result[prod.id]['cycle_total'] += wc.cycle
413 def _src_id_default(self, cr, uid, ids, context=None):
415 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
416 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
417 except (orm.except_orm, ValueError):
421 def _dest_id_default(self, cr, uid, ids, context=None):
423 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
424 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
425 except (orm.except_orm, ValueError):
429 def _get_progress(self, cr, uid, ids, name, arg, context=None):
430 """ Return product quantity percentage """
431 result = dict.fromkeys(ids, 100)
432 for mrp_production in self.browse(cr, uid, ids, context=context):
433 if mrp_production.product_qty:
435 for move in mrp_production.move_created_ids2:
436 if not move.scrapped and move.product_id == mrp_production.product_id:
437 done += move.product_qty
438 result[mrp_production.id] = done / mrp_production.product_qty * 100
442 'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
443 'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
444 help="Reference of the document that generated this production order request."),
445 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority',
446 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
448 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
449 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
450 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
451 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
452 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
453 'progress': fields.function(_get_progress, type='float',
454 string='Production progress'),
456 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
457 readonly=True, states={'draft':[('readonly',False)]},
458 help="Location where the system will look for components."),
459 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
460 readonly=True, states={'draft':[('readonly',False)]},
461 help="Location where the system will stock the finished products."),
462 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft':[('readonly',False)]}),
463 'date_start': fields.datetime('Start Date', select=True, readonly=True),
464 'date_finished': fields.datetime('End Date', select=True, readonly=True),
465 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]},
466 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
467 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]},
468 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."),
469 'picking_id': fields.many2one('stock.picking', 'Picking List', readonly=True, ondelete="restrict",
470 help="This is the Internal Picking List that brings the finished product to the production plan"),
471 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
472 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume',
473 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
474 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products',
475 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
476 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
477 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
478 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
479 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
480 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
481 readonly=True, states={'draft':[('readonly',False)]}),
482 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
483 readonly=True, states={'draft':[('readonly',False)]}),
484 'state': fields.selection(
485 [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
486 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
487 string='Status', readonly=True,
488 track_visibility='onchange',
489 help="When the production order is created the status is set to 'Draft'.\n\
490 If the order is confirmed the status is set to 'Waiting Goods'.\n\
491 If any exceptions are there, the status is set to 'Picking Exception'.\n\
492 If the stock is available then the status is set to 'Ready to Produce'.\n\
493 When the production gets started then the status is set to 'In Production'.\n\
494 When the production is over, the status is set to 'Done'."),
495 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
496 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
497 'user_id':fields.many2one('res.users', 'Responsible'),
498 'company_id': fields.many2one('res.company','Company',required=True),
501 'priority': lambda *a: '1',
502 'state': lambda *a: 'draft',
503 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
504 'product_qty': lambda *a: 1.0,
505 'user_id': lambda self, cr, uid, c: uid,
506 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
507 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
508 'location_src_id': _src_id_default,
509 'location_dest_id': _dest_id_default
512 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
514 _order = 'priority desc, date_planned asc';
516 def _check_qty(self, cr, uid, ids, context=None):
517 for order in self.browse(cr, uid, ids, context=context):
518 if order.product_qty <= 0:
523 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
526 def unlink(self, cr, uid, ids, context=None):
527 for production in self.browse(cr, uid, ids, context=context):
528 if production.state not in ('draft', 'cancel'):
529 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
530 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
532 def copy(self, cr, uid, id, default=None, context=None):
536 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
539 'move_created_ids' : [],
540 'move_created_ids2' : [],
541 'product_lines' : [],
542 'move_prod_id' : False,
545 return super(mrp_production, self).copy(cr, uid, id, default, context)
547 def location_id_change(self, cr, uid, ids, src, dest, context=None):
548 """ Changes destination location if source location is changed.
549 @param src: Source location id.
550 @param dest: Destination location id.
551 @return: Dictionary of values.
556 return {'value': {'location_dest_id': src}}
559 def product_id_change(self, cr, uid, ids, product_id, context=None):
560 """ Finds UoM of changed product.
561 @param product_id: Id of changed product.
562 @return: Dictionary of values.
566 'product_uom': False,
570 bom_obj = self.pool.get('mrp.bom')
571 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
572 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
575 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
576 routing_id = bom_point.routing_id.id or False
578 product_uom_id = product.uom_id and product.uom_id.id or False
580 'product_uom': product_uom_id,
582 'routing_id': routing_id,
584 return {'value': result}
586 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
587 """ Finds routing for changed BoM.
588 @param product: Id of product.
589 @return: Dictionary of values.
595 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
596 routing_id = bom_point.routing_id.id or False
598 'routing_id': routing_id
600 return {'value': result}
602 def action_picking_except(self, cr, uid, ids):
603 """ Changes the state to Exception.
606 self.write(cr, uid, ids, {'state': 'picking_except'})
609 def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
610 """ Compute product_lines and workcenter_lines from BoM structure
611 @return: product_lines
614 if properties is None:
617 bom_obj = self.pool.get('mrp.bom')
618 uom_obj = self.pool.get('product.uom')
619 prod_line_obj = self.pool.get('mrp.production.product.line')
620 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
622 for production in self.browse(cr, uid, ids, context=context):
623 #unlink product_lines
624 prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
626 #unlink workcenter_lines
627 workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
629 # search BoM structure and route
630 bom_point = production.bom_id
631 bom_id = production.bom_id.id
633 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
635 bom_point = bom_obj.browse(cr, uid, bom_id)
636 routing_id = bom_point.routing_id.id or False
637 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
640 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
642 # get components and workcenter_lines from BoM structure
643 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
644 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
645 results = res[0] # product_lines
646 results2 = res[1] # workcenter_lines
648 # reset product_lines in production order
650 line['production_id'] = production.id
651 prod_line_obj.create(cr, uid, line)
653 #reset workcenter_lines in production order
654 for line in results2:
655 line['production_id'] = production.id
656 workcenter_line_obj.create(cr, uid, line)
659 def action_compute(self, cr, uid, ids, properties=None, context=None):
660 """ Computes bills of material of a product.
661 @param properties: List containing dictionaries of properties.
662 @return: No. of products.
664 return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
666 def action_cancel(self, cr, uid, ids, context=None):
667 """ Cancels the production order and related stock moves.
672 move_obj = self.pool.get('stock.move')
673 for production in self.browse(cr, uid, ids, context=context):
674 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
675 raise osv.except_osv(
676 _('Cannot cancel manufacturing order!'),
677 _('You must first cancel related internal picking attached to this manufacturing order.'))
678 if production.move_created_ids:
679 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
680 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
681 self.write(cr, uid, ids, {'state': 'cancel'})
684 def action_ready(self, cr, uid, ids, context=None):
685 """ Changes the production state to Ready and location id of stock move.
688 move_obj = self.pool.get('stock.move')
689 self.write(cr, uid, ids, {'state': 'ready'})
691 for production in self.browse(cr, uid, ids, context=context):
692 if not production.move_created_ids:
693 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
694 for scheduled in production.product_lines:
695 self._make_production_line_procurement(cr, uid, scheduled, False, context=context)
697 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
698 move_obj.write(cr, uid, [production.move_prod_id.id],
699 {'location_id': production.location_dest_id.id})
702 def action_production_end(self, cr, uid, ids, context=None):
703 """ Changes production state to Finish and writes finished date.
706 for production in self.browse(cr, uid, ids):
707 self._costs_generate(cr, uid, production)
708 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
711 def test_production_done(self, cr, uid, ids):
712 """ Tests whether production is done or not.
713 @return: True or False
716 for production in self.browse(cr, uid, ids):
717 if production.move_lines:
720 if production.move_created_ids:
724 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
725 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
726 it's always equal to the quantity encoded in the production order or the production wizard, but if the
727 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
729 :param production_id: ID of the mrp.order
730 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
731 :return: The factor to apply to the quantity that we should produce for the given production order.
735 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
736 """ To produce final product based on production mode (consume/consume&produce).
737 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
738 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
739 and stock move lines of final product will be also done/produced.
740 @param production_id: the ID of mrp.production object
741 @param production_qty: specify qty to produce
742 @param production_mode: specify production mode (consume/consume&produce).
745 stock_mov_obj = self.pool.get('stock.move')
746 production = self.browse(cr, uid, production_id, context=context)
748 if not production.move_lines and production.state == 'ready':
749 # trigger workflow if not products to consume (eg: services)
750 self.signal_button_produce(cr, uid, [production_id])
753 for produced_product in production.move_created_ids2:
754 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
756 produced_qty += produced_product.product_qty
757 if production_mode in ['consume','consume_produce']:
760 # Calculate already consumed qtys
761 for consumed in production.move_lines2:
762 if consumed.scrapped:
764 if not consumed_data.get(consumed.product_id.id, False):
765 consumed_data[consumed.product_id.id] = 0
766 consumed_data[consumed.product_id.id] += consumed.product_qty
768 # Find product qty to be consumed and consume it
769 for scheduled in production.product_lines:
771 # total qty of consumed product we need after this consumption
772 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
774 # qty available for consume and produce
775 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
777 if float_compare(qty_avail, 0, precision_rounding=scheduled.product_id.uom_id.rounding) <= 0:
778 # there will be nothing to consume for this raw material
781 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
783 # qtys we have to consume
784 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
785 if float_compare(qty, qty_avail, precision_rounding=scheduled.product_id.uom_id.rounding) == 1:
786 # if qtys we have to consume is more than qtys available to consume
787 prod_name = scheduled.product_id.name_get()[0][1]
788 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))
789 if float_compare(qty, 0, precision_rounding=scheduled.product_id.uom_id.rounding) <= 0:
790 # we already have more qtys consumed than we need
793 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
795 if production_mode == 'consume_produce':
796 # To produce remaining qty of final product
797 #vals = {'state':'confirmed'}
798 #final_product_todo = [x.id for x in production.move_created_ids]
799 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
800 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
801 produced_products = {}
802 for produced_product in production.move_created_ids2:
803 if produced_product.scrapped:
805 if not produced_products.get(produced_product.product_id.id, False):
806 produced_products[produced_product.product_id.id] = 0
807 produced_products[produced_product.product_id.id] += produced_product.product_qty
809 for produce_product in production.move_created_ids:
810 produced_qty = produced_products.get(produce_product.product_id.id, 0)
811 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
812 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
814 if rest_qty < (subproduct_factor * production_qty):
815 prod_name = produce_product.product_id.name_get()[0][1]
816 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.') % ((subproduct_factor * production_qty), prod_name, rest_qty))
818 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
820 for raw_product in production.move_lines2:
822 parent_move_ids = [x.id for x in raw_product.move_history_ids]
823 for final_product in production.move_created_ids2:
824 if final_product.id not in parent_move_ids:
825 new_parent_ids.append(final_product.id)
826 for new_parent_id in new_parent_ids:
827 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
828 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
829 self.signal_button_produce_done(cr, uid, [production_id])
832 def _costs_generate(self, cr, uid, production):
833 """ Calculates total costs at the end of the production.
834 @param production: Id of production order.
835 @return: Calculated amount.
838 analytic_line_obj = self.pool.get('account.analytic.line')
839 for wc_line in production.workcenter_lines:
840 wc = wc_line.workcenter_id
841 if wc.costs_journal_id and wc.costs_general_account_id:
843 value = wc_line.hour * wc.costs_hour
844 account = wc.costs_hour_account_id.id
845 if value and account:
847 # we user SUPERUSER_ID as we do not garantee an mrp user
848 # has access to account analytic lines but still should be
849 # able to produce orders
850 analytic_line_obj.create(cr, SUPERUSER_ID, {
851 'name': wc_line.name + ' (H)',
853 'account_id': account,
854 'general_account_id': wc.costs_general_account_id.id,
855 'journal_id': wc.costs_journal_id.id,
857 'product_id': wc.product_id.id,
858 'unit_amount': wc_line.hour,
859 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
862 value = wc_line.cycle * wc.costs_cycle
863 account = wc.costs_cycle_account_id.id
864 if value and account:
866 analytic_line_obj.create(cr, SUPERUSER_ID, {
867 'name': wc_line.name+' (C)',
869 'account_id': account,
870 'general_account_id': wc.costs_general_account_id.id,
871 'journal_id': wc.costs_journal_id.id,
873 'product_id': wc.product_id.id,
874 'unit_amount': wc_line.cycle,
875 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
879 def action_in_production(self, cr, uid, ids, context=None):
880 """ Changes state to In Production and writes starting date.
883 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
885 def test_if_product(self, cr, uid, ids):
887 @return: True or False
890 for production in self.browse(cr, uid, ids):
891 boms = self._action_compute_lines(cr, uid, [production.id])
894 product = self.pool.get('product.product').browse(cr, uid, bom['product_id'])
895 if product.type in ('product', 'consu'):
899 def _get_auto_picking(self, cr, uid, production):
902 def _hook_create_post_procurement(self, cr, uid, production, procurement_id, context=None):
905 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
906 procurement_order = self.pool.get('procurement.order')
907 production = production_line.production_id
908 location_id = production.location_src_id.id
909 date_planned = production.date_planned
910 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
911 procurement_id = procurement_order.create(cr, uid, {
912 'name': procurement_name,
913 'origin': procurement_name,
914 'date_planned': date_planned,
915 'product_id': production_line.product_id.id,
916 'product_qty': production_line.product_qty,
917 'product_uom': production_line.product_uom.id,
918 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
919 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
920 'location_id': location_id,
921 'procure_method': production_line.product_id.procure_method,
922 'move_id': shipment_move_id,
923 'company_id': production.company_id.id,
925 procurement_order.signal_button_confirm(cr, uid, [procurement_id])
926 return procurement_id
928 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
929 stock_move = self.pool.get('stock.move')
930 production = production_line.production_id
931 date_planned = production.date_planned
932 # Internal shipment is created for Stockable and Consumer Products
933 if production_line.product_id.type not in ('product', 'consu'):
935 source_location_id = production.location_src_id.id
936 if not destination_location_id:
937 destination_location_id = source_location_id
938 return stock_move.create(cr, uid, {
939 'name': production.name,
940 'picking_id': shipment_id,
941 'product_id': production_line.product_id.id,
942 'product_qty': production_line.product_qty,
943 'product_uom': production_line.product_uom.id,
944 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
945 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
946 'date': date_planned,
947 'move_dest_id': parent_move_id,
948 'location_id': source_location_id,
949 'location_dest_id': destination_location_id,
951 'company_id': production.company_id.id,
954 def _make_production_internal_shipment(self, cr, uid, production, context=None):
955 ir_sequence = self.pool.get('ir.sequence')
956 stock_picking = self.pool.get('stock.picking')
958 pick_type = 'internal'
961 # Take routing address as a Shipment Address.
962 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
963 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
964 routing_loc = production.bom_id.routing_id.location_id
965 if routing_loc.usage != 'internal':
967 partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
969 # Take next Sequence number of shipment base on type
970 if pick_type!='internal':
971 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
973 pick_name = ir_sequence.get(cr, uid, 'stock.picking')
975 picking_id = stock_picking.create(cr, uid, {
977 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
981 'partner_id': partner_id,
982 'auto_picking': self._get_auto_picking(cr, uid, production),
983 'company_id': production.company_id.id,
985 production.write({'picking_id': picking_id}, context=context)
988 def _make_production_produce_line(self, cr, uid, production, context=None):
989 stock_move = self.pool.get('stock.move')
990 source_location_id = production.product_id.property_stock_production.id
991 destination_location_id = production.location_dest_id.id
993 'name': production.name,
994 'date': production.date_planned,
995 'product_id': production.product_id.id,
996 'product_qty': production.product_qty,
997 'product_uom': production.product_uom.id,
998 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
999 'product_uos': production.product_uos and production.product_uos.id or False,
1000 'location_id': source_location_id,
1001 'location_dest_id': destination_location_id,
1002 'move_dest_id': production.move_prod_id.id,
1004 'company_id': production.company_id.id,
1006 if production.move_prod_id:
1007 production.move_prod_id.write({'location_id': destination_location_id})
1008 move_id = stock_move.create(cr, uid, data, context=context)
1009 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
1012 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
1013 stock_move = self.pool.get('stock.move')
1014 production = production_line.production_id
1015 # Internal shipment is created for Stockable and Consumer Products
1016 if production_line.product_id.type not in ('product', 'consu'):
1018 destination_location_id = production.product_id.property_stock_production.id
1019 if not source_location_id:
1020 source_location_id = production.location_src_id.id
1021 move_id = stock_move.create(cr, uid, {
1022 'name': production.name,
1023 'date': production.date_planned,
1024 'product_id': production_line.product_id.id,
1025 'product_qty': production_line.product_qty,
1026 'product_uom': production_line.product_uom.id,
1027 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
1028 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
1029 'location_id': source_location_id,
1030 'location_dest_id': destination_location_id,
1031 'move_dest_id': parent_move_id,
1033 'company_id': production.company_id.id,
1035 production.write({'move_lines': [(4, move_id)]}, context=context)
1038 def action_confirm(self, cr, uid, ids, context=None):
1039 """ Confirms production order.
1040 @return: Newly generated Shipment Id.
1043 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)])
1044 self.action_compute(cr, uid, uncompute_ids, context=context)
1045 for production in self.browse(cr, uid, ids, context=context):
1046 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
1047 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
1049 # Take routing location as a Source Location.
1050 source_location_id = production.location_src_id.id
1051 if production.routing_id and production.routing_id.location_id:
1052 source_location_id = production.routing_id.location_id.id
1054 for line in production.product_lines:
1055 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1057 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1058 destination_location_id=source_location_id, context=context)
1059 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1062 self.pool.get('stock.picking').signal_button_confirm(cr, uid, [shipment_id])
1063 production.write({'state':'confirmed'}, context=context)
1066 def force_production(self, cr, uid, ids, *args):
1067 """ Assigns products.
1068 @param *args: Arguments
1071 pick_obj = self.pool.get('stock.picking')
1072 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1076 class mrp_production_workcenter_line(osv.osv):
1077 _name = 'mrp.production.workcenter.line'
1078 _description = 'Work Order'
1080 _inherit = ['mail.thread']
1083 'name': fields.char('Work Order', size=64, required=True),
1084 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1085 'cycle': fields.float('Number of Cycles', digits=(16,2)),
1086 'hour': fields.float('Number of Hours', digits=(16,2)),
1087 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1088 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1089 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1092 'sequence': lambda *a: 1,
1093 'hour': lambda *a: 0,
1094 'cycle': lambda *a: 0,
1097 class mrp_production_product_line(osv.osv):
1098 _name = 'mrp.production.product.line'
1099 _description = 'Production Scheduled Product'
1101 'name': fields.char('Name', size=64, required=True),
1102 'product_id': fields.many2one('product.product', 'Product', required=True),
1103 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1104 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1105 'product_uos_qty': fields.float('Product UOS Quantity'),
1106 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1107 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1110 class product_product(osv.osv):
1111 _inherit = "product.product"
1113 'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1116 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: