1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
23 from datetime import datetime
25 import openerp.addons.decimal_precision as dp
26 from openerp.osv import fields, osv
27 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
28 from openerp.tools import float_compare
29 from openerp.tools.translate import _
30 from openerp import tools
32 #----------------------------------------------------------
34 #----------------------------------------------------------
35 # capacity_hour : capacity per hour. default: 1.0.
36 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
37 # unit_per_cycle : how many units are produced for one cycle
39 class mrp_workcenter(osv.osv):
40 _name = 'mrp.workcenter'
41 _description = 'Work Center'
42 _inherits = {'resource.resource':"resource_id"}
44 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
45 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this Work Center can do in parallel. If this Work Center represents a team of 5 workers, the capacity per cycle is 5."),
46 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
47 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
48 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
49 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
50 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
51 help="Fill this only if you want automatic analytic accounting entries on production orders."),
52 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
53 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
54 help="Fill this only if you want automatic analytic accounting entries on production orders."),
55 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
56 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
57 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
58 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
61 'capacity_per_cycle': 1.0,
62 'resource_type': 'material',
65 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
69 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
70 value = {'costs_hour': cost.standard_price}
71 return {'value': value}
75 class mrp_routing(osv.osv):
77 For specifying the routings of Work Centers.
80 _description = 'Routing'
82 'name': fields.char('Name', size=64, required=True),
83 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
84 'code': fields.char('Code', size=8),
86 'note': fields.text('Description'),
87 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
89 'location_id': fields.many2one('stock.location', 'Production Location',
90 help="Keep empty if you produce at the location where the finished products are needed." \
91 "Set a location if you produce at a fixed location. This can be a partner location " \
92 "if you subcontract the manufacturing operations."
94 'company_id': fields.many2one('res.company', 'Company'),
97 'active': lambda *a: 1,
98 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
101 class mrp_routing_workcenter(osv.osv):
103 Defines working cycles and hours of a Work Center using routings.
105 _name = 'mrp.routing.workcenter'
106 _description = 'Work Center Usage'
109 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
110 'name': fields.char('Name', size=64, required=True),
111 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
112 'cycle_nbr': fields.float('Number of Cycles', required=True,
113 help="Number of iterations this work center has to do in the specified operation of the routing."),
114 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for this Work Center to achieve the operation of the specified routing."),
115 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
116 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
117 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
118 'note': fields.text('Description'),
119 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
122 'cycle_nbr': lambda *a: 1.0,
123 'hour_nbr': lambda *a: 0.0,
126 class mrp_bom(osv.osv):
128 Defines bills of material for a product.
131 _description = 'Bill of Material'
132 _inherit = ['mail.thread']
134 def _child_compute(self, cr, uid, ids, name, arg, context=None):
136 @param self: The object pointer
137 @param cr: The current row, from the database cursor,
138 @param uid: The current user ID for security checks
139 @param ids: List of selected IDs
140 @param name: Name of the field
141 @param arg: User defined argument
142 @param context: A standard dictionary for contextual values
143 @return: Dictionary of values
148 bom_obj = self.pool.get('mrp.bom')
149 bom_id = context and context.get('active_id', False) or False
150 cr.execute('select id from mrp_bom')
151 if all(bom_id != r[0] for r in cr.fetchall()):
154 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
155 for bom in self.browse(cr, uid, ids, context=context):
156 if (bom_parent) or (bom.id == bom_id):
157 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
162 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
163 if (bom.type=='phantom' or ok):
164 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
166 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
167 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
171 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
172 """ Sets particular method for the selected bom type.
173 @param field_name: Name of the field
174 @param arg: User defined argument
175 @return: Dictionary of values
177 res = dict.fromkeys(ids, False)
178 for line in self.browse(cr, uid, ids, context=context):
179 if line.type == 'phantom' and not line.bom_id:
182 if line.bom_lines or line.type == 'phantom':
184 if line.product_id.supply_method == 'produce':
185 if line.product_id.procure_method == 'make_to_stock':
186 res[line.id] = 'stock'
188 res[line.id] = 'order'
192 'name': fields.char('Name', size=64),
193 'code': fields.char('Reference', size=16),
194 '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."),
195 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
196 help= "If a by-product is used in several products, it can be useful to create its own BoM. "\
197 "Though if you don't want separated production orders for this by-product, select Set/Phantom as BoM type. "\
198 "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."),
199 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
200 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
201 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
202 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
203 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
204 'product_id': fields.many2one('product.product', 'Product', required=True),
205 'product_uos_qty': fields.float('Product UOS Qty'),
206 '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."),
207 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
208 '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"),
209 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
210 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
211 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
212 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
213 '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."),
214 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
215 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
216 'company_id': fields.many2one('res.company','Company',required=True),
219 'active': lambda *a: 1,
220 'product_efficiency': lambda *a: 1.0,
221 'product_qty': lambda *a: 1.0,
222 'product_rounding': lambda *a: 0.0,
223 'type': lambda *a: 'normal',
224 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
227 _parent_name = "bom_id"
229 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
230 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
233 def _check_recursion(self, cr, uid, ids, context=None):
236 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
237 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
243 def _check_product(self, cr, uid, ids, context=None):
245 boms = self.browse(cr, uid, ids, context=context)
249 if bom.product_id.id in all_prod:
251 all_prod.append(bom.product_id.id)
252 lines = bom.bom_lines
254 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
256 return check_bom(boms)
259 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
260 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
263 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
264 """ Changes UoM and name if product_id changes.
265 @param name: Name of the field
266 @param product_id: Changed product_id
267 @return: Dictionary of changed values
270 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
271 return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}}
274 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
276 if not product_uom or not product_id:
278 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
279 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
280 if uom.category_id.id != product.uom_id.category_id.id:
281 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
282 res['value'].update({'product_uom': product.uom_id.id})
285 def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
286 """ Finds BoM for particular product and product uom.
287 @param product_id: Selected product.
288 @param product_uom: Unit of measure of a product.
289 @param properties: List of related properties.
290 @return: False or BoM id.
292 if properties is None:
294 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
295 ids = map(lambda x: x[0], cr.fetchall())
298 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
300 for prop_id in bom.property_ids:
301 if prop_id.id in properties:
303 if (prop > max_prop) or ((max_prop == 0) and not result):
308 def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
309 """ Finds Products and Work Centers for related BoM for manufacturing order.
310 @param bom: BoM of particular product.
311 @param factor: Factor of product UoM.
312 @param properties: A List of properties Ids.
313 @param addthis: If BoM found then True else False.
314 @param level: Depth level to find BoM lines starts from 10.
315 @return: result: List of dictionaries containing product details.
316 result2: List of dictionaries containing Work Center details.
318 routing_obj = self.pool.get('mrp.routing')
319 factor = factor / (bom.product_efficiency or 1.0)
320 factor = rounding(factor, bom.product_rounding)
321 if factor < bom.product_rounding:
322 factor = bom.product_rounding
326 if bom.type == 'phantom' and not bom.bom_lines:
327 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
330 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
331 result = result + res[0]
332 result2 = result2 + res[1]
337 if addthis and not bom.bom_lines:
340 'name': bom.product_id.name,
341 'product_id': bom.product_id.id,
342 'product_qty': bom.product_qty * factor,
343 'product_uom': bom.product_uom.id,
344 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
345 'product_uos': bom.product_uos and bom.product_uos.id or False,
347 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
349 for wc_use in routing.workcenter_lines:
350 wc = wc_use.workcenter_id
351 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
352 mult = (d + (m and 1.0 or 0.0))
353 cycle = mult * wc_use.cycle_nbr
355 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
356 'workcenter_id': wc.id,
357 'sequence': level+(wc_use.sequence or 0),
359 '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)),
361 for bom2 in bom.bom_lines:
362 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
363 result = result + res[0]
364 result2 = result2 + res[1]
365 return result, result2
367 def copy_data(self, cr, uid, id, default=None, context=None):
370 bom_data = self.read(cr, uid, id, [], context=context)
371 default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
372 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
379 return math.ceil(f / r) * r
381 class mrp_production(osv.osv):
383 Production Orders / Manufacturing Orders
385 _name = 'mrp.production'
386 _description = 'Manufacturing Order'
387 _date_name = 'date_planned'
388 _inherit = ['mail.thread', 'ir.needaction_mixin']
390 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
391 """ Calculates total hours and total no. of cycles for a production order.
392 @param prop: Name of field.
394 @return: Dictionary of values.
397 for prod in self.browse(cr, uid, ids, context=context):
402 for wc in prod.workcenter_lines:
403 result[prod.id]['hour_total'] += wc.hour
404 result[prod.id]['cycle_total'] += wc.cycle
407 def _src_id_default(self, cr, uid, ids, context=None):
408 src_location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
409 return src_location_id.id
411 def _dest_id_default(self, cr, uid, ids, context=None):
412 dest_location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
413 return dest_location_id.id
415 def _get_progress(self, cr, uid, ids, name, arg, context=None):
416 """ Return product quantity percentage """
417 result = dict.fromkeys(ids, 100)
418 for mrp_production in self.browse(cr, uid, ids, context=context):
419 if mrp_production.product_qty:
421 for move in mrp_production.move_created_ids2:
422 if not move.scrapped and move.product_id == mrp_production.product_id:
423 done += move.product_qty
424 result[mrp_production.id] = done / mrp_production.product_qty * 100
428 'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
429 'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
430 help="Reference of the document that generated this production order request."),
431 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority',
432 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
434 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
435 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
436 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
437 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
438 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
439 'progress': fields.function(_get_progress, type='float',
440 string='Production progress'),
442 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
443 readonly=True, states={'draft':[('readonly',False)]},
444 help="Location where the system will look for components."),
445 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
446 readonly=True, states={'draft':[('readonly',False)]},
447 help="Location where the system will stock the finished products."),
448 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft':[('readonly',False)]}),
449 'date_start': fields.datetime('Start Date', select=True, readonly=True),
450 'date_finished': fields.datetime('End Date', select=True, readonly=True),
451 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]},
452 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
453 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]},
454 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."),
455 'picking_id': fields.many2one('stock.picking', 'Picking List', readonly=True, ondelete="restrict",
456 help="This is the Internal Picking List that brings the finished product to the production plan"),
457 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
458 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume',
459 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
460 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products',
461 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
462 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
463 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
464 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
465 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
466 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
467 readonly=True, states={'draft':[('readonly',False)]}),
468 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
469 readonly=True, states={'draft':[('readonly',False)]}),
470 'state': fields.selection(
471 [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
472 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
473 string='Status', readonly=True,
474 track_visibility='onchange',
475 help="When the production order is created the status is set to 'Draft'.\n\
476 If the order is confirmed the status is set to 'Waiting Goods'.\n\
477 If any exceptions are there, the status is set to 'Picking Exception'.\n\
478 If the stock is available then the status is set to 'Ready to Produce'.\n\
479 When the production gets started then the status is set to 'In Production'.\n\
480 When the production is over, the status is set to 'Done'."),
481 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
482 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
483 'user_id':fields.many2one('res.users', 'Responsible'),
484 'company_id': fields.many2one('res.company','Company',required=True),
487 'priority': lambda *a: '1',
488 'state': lambda *a: 'draft',
489 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
490 'product_qty': lambda *a: 1.0,
491 'user_id': lambda self, cr, uid, c: uid,
492 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
493 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
494 'location_src_id': _src_id_default,
495 'location_dest_id': _dest_id_default
498 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
500 _order = 'priority desc, date_planned asc';
502 def _check_qty(self, cr, uid, ids, context=None):
503 for order in self.browse(cr, uid, ids, context=context):
504 if order.product_qty <= 0:
509 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
512 def unlink(self, cr, uid, ids, context=None):
513 for production in self.browse(cr, uid, ids, context=context):
514 if production.state not in ('draft', 'cancel'):
515 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
516 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
518 def copy(self, cr, uid, id, default=None, context=None):
522 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
525 'move_created_ids' : [],
526 'move_created_ids2' : [],
527 'product_lines' : [],
528 'move_prod_id' : False,
531 return super(mrp_production, self).copy(cr, uid, id, default, context)
533 def location_id_change(self, cr, uid, ids, src, dest, context=None):
534 """ Changes destination location if source location is changed.
535 @param src: Source location id.
536 @param dest: Destination location id.
537 @return: Dictionary of values.
542 return {'value': {'location_dest_id': src}}
545 def product_id_change(self, cr, uid, ids, product_id, context=None):
546 """ Finds UoM of changed product.
547 @param product_id: Id of changed product.
548 @return: Dictionary of values.
552 'product_uom': False,
556 bom_obj = self.pool.get('mrp.bom')
557 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
558 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
561 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
562 routing_id = bom_point.routing_id.id or False
564 product_uom_id = product.uom_id and product.uom_id.id or False
566 'product_uom': product_uom_id,
568 'routing_id': routing_id,
570 return {'value': result}
572 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
573 """ Finds routing for changed BoM.
574 @param product: Id of product.
575 @return: Dictionary of values.
581 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
582 routing_id = bom_point.routing_id.id or False
584 'routing_id': routing_id
586 return {'value': result}
588 def action_picking_except(self, cr, uid, ids):
589 """ Changes the state to Exception.
592 self.write(cr, uid, ids, {'state': 'picking_except'})
595 def action_compute(self, cr, uid, ids, properties=None, context=None):
596 """ Computes bills of material of a product.
597 @param properties: List containing dictionaries of properties.
598 @return: No. of products.
600 if properties is None:
603 bom_obj = self.pool.get('mrp.bom')
604 uom_obj = self.pool.get('product.uom')
605 prod_line_obj = self.pool.get('mrp.production.product.line')
606 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
607 for production in self.browse(cr, uid, ids):
608 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
609 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
610 bom_point = production.bom_id
611 bom_id = production.bom_id.id
613 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
615 bom_point = bom_obj.browse(cr, uid, bom_id)
616 routing_id = bom_point.routing_id.id or False
617 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
620 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
621 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
622 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
626 line['production_id'] = production.id
627 prod_line_obj.create(cr, uid, line)
628 for line in results2:
629 line['production_id'] = production.id
630 workcenter_line_obj.create(cr, uid, line)
633 def action_cancel(self, cr, uid, ids, context=None):
634 """ Cancels the production order and related stock moves.
639 move_obj = self.pool.get('stock.move')
640 for production in self.browse(cr, uid, ids, context=context):
641 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
642 raise osv.except_osv(
643 _('Cannot cancel manufacturing order!'),
644 _('You must first cancel related internal picking attached to this manufacturing order.'))
645 if production.move_created_ids:
646 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
647 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
648 self.write(cr, uid, ids, {'state': 'cancel'})
651 def action_ready(self, cr, uid, ids, context=None):
652 """ Changes the production state to Ready and location id of stock move.
655 move_obj = self.pool.get('stock.move')
656 self.write(cr, uid, ids, {'state': 'ready'})
658 for (production_id,name) in self.name_get(cr, uid, ids):
659 production = self.browse(cr, uid, production_id)
660 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
661 move_obj.write(cr, uid, [production.move_prod_id.id],
662 {'location_id': production.location_dest_id.id})
665 def action_production_end(self, cr, uid, ids, context=None):
666 """ Changes production state to Finish and writes finished date.
669 for production in self.browse(cr, uid, ids):
670 self._costs_generate(cr, uid, production)
671 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
674 def test_production_done(self, cr, uid, ids):
675 """ Tests whether production is done or not.
676 @return: True or False
679 for production in self.browse(cr, uid, ids):
680 if production.move_lines:
683 if production.move_created_ids:
687 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
688 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
689 it's always equal to the quantity encoded in the production order or the production wizard, but if the
690 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
692 :param production_id: ID of the mrp.order
693 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
694 :return: The factor to apply to the quantity that we should produce for the given production order.
698 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
699 """ To produce final product based on production mode (consume/consume&produce).
700 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
701 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
702 and stock move lines of final product will be also done/produced.
703 @param production_id: the ID of mrp.production object
704 @param production_qty: specify qty to produce
705 @param production_mode: specify production mode (consume/consume&produce).
708 stock_mov_obj = self.pool.get('stock.move')
709 production = self.browse(cr, uid, production_id, context=context)
712 for produced_product in production.move_created_ids2:
713 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
715 produced_qty += produced_product.product_qty
716 if production_mode in ['consume','consume_produce']:
719 # Calculate already consumed qtys
720 for consumed in production.move_lines2:
721 if consumed.scrapped:
723 if not consumed_data.get(consumed.product_id.id, False):
724 consumed_data[consumed.product_id.id] = 0
725 consumed_data[consumed.product_id.id] += consumed.product_qty
727 # Find product qty to be consumed and consume it
728 for scheduled in production.product_lines:
730 # total qty of consumed product we need after this consumption
731 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
733 # qty available for consume and produce
734 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
737 # there will be nothing to consume for this raw material
740 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
742 # qtys we have to consume
743 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
744 if float_compare(qty, qty_avail, precision_rounding=scheduled.product_id.uom_id.rounding) == 1:
745 # if qtys we have to consume is more than qtys available to consume
746 prod_name = scheduled.product_id.name_get()[0][1]
747 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))
749 # we already have more qtys consumed than we need
752 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
754 if production_mode == 'consume_produce':
755 # To produce remaining qty of final product
756 #vals = {'state':'confirmed'}
757 #final_product_todo = [x.id for x in production.move_created_ids]
758 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
759 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
760 produced_products = {}
761 for produced_product in production.move_created_ids2:
762 if produced_product.scrapped:
764 if not produced_products.get(produced_product.product_id.id, False):
765 produced_products[produced_product.product_id.id] = 0
766 produced_products[produced_product.product_id.id] += produced_product.product_qty
768 for produce_product in production.move_created_ids:
769 produced_qty = produced_products.get(produce_product.product_id.id, 0)
770 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
771 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
773 if rest_qty < production_qty:
774 prod_name = produce_product.product_id.name_get()[0][1]
775 raise osv.except_osv(_('Warning!'), _('You are going to produce total %s quantities of "%s".\nBut you can only produce up to total %s quantities.') % (production_qty, prod_name, rest_qty))
777 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
779 for raw_product in production.move_lines2:
781 parent_move_ids = [x.id for x in raw_product.move_history_ids]
782 for final_product in production.move_created_ids2:
783 if final_product.id not in parent_move_ids:
784 new_parent_ids.append(final_product.id)
785 for new_parent_id in new_parent_ids:
786 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
787 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
788 self.signal_button_produce_done(cr, uid, [production_id])
791 def _costs_generate(self, cr, uid, production):
792 """ Calculates total costs at the end of the production.
793 @param production: Id of production order.
794 @return: Calculated amount.
797 analytic_line_obj = self.pool.get('account.analytic.line')
798 for wc_line in production.workcenter_lines:
799 wc = wc_line.workcenter_id
800 if wc.costs_journal_id and wc.costs_general_account_id:
802 value = wc_line.hour * wc.costs_hour
803 account = wc.costs_hour_account_id.id
804 if value and account:
806 analytic_line_obj.create(cr, uid, {
807 'name': wc_line.name + ' (H)',
809 'account_id': account,
810 'general_account_id': wc.costs_general_account_id.id,
811 'journal_id': wc.costs_journal_id.id,
813 'product_id': wc.product_id.id,
814 'unit_amount': wc_line.hour,
815 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
818 value = wc_line.cycle * wc.costs_cycle
819 account = wc.costs_cycle_account_id.id
820 if value and account:
822 analytic_line_obj.create(cr, uid, {
823 'name': wc_line.name+' (C)',
825 'account_id': account,
826 'general_account_id': wc.costs_general_account_id.id,
827 'journal_id': wc.costs_journal_id.id,
829 'product_id': wc.product_id.id,
830 'unit_amount': wc_line.cycle,
831 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
835 def action_in_production(self, cr, uid, ids, context=None):
836 """ Changes state to In Production and writes starting date.
839 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
841 def test_if_product(self, cr, uid, ids):
843 @return: True or False
846 for production in self.browse(cr, uid, ids):
847 if not production.product_lines:
848 if not self.action_compute(cr, uid, [production.id]):
852 def _get_auto_picking(self, cr, uid, production):
855 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
856 procurement_order = self.pool.get('procurement.order')
857 production = production_line.production_id
858 location_id = production.location_src_id.id
859 date_planned = production.date_planned
860 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
861 procurement_id = procurement_order.create(cr, uid, {
862 'name': procurement_name,
863 'origin': procurement_name,
864 'date_planned': date_planned,
865 'product_id': production_line.product_id.id,
866 'product_qty': production_line.product_qty,
867 'product_uom': production_line.product_uom.id,
868 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
869 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
870 'location_id': location_id,
871 'procure_method': production_line.product_id.procure_method,
872 'move_id': shipment_move_id,
873 'company_id': production.company_id.id,
875 self.signal_button_confirm(cr, uid, [procurement_id])
876 return procurement_id
878 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
879 stock_move = self.pool.get('stock.move')
880 production = production_line.production_id
881 date_planned = production.date_planned
882 # Internal shipment is created for Stockable and Consumer Products
883 if production_line.product_id.type not in ('product', 'consu'):
885 source_location_id = production.location_src_id.id
886 if not destination_location_id:
887 destination_location_id = source_location_id
888 return stock_move.create(cr, uid, {
889 'name': production.name,
890 'picking_id': shipment_id,
891 'product_id': production_line.product_id.id,
892 'product_qty': production_line.product_qty,
893 'product_uom': production_line.product_uom.id,
894 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
895 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
896 'date': date_planned,
897 'move_dest_id': parent_move_id,
898 'location_id': source_location_id,
899 'location_dest_id': destination_location_id,
901 'company_id': production.company_id.id,
904 def _make_production_internal_shipment(self, cr, uid, production, context=None):
905 ir_sequence = self.pool.get('ir.sequence')
906 stock_picking = self.pool.get('stock.picking')
908 pick_type = 'internal'
911 # Take routing address as a Shipment Address.
912 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
913 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
914 routing_loc = production.bom_id.routing_id.location_id
915 if routing_loc.usage != 'internal':
917 partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
919 # Take next Sequence number of shipment base on type
920 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
922 picking_id = stock_picking.create(cr, uid, {
924 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
928 'partner_id': partner_id,
929 'auto_picking': self._get_auto_picking(cr, uid, production),
930 'company_id': production.company_id.id,
932 production.write({'picking_id': picking_id}, context=context)
935 def _make_production_produce_line(self, cr, uid, production, context=None):
936 stock_move = self.pool.get('stock.move')
937 source_location_id = production.product_id.property_stock_production.id
938 destination_location_id = production.location_dest_id.id
940 'name': production.name,
941 'date': production.date_planned,
942 'product_id': production.product_id.id,
943 'product_qty': production.product_qty,
944 'product_uom': production.product_uom.id,
945 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
946 'product_uos': production.product_uos and production.product_uos.id or False,
947 'location_id': source_location_id,
948 'location_dest_id': destination_location_id,
949 'move_dest_id': production.move_prod_id.id,
951 'company_id': production.company_id.id,
953 move_id = stock_move.create(cr, uid, data, context=context)
954 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
957 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
958 stock_move = self.pool.get('stock.move')
959 production = production_line.production_id
960 # Internal shipment is created for Stockable and Consumer Products
961 if production_line.product_id.type not in ('product', 'consu'):
963 destination_location_id = production.product_id.property_stock_production.id
964 if not source_location_id:
965 source_location_id = production.location_src_id.id
966 move_id = stock_move.create(cr, uid, {
967 'name': production.name,
968 'date': production.date_planned,
969 'product_id': production_line.product_id.id,
970 'product_qty': production_line.product_qty,
971 'product_uom': production_line.product_uom.id,
972 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
973 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
974 'location_id': source_location_id,
975 'location_dest_id': destination_location_id,
976 'move_dest_id': parent_move_id,
978 'company_id': production.company_id.id,
980 production.write({'move_lines': [(4, move_id)]}, context=context)
983 def action_confirm(self, cr, uid, ids, context=None):
984 """ Confirms production order.
985 @return: Newly generated Shipment Id.
988 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)])
989 self.action_compute(cr, uid, uncompute_ids, context=context)
990 for production in self.browse(cr, uid, ids, context=context):
991 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
992 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
994 # Take routing location as a Source Location.
995 source_location_id = production.location_src_id.id
996 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
997 source_location_id = production.bom_id.routing_id.location_id.id
999 for line in production.product_lines:
1000 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1001 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1002 destination_location_id=source_location_id, context=context)
1003 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1005 self.pool.get('stock.picking').signal_button_confirm(cr, uid, [shipment_id])
1006 production.write({'state':'confirmed'}, context=context)
1009 def force_production(self, cr, uid, ids, *args):
1010 """ Assigns products.
1011 @param *args: Arguments
1014 pick_obj = self.pool.get('stock.picking')
1015 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1019 class mrp_production_workcenter_line(osv.osv):
1020 _name = 'mrp.production.workcenter.line'
1021 _description = 'Work Order'
1023 _inherit = ['mail.thread']
1026 'name': fields.char('Work Order', size=64, required=True),
1027 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1028 'cycle': fields.float('Number of Cycles', digits=(16,2)),
1029 'hour': fields.float('Number of Hours', digits=(16,2)),
1030 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1031 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1032 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1035 'sequence': lambda *a: 1,
1036 'hour': lambda *a: 0,
1037 'cycle': lambda *a: 0,
1040 class mrp_production_product_line(osv.osv):
1041 _name = 'mrp.production.product.line'
1042 _description = 'Production Scheduled Product'
1044 'name': fields.char('Name', size=64, required=True),
1045 'product_id': fields.many2one('product.product', 'Product', required=True),
1046 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1047 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1048 'product_uos_qty': fields.float('Product UOS Quantity'),
1049 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1050 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1053 class product_product(osv.osv):
1054 _inherit = "product.product"
1056 'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1059 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: