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 netsvc
31 from openerp import tools
32 from openerp import SUPERUSER_ID
33 from openerp.addons.product import _common
35 #----------------------------------------------------------
37 #----------------------------------------------------------
38 # capacity_hour : capacity per hour. default: 1.0.
39 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
40 # unit_per_cycle : how many units are produced for one cycle
42 class mrp_workcenter(osv.osv):
43 _name = 'mrp.workcenter'
44 _description = 'Work Center'
45 _inherits = {'resource.resource':"resource_id"}
47 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
48 '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."),
49 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
50 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
51 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
52 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
53 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
54 help="Fill this only if you want automatic analytic accounting entries on production orders."),
55 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
56 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
57 help="Fill this only if you want automatic analytic accounting entries on production orders."),
58 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
59 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
60 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
61 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
64 'capacity_per_cycle': 1.0,
65 'resource_type': 'material',
68 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
72 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
73 value = {'costs_hour': cost.standard_price}
74 return {'value': value}
79 class mrp_routing(osv.osv):
81 For specifying the routings of Work Centers.
84 _description = 'Routing'
86 'name': fields.char('Name', size=64, required=True),
87 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
88 'code': fields.char('Code', size=8),
90 'note': fields.text('Description'),
91 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
93 'location_id': fields.many2one('stock.location', 'Production Location',
94 help="Keep empty if you produce at the location where the finished products are needed." \
95 "Set a location if you produce at a fixed location. This can be a partner location " \
96 "if you subcontract the manufacturing operations."
98 'company_id': fields.many2one('res.company', 'Company'),
101 'active': lambda *a: 1,
102 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
106 class mrp_routing_workcenter(osv.osv):
108 Defines working cycles and hours of a Work Center using routings.
110 _name = 'mrp.routing.workcenter'
111 _description = 'Work Center Usage'
114 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
115 'name': fields.char('Name', size=64, required=True),
116 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
117 'cycle_nbr': fields.float('Number of Cycles', required=True,
118 help="Number of iterations this work center has to do in the specified operation of the routing."),
119 '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."),
120 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
121 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
122 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
123 'note': fields.text('Description'),
124 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
127 'cycle_nbr': lambda *a: 1.0,
128 'hour_nbr': lambda *a: 0.0,
130 mrp_routing_workcenter()
132 class mrp_bom(osv.osv):
134 Defines bills of material for a product.
137 _description = 'Bill of Material'
138 _inherit = ['mail.thread']
140 def _child_compute(self, cr, uid, ids, name, arg, context=None):
142 @param self: The object pointer
143 @param cr: The current row, from the database cursor,
144 @param uid: The current user ID for security checks
145 @param ids: List of selected IDs
146 @param name: Name of the field
147 @param arg: User defined argument
148 @param context: A standard dictionary for contextual values
149 @return: Dictionary of values
154 bom_obj = self.pool.get('mrp.bom')
155 bom_id = context and context.get('active_id', False) or False
156 cr.execute('select id from mrp_bom')
157 if all(bom_id != r[0] for r in cr.fetchall()):
160 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
161 for bom in self.browse(cr, uid, ids, context=context):
162 if (bom_parent) or (bom.id == bom_id):
163 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
168 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
169 if (bom.type=='phantom' or ok):
170 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
172 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
173 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
177 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
178 """ Sets particular method for the selected bom type.
179 @param field_name: Name of the field
180 @param arg: User defined argument
181 @return: Dictionary of values
183 res = dict.fromkeys(ids, False)
184 for line in self.browse(cr, uid, ids, context=context):
185 if line.type == 'phantom' and not line.bom_id:
188 if line.bom_lines or line.type == 'phantom':
190 if line.product_id.supply_method == 'produce':
191 if line.product_id.procure_method == 'make_to_stock':
192 res[line.id] = 'stock'
194 res[line.id] = 'order'
198 'name': fields.char('Name', size=64),
199 'code': fields.char('Reference', size=16),
200 '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."),
201 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
202 help= "If a by-product is used in several products, it can be useful to create its own BoM. "\
203 "Though if you don't want separated production orders for this by-product, select Set/Phantom as BoM type. "\
204 "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."),
205 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
206 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
207 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
208 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
209 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
210 'product_id': fields.many2one('product.product', 'Product', required=True),
211 'product_uos_qty': fields.float('Product UOS Qty'),
212 '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."),
213 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
214 '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"),
215 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
216 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
217 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
218 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
219 '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."),
220 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
221 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
222 'company_id': fields.many2one('res.company','Company',required=True),
225 'active': lambda *a: 1,
226 'product_efficiency': lambda *a: 1.0,
227 'product_qty': lambda *a: 1.0,
228 'product_rounding': lambda *a: 0.0,
229 'type': lambda *a: 'normal',
230 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
233 _parent_name = "bom_id"
235 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
236 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
239 def _check_recursion(self, cr, uid, ids, context=None):
242 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
243 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
249 def _check_product(self, cr, uid, ids, context=None):
251 boms = self.browse(cr, uid, ids, context=context)
255 if bom.product_id.id in all_prod:
257 all_prod.append(bom.product_id.id)
258 lines = bom.bom_lines
260 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
262 return check_bom(boms)
265 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
266 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
269 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
270 """ Changes UoM and name if product_id changes.
271 @param name: Name of the field
272 @param product_id: Changed product_id
273 @return: Dictionary of changed values
276 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
277 return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}}
280 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
282 if not product_uom or not product_id:
284 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
285 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
286 if uom.category_id.id != product.uom_id.category_id.id:
287 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
288 res['value'].update({'product_uom': product.uom_id.id})
291 def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
292 """ Finds BoM for particular product and product uom.
293 @param product_id: Selected product.
294 @param product_uom: Unit of measure of a product.
295 @param properties: List of related properties.
296 @return: False or BoM id.
298 if properties is None:
300 domain = [('product_id', '=', product_id), ('bom_id', '=', False),
301 '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
302 '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
303 ids = self.search(cr, uid, domain)
306 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
308 for prop_id in bom.property_ids:
309 if prop_id.id in properties:
311 if (prop > max_prop) or ((max_prop == 0) and not result):
316 def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False, context=None):
317 """ Finds Products and Work Centers for related BoM for manufacturing order.
318 @param bom: BoM of particular product.
319 @param factor: Factor of product UoM.
320 @param properties: A List of properties Ids.
321 @param addthis: If BoM found then True else False.
322 @param level: Depth level to find BoM lines starts from 10.
323 @return: result: List of dictionaries containing product details.
324 result2: List of dictionaries containing Work Center details.
326 routing_obj = self.pool.get('mrp.routing')
327 factor = factor / (bom.product_efficiency or 1.0)
328 factor = _common.ceiling(factor, bom.product_rounding)
329 if factor < bom.product_rounding:
330 factor = bom.product_rounding
334 if bom.type == 'phantom' and not bom.bom_lines:
335 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
338 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10, context=context)
339 result = result + res[0]
340 result2 = result2 + res[1]
345 if addthis and not bom.bom_lines:
348 'name': bom.product_id.name,
349 'product_id': bom.product_id.id,
350 'product_qty': bom.product_qty * factor,
351 'product_uom': bom.product_uom.id,
352 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
353 'product_uos': bom.product_uos and bom.product_uos.id or False,
355 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
357 for wc_use in routing.workcenter_lines:
358 wc = wc_use.workcenter_id
359 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
360 mult = (d + (m and 1.0 or 0.0))
361 cycle = mult * wc_use.cycle_nbr
363 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
364 'workcenter_id': wc.id,
365 'sequence': level+(wc_use.sequence or 0),
367 '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)),
369 for bom2 in bom.bom_lines:
370 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10, context=context)
371 result = result + res[0]
372 result2 = result2 + res[1]
373 return result, result2
375 def copy_data(self, cr, uid, id, default=None, context=None):
378 bom_data = self.read(cr, uid, id, [], context=context)
379 default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
380 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
384 # TODO for trunk: log deprecation warning
385 # _logger.warning("Deprecated rounding method, please use tools.float_round to round floats.")
389 return math.ceil(f / r) * r
391 class mrp_production(osv.osv):
393 Production Orders / Manufacturing Orders
395 _name = 'mrp.production'
396 _description = 'Manufacturing Order'
397 _date_name = 'date_planned'
398 _inherit = ['mail.thread', 'ir.needaction_mixin']
400 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
401 """ Calculates total hours and total no. of cycles for a production order.
402 @param prop: Name of field.
404 @return: Dictionary of values.
407 for prod in self.browse(cr, uid, ids, context=context):
412 for wc in prod.workcenter_lines:
413 result[prod.id]['hour_total'] += wc.hour
414 result[prod.id]['cycle_total'] += wc.cycle
417 def _src_id_default(self, cr, uid, ids, context=None):
419 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
420 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
421 except (orm.except_orm, ValueError):
425 def _dest_id_default(self, cr, uid, ids, context=None):
427 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
428 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
429 except (orm.except_orm, ValueError):
434 'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
435 'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
436 help="Reference of the document that generated this production order request."),
437 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority',
438 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
440 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
441 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
442 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
443 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
444 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
446 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
447 readonly=True, states={'draft':[('readonly',False)]},
448 help="Location where the system will look for components."),
449 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
450 readonly=True, states={'draft':[('readonly',False)]},
451 help="Location where the system will stock the finished products."),
452 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft':[('readonly',False)]}),
453 'date_start': fields.datetime('Start Date', select=True, readonly=True),
454 'date_finished': fields.datetime('End Date', select=True, readonly=True),
455 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]},
456 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
457 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]},
458 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."),
459 'picking_id': fields.many2one('stock.picking', 'Picking List', readonly=True, ondelete="restrict",
460 help="This is the Internal Picking List that brings the finished product to the production plan"),
461 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
462 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume',
463 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
464 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products',
465 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
466 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
467 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
468 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
469 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
470 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
471 readonly=True, states={'draft':[('readonly',False)]}),
472 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
473 readonly=True, states={'draft':[('readonly',False)]}),
474 'state': fields.selection(
475 [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
476 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
477 string='Status', readonly=True,
478 track_visibility='onchange',
479 help="When the production order is created the status is set to 'Draft'.\n\
480 If the order is confirmed the status is set to 'Waiting Goods'.\n\
481 If any exceptions are there, the status is set to 'Picking Exception'.\n\
482 If the stock is available then the status is set to 'Ready to Produce'.\n\
483 When the production gets started then the status is set to 'In Production'.\n\
484 When the production is over, the status is set to 'Done'."),
485 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
486 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
487 'user_id':fields.many2one('res.users', 'Responsible'),
488 'company_id': fields.many2one('res.company','Company',required=True),
491 'priority': lambda *a: '1',
492 'state': lambda *a: 'draft',
493 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
494 'product_qty': lambda *a: 1.0,
495 'user_id': lambda self, cr, uid, c: uid,
496 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
497 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
498 'location_src_id': _src_id_default,
499 'location_dest_id': _dest_id_default
502 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
504 _order = 'priority desc, date_planned asc';
506 def _check_qty(self, cr, uid, ids, context=None):
507 for order in self.browse(cr, uid, ids, context=context):
508 if order.product_qty <= 0:
513 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
516 def unlink(self, cr, uid, ids, context=None):
517 for production in self.browse(cr, uid, ids, context=context):
518 if production.state not in ('draft', 'cancel'):
519 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
520 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
522 def copy(self, cr, uid, id, default=None, context=None):
526 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
529 'move_created_ids' : [],
530 'move_created_ids2' : [],
531 'product_lines' : [],
532 'move_prod_id' : False,
535 return super(mrp_production, self).copy(cr, uid, id, default, context)
537 def location_id_change(self, cr, uid, ids, src, dest, context=None):
538 """ Changes destination location if source location is changed.
539 @param src: Source location id.
540 @param dest: Destination location id.
541 @return: Dictionary of values.
546 return {'value': {'location_dest_id': src}}
549 def product_id_change(self, cr, uid, ids, product_id, context=None):
550 """ Finds UoM of changed product.
551 @param product_id: Id of changed product.
552 @return: Dictionary of values.
556 'product_uom': False,
560 bom_obj = self.pool.get('mrp.bom')
561 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
562 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
565 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
566 routing_id = bom_point.routing_id.id or False
568 product_uom_id = product.uom_id and product.uom_id.id or False
570 'product_uom': product_uom_id,
572 'routing_id': routing_id,
574 return {'value': result}
576 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
577 """ Finds routing for changed BoM.
578 @param product: Id of product.
579 @return: Dictionary of values.
585 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
586 routing_id = bom_point.routing_id.id or False
588 'routing_id': routing_id
590 return {'value': result}
592 def action_picking_except(self, cr, uid, ids):
593 """ Changes the state to Exception.
596 self.write(cr, uid, ids, {'state': 'picking_except'})
599 def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
600 """ Compute product_lines and workcenter_lines from BoM structure
601 @return: product_lines
604 if properties is None:
607 bom_obj = self.pool.get('mrp.bom')
608 uom_obj = self.pool.get('product.uom')
609 prod_line_obj = self.pool.get('mrp.production.product.line')
610 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
612 for production in self.browse(cr, uid, ids, context=context):
613 #unlink product_lines
614 prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
616 #unlink workcenter_lines
617 workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
619 # search BoM structure and route
620 bom_point = production.bom_id
621 bom_id = production.bom_id.id
623 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
625 bom_point = bom_obj.browse(cr, uid, bom_id)
626 routing_id = bom_point.routing_id.id or False
627 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
630 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
632 # get components and workcenter_lines from BoM structure
633 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
634 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id, context=context)
635 results = res[0] # product_lines
636 results2 = res[1] # workcenter_lines
638 # reset product_lines in production order
640 line['production_id'] = production.id
641 prod_line_obj.create(cr, uid, line)
643 #reset workcenter_lines in production order
644 for line in results2:
645 line['production_id'] = production.id
646 workcenter_line_obj.create(cr, uid, line)
649 def action_compute(self, cr, uid, ids, properties=None, context=None):
650 """ Computes bills of material of a product.
651 @param properties: List containing dictionaries of properties.
652 @return: No. of products.
654 return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
656 def action_cancel(self, cr, uid, ids, context=None):
657 """ Cancels the production order and related stock moves.
662 move_obj = self.pool.get('stock.move')
663 for production in self.browse(cr, uid, ids, context=context):
664 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
665 raise osv.except_osv(
666 _('Cannot cancel manufacturing order!'),
667 _('You must first cancel related internal picking attached to this manufacturing order.'))
668 if production.move_created_ids:
669 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
670 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
671 self.write(cr, uid, ids, {'state': 'cancel'})
674 def action_ready(self, cr, uid, ids, context=None):
675 """ Changes the production state to Ready and location id of stock move.
678 move_obj = self.pool.get('stock.move')
679 self.write(cr, uid, ids, {'state': 'ready'})
681 for production in self.browse(cr, uid, ids, context=context):
682 if not production.move_created_ids:
683 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
684 for scheduled in production.product_lines:
685 self._make_production_line_procurement(cr, uid, scheduled, False, context=context)
687 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
688 move_obj.write(cr, uid, [production.move_prod_id.id],
689 {'location_id': production.location_dest_id.id})
692 def action_production_end(self, cr, uid, ids, context=None):
693 """ Changes production state to Finish and writes finished date.
696 for production in self.browse(cr, uid, ids):
697 self._costs_generate(cr, uid, production)
698 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
701 def test_production_done(self, cr, uid, ids):
702 """ Tests whether production is done or not.
703 @return: True or False
706 for production in self.browse(cr, uid, ids):
707 if production.move_lines:
710 if production.move_created_ids:
714 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
715 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
716 it's always equal to the quantity encoded in the production order or the production wizard, but if the
717 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
719 :param production_id: ID of the mrp.order
720 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
721 :return: The factor to apply to the quantity that we should produce for the given production order.
725 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
726 """ To produce final product based on production mode (consume/consume&produce).
727 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
728 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
729 and stock move lines of final product will be also done/produced.
730 @param production_id: the ID of mrp.production object
731 @param production_qty: specify qty to produce
732 @param production_mode: specify production mode (consume/consume&produce).
735 stock_mov_obj = self.pool.get('stock.move')
736 production = self.browse(cr, uid, production_id, context=context)
738 wf_service = netsvc.LocalService("workflow")
739 if not production.move_lines and production.state == 'ready':
740 # trigger workflow if not products to consume (eg: services)
741 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce', cr)
744 for produced_product in production.move_created_ids2:
745 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
747 produced_qty += produced_product.product_qty
748 if production_mode in ['consume','consume_produce']:
751 # Calculate already consumed qtys
752 for consumed in production.move_lines2:
753 if consumed.scrapped:
755 if not consumed_data.get(consumed.product_id.id, False):
756 consumed_data[consumed.product_id.id] = 0
757 consumed_data[consumed.product_id.id] += consumed.product_qty
759 # Find product qty to be consumed and consume it
760 for scheduled in production.product_lines:
762 # total qty of consumed product we need after this consumption
763 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
765 # qty available for consume and produce
766 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
768 if float_compare(qty_avail, 0, precision_rounding=scheduled.product_id.uom_id.rounding) <= 0:
769 # there will be nothing to consume for this raw material
772 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
774 # qtys we have to consume
775 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
776 if float_compare(qty, qty_avail, precision_rounding=scheduled.product_id.uom_id.rounding) == 1:
777 # if qtys we have to consume is more than qtys available to consume
778 prod_name = scheduled.product_id.name_get()[0][1]
779 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))
780 if float_compare(qty, 0, precision_rounding=scheduled.product_id.uom_id.rounding) <= 0:
781 # we already have more qtys consumed than we need
784 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
786 if production_mode == 'consume_produce':
787 # To produce remaining qty of final product
788 #vals = {'state':'confirmed'}
789 #final_product_todo = [x.id for x in production.move_created_ids]
790 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
791 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
792 produced_products = {}
793 for produced_product in production.move_created_ids2:
794 if produced_product.scrapped:
796 if not produced_products.get(produced_product.product_id.id, False):
797 produced_products[produced_product.product_id.id] = 0
798 produced_products[produced_product.product_id.id] += produced_product.product_qty
800 for produce_product in production.move_created_ids:
801 produced_qty = produced_products.get(produce_product.product_id.id, 0)
802 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
803 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
805 if rest_qty < (subproduct_factor * production_qty):
806 prod_name = produce_product.product_id.name_get()[0][1]
807 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))
809 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
811 for raw_product in production.move_lines2:
813 parent_move_ids = [x.id for x in raw_product.move_history_ids]
814 for final_product in production.move_created_ids2:
815 if final_product.id not in parent_move_ids:
816 new_parent_ids.append(final_product.id)
817 for new_parent_id in new_parent_ids:
818 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
820 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
823 def _costs_generate(self, cr, uid, production):
824 """ Calculates total costs at the end of the production.
825 @param production: Id of production order.
826 @return: Calculated amount.
829 analytic_line_obj = self.pool.get('account.analytic.line')
830 for wc_line in production.workcenter_lines:
831 wc = wc_line.workcenter_id
832 if wc.costs_journal_id and wc.costs_general_account_id:
834 value = wc_line.hour * wc.costs_hour
835 account = wc.costs_hour_account_id.id
836 if value and account:
838 analytic_line_obj.create(cr, uid, {
839 'name': wc_line.name + ' (H)',
841 'account_id': account,
842 'general_account_id': wc.costs_general_account_id.id,
843 'journal_id': wc.costs_journal_id.id,
845 'product_id': wc.product_id.id,
846 'unit_amount': wc_line.hour,
847 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
850 value = wc_line.cycle * wc.costs_cycle
851 account = wc.costs_cycle_account_id.id
852 if value and account:
854 analytic_line_obj.create(cr, uid, {
855 'name': wc_line.name+' (C)',
857 'account_id': account,
858 'general_account_id': wc.costs_general_account_id.id,
859 'journal_id': wc.costs_journal_id.id,
861 'product_id': wc.product_id.id,
862 'unit_amount': wc_line.cycle,
863 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
867 def action_in_production(self, cr, uid, ids, context=None):
868 """ Changes state to In Production and writes starting date.
871 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
873 def test_if_product(self, cr, uid, ids):
875 @return: True or False
878 for production in self.browse(cr, uid, ids):
879 boms = self._action_compute_lines(cr, uid, [production.id])
882 product = self.pool.get('product.product').browse(cr, uid, bom['product_id'])
883 if product.type in ('product', 'consu'):
887 def _get_auto_picking(self, cr, uid, production):
890 def _hook_create_post_procurement(self, cr, uid, production, procurement_id, context=None):
893 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
894 wf_service = netsvc.LocalService("workflow")
895 procurement_order = self.pool.get('procurement.order')
896 production = production_line.production_id
897 location_id = production.location_src_id.id
898 date_planned = production.date_planned
899 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
900 procurement_id = procurement_order.create(cr, uid, {
901 'name': procurement_name,
902 'origin': procurement_name,
903 'date_planned': date_planned,
904 'product_id': production_line.product_id.id,
905 'product_qty': production_line.product_qty,
906 'product_uom': production_line.product_uom.id,
907 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
908 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
909 'location_id': location_id,
910 'procure_method': production_line.product_id.procure_method,
911 'move_id': shipment_move_id,
912 'company_id': production.company_id.id,
914 self._hook_create_post_procurement(cr, uid, production, procurement_id, context=context)
915 wf_service.trg_validate(uid, procurement_order._name, procurement_id, 'button_confirm', cr)
916 return procurement_id
918 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
919 stock_move = self.pool.get('stock.move')
920 production = production_line.production_id
921 date_planned = production.date_planned
922 # Internal shipment is created for Stockable and Consumer Products
923 if production_line.product_id.type not in ('product', 'consu'):
925 source_location_id = production.location_src_id.id
926 if not destination_location_id:
927 destination_location_id = source_location_id
928 return stock_move.create(cr, uid, {
929 'name': production.name,
930 'picking_id': shipment_id,
931 'product_id': production_line.product_id.id,
932 'product_qty': production_line.product_qty,
933 'product_uom': production_line.product_uom.id,
934 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
935 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
936 'date': date_planned,
937 'move_dest_id': parent_move_id,
938 'location_id': source_location_id,
939 'location_dest_id': destination_location_id,
941 'company_id': production.company_id.id,
944 def _make_production_internal_shipment(self, cr, uid, production, context=None):
945 ir_sequence = self.pool.get('ir.sequence')
946 stock_picking = self.pool.get('stock.picking')
948 pick_type = 'internal'
951 # Take routing address as a Shipment Address.
952 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
953 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
954 routing_loc = production.bom_id.routing_id.location_id
955 if routing_loc.usage != 'internal':
957 partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
959 # Take next Sequence number of shipment base on type
960 if pick_type!='internal':
961 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
963 pick_name = ir_sequence.get(cr, uid, 'stock.picking')
965 picking_id = stock_picking.create(cr, uid, {
967 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
971 'partner_id': partner_id,
972 'auto_picking': self._get_auto_picking(cr, uid, production),
973 'company_id': production.company_id.id,
975 production.write({'picking_id': picking_id}, context=context)
978 def _make_production_produce_line(self, cr, uid, production, context=None):
979 stock_move = self.pool.get('stock.move')
980 source_location_id = production.product_id.property_stock_production.id
981 destination_location_id = production.location_dest_id.id
983 'name': production.name,
984 'date': production.date_planned,
985 'product_id': production.product_id.id,
986 'product_qty': production.product_qty,
987 'product_uom': production.product_uom.id,
988 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
989 'product_uos': production.product_uos and production.product_uos.id or False,
990 'location_id': source_location_id,
991 'location_dest_id': destination_location_id,
992 'move_dest_id': production.move_prod_id.id,
994 'company_id': production.company_id.id,
996 if production.move_prod_id:
997 production.move_prod_id.write({'location_id': destination_location_id})
998 move_id = stock_move.create(cr, uid, data, context=context)
999 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
1002 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
1003 stock_move = self.pool.get('stock.move')
1004 production = production_line.production_id
1005 # Internal shipment is created for Stockable and Consumer Products
1006 if production_line.product_id.type not in ('product', 'consu'):
1008 destination_location_id = production.product_id.property_stock_production.id
1009 if not source_location_id:
1010 source_location_id = production.location_src_id.id
1011 move_id = stock_move.create(cr, uid, {
1012 'name': production.name,
1013 'date': production.date_planned,
1014 'product_id': production_line.product_id.id,
1015 'product_qty': production_line.product_qty,
1016 'product_uom': production_line.product_uom.id,
1017 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
1018 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
1019 'location_id': source_location_id,
1020 'location_dest_id': destination_location_id,
1021 'move_dest_id': parent_move_id,
1023 'company_id': production.company_id.id,
1025 production.write({'move_lines': [(4, move_id)]}, context=context)
1028 def action_confirm(self, cr, uid, ids, context=None):
1029 """ Confirms production order.
1030 @return: Newly generated Shipment Id.
1033 wf_service = netsvc.LocalService("workflow")
1034 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)])
1035 self.action_compute(cr, uid, uncompute_ids, context=context)
1036 for production in self.browse(cr, uid, ids, context=context):
1037 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
1038 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
1040 # Take routing location as a Source Location.
1041 source_location_id = production.location_src_id.id
1042 if production.routing_id and production.routing_id.location_id:
1043 source_location_id = production.routing_id.location_id.id
1045 for line in production.product_lines:
1046 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1048 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1049 destination_location_id=source_location_id, context=context)
1050 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1053 wf_service.trg_validate(uid, 'stock.picking', shipment_id, 'button_confirm', cr)
1054 production.write({'state':'confirmed'}, context=context)
1057 def force_production(self, cr, uid, ids, *args):
1058 """ Assigns products.
1059 @param *args: Arguments
1062 pick_obj = self.pool.get('stock.picking')
1063 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1067 class mrp_production_workcenter_line(osv.osv):
1068 _name = 'mrp.production.workcenter.line'
1069 _description = 'Work Order'
1071 _inherit = ['mail.thread']
1074 'name': fields.char('Work Order', size=64, required=True),
1075 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1076 'cycle': fields.float('Number of Cycles', digits=(16,2)),
1077 'hour': fields.float('Number of Hours', digits=(16,2)),
1078 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1079 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1080 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1083 'sequence': lambda *a: 1,
1084 'hour': lambda *a: 0,
1085 'cycle': lambda *a: 0,
1088 class mrp_production_product_line(osv.osv):
1089 _name = 'mrp.production.product.line'
1090 _description = 'Production Scheduled Product'
1092 'name': fields.char('Name', size=64, required=True),
1093 'product_id': fields.many2one('product.product', 'Product', required=True),
1094 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1095 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1096 'product_uos_qty': fields.float('Product UOS Quantity'),
1097 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1098 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1101 class product_product(osv.osv):
1102 _inherit = "product.product"
1104 'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1107 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: