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
31 from openerp import SUPERUSER_ID
33 #----------------------------------------------------------
35 #----------------------------------------------------------
36 # capacity_hour : capacity per hour. default: 1.0.
37 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
38 # unit_per_cycle : how many units are produced for one cycle
40 class mrp_workcenter(osv.osv):
41 _name = 'mrp.workcenter'
42 _description = 'Work Center'
43 _inherits = {'resource.resource':"resource_id"}
45 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
46 '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."),
47 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
48 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
49 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
50 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
51 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
52 help="Fill this only if you want automatic analytic accounting entries on production orders."),
53 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
54 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
55 help="Fill this only if you want automatic analytic accounting entries on production orders."),
56 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
57 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
58 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
59 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
62 'capacity_per_cycle': 1.0,
63 'resource_type': 'material',
66 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
70 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
71 value = {'costs_hour': cost.standard_price}
72 return {'value': value}
76 class mrp_routing(osv.osv):
78 For specifying the routings of Work Centers.
81 _description = 'Routing'
83 'name': fields.char('Name', size=64, required=True),
84 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
85 'code': fields.char('Code', size=8),
87 'note': fields.text('Description'),
88 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
90 'location_id': fields.many2one('stock.location', 'Production Location',
91 help="Keep empty if you produce at the location where the finished products are needed." \
92 "Set a location if you produce at a fixed location. This can be a partner location " \
93 "if you subcontract the manufacturing operations."
95 'company_id': fields.many2one('res.company', 'Company'),
98 'active': lambda *a: 1,
99 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
102 class mrp_routing_workcenter(osv.osv):
104 Defines working cycles and hours of a Work Center using routings.
106 _name = 'mrp.routing.workcenter'
107 _description = 'Work Center Usage'
110 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
111 'name': fields.char('Name', size=64, required=True),
112 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
113 'cycle_nbr': fields.float('Number of Cycles', required=True,
114 help="Number of iterations this work center has to do in the specified operation of the routing."),
115 '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."),
116 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
117 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
118 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
119 'note': fields.text('Description'),
120 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
123 'cycle_nbr': lambda *a: 1.0,
124 'hour_nbr': lambda *a: 0.0,
127 class mrp_bom(osv.osv):
129 Defines bills of material for a product.
132 _description = 'Bill of Material'
133 _inherit = ['mail.thread']
135 def _child_compute(self, cr, uid, ids, name, arg, context=None):
137 @param self: The object pointer
138 @param cr: The current row, from the database cursor,
139 @param uid: The current user ID for security checks
140 @param ids: List of selected IDs
141 @param name: Name of the field
142 @param arg: User defined argument
143 @param context: A standard dictionary for contextual values
144 @return: Dictionary of values
149 bom_obj = self.pool.get('mrp.bom')
150 bom_id = context and context.get('active_id', False) or False
151 cr.execute('select id from mrp_bom')
152 if all(bom_id != r[0] for r in cr.fetchall()):
155 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
156 for bom in self.browse(cr, uid, ids, context=context):
157 if (bom_parent) or (bom.id == bom_id):
158 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
163 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
164 if (bom.type=='phantom' or ok):
165 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
167 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
168 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
172 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
173 """ Sets particular method for the selected bom type.
174 @param field_name: Name of the field
175 @param arg: User defined argument
176 @return: Dictionary of values
178 res = dict.fromkeys(ids, False)
179 for line in self.browse(cr, uid, ids, context=context):
180 if line.type == 'phantom' and not line.bom_id:
183 if line.bom_lines or line.type == 'phantom':
185 if line.product_id.supply_method == 'produce':
186 if line.product_id.procure_method == 'make_to_stock':
187 res[line.id] = 'stock'
189 res[line.id] = 'order'
193 'name': fields.char('Name', size=64),
194 'code': fields.char('Reference', size=16),
195 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the bills of material without removing it."),
196 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
197 help= "If a by-product is used in several products, it can be useful to create its own BoM. "\
198 "Though if you don't want separated production orders for this by-product, select Set/Phantom as BoM type. "\
199 "If a Phantom BoM is used for a root product, it will be sold and shipped as a set of components, instead of being produced."),
200 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
201 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
202 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
203 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
204 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
205 'product_id': fields.many2one('product.product', 'Product', required=True),
206 'product_uos_qty': fields.float('Product UOS Qty'),
207 'product_uos': fields.many2one('product.uom', 'Product UOS', help="Product UOS (Unit of Sale) is the unit of measurement for the invoicing and promotion of stock."),
208 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
209 '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"),
210 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
211 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
212 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
213 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
214 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
215 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
216 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
217 'company_id': fields.many2one('res.company','Company',required=True),
220 'active': lambda *a: 1,
221 'product_efficiency': lambda *a: 1.0,
222 'product_qty': lambda *a: 1.0,
223 'product_rounding': lambda *a: 0.0,
224 'type': lambda *a: 'normal',
225 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
228 _parent_name = "bom_id"
230 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
231 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
234 def _check_recursion(self, cr, uid, ids, context=None):
237 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
238 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
244 def _check_product(self, cr, uid, ids, context=None):
246 boms = self.browse(cr, uid, ids, context=context)
250 if bom.product_id.id in all_prod:
252 all_prod.append(bom.product_id.id)
253 lines = bom.bom_lines
255 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
257 return check_bom(boms)
260 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
261 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
264 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
265 """ Changes UoM and name if product_id changes.
266 @param name: Name of the field
267 @param product_id: Changed product_id
268 @return: Dictionary of changed values
271 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
272 return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}}
275 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
277 if not product_uom or not product_id:
279 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
280 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
281 if uom.category_id.id != product.uom_id.category_id.id:
282 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
283 res['value'].update({'product_uom': product.uom_id.id})
286 def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
287 """ Finds BoM for particular product and product uom.
288 @param product_id: Selected product.
289 @param product_uom: Unit of measure of a product.
290 @param properties: List of related properties.
291 @return: False or BoM id.
293 if properties is None:
295 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
296 ids = map(lambda x: x[0], cr.fetchall())
299 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
301 for prop_id in bom.property_ids:
302 if prop_id.id in properties:
304 if (prop > max_prop) or ((max_prop == 0) and not result):
309 def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
310 """ Finds Products and Work Centers for related BoM for manufacturing order.
311 @param bom: BoM of particular product.
312 @param factor: Factor of product UoM.
313 @param properties: A List of properties Ids.
314 @param addthis: If BoM found then True else False.
315 @param level: Depth level to find BoM lines starts from 10.
316 @return: result: List of dictionaries containing product details.
317 result2: List of dictionaries containing Work Center details.
319 routing_obj = self.pool.get('mrp.routing')
320 factor = factor / (bom.product_efficiency or 1.0)
321 factor = rounding(factor, bom.product_rounding)
322 if factor < bom.product_rounding:
323 factor = bom.product_rounding
327 if bom.type == 'phantom' and not bom.bom_lines:
328 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
331 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
332 result = result + res[0]
333 result2 = result2 + res[1]
338 if addthis and not bom.bom_lines:
341 'name': bom.product_id.name,
342 'product_id': bom.product_id.id,
343 'product_qty': bom.product_qty * factor,
344 'product_uom': bom.product_uom.id,
345 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
346 'product_uos': bom.product_uos and bom.product_uos.id or False,
348 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
350 for wc_use in routing.workcenter_lines:
351 wc = wc_use.workcenter_id
352 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
353 mult = (d + (m and 1.0 or 0.0))
354 cycle = mult * wc_use.cycle_nbr
356 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
357 'workcenter_id': wc.id,
358 'sequence': level+(wc_use.sequence or 0),
360 '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)),
362 for bom2 in bom.bom_lines:
363 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
364 result = result + res[0]
365 result2 = result2 + res[1]
366 return result, result2
368 def copy_data(self, cr, uid, id, default=None, context=None):
371 bom_data = self.read(cr, uid, id, [], context=context)
372 default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
373 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
380 return math.ceil(f / r) * r
382 class mrp_production(osv.osv):
384 Production Orders / Manufacturing Orders
386 _name = 'mrp.production'
387 _description = 'Manufacturing Order'
388 _date_name = 'date_planned'
389 _inherit = ['mail.thread', 'ir.needaction_mixin']
391 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
392 """ Calculates total hours and total no. of cycles for a production order.
393 @param prop: Name of field.
395 @return: Dictionary of values.
398 for prod in self.browse(cr, uid, ids, context=context):
403 for wc in prod.workcenter_lines:
404 result[prod.id]['hour_total'] += wc.hour
405 result[prod.id]['cycle_total'] += wc.cycle
408 def _src_id_default(self, cr, uid, ids, context=None):
409 src_location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
410 return src_location_id.id
412 def _dest_id_default(self, cr, uid, ids, context=None):
413 dest_location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
414 return dest_location_id.id
416 def _get_progress(self, cr, uid, ids, name, arg, context=None):
417 """ Return product quantity percentage """
418 result = dict.fromkeys(ids, 100)
419 for mrp_production in self.browse(cr, uid, ids, context=context):
420 if mrp_production.product_qty:
422 for move in mrp_production.move_created_ids2:
423 if not move.scrapped and move.product_id == mrp_production.product_id:
424 done += move.product_qty
425 result[mrp_production.id] = done / mrp_production.product_qty * 100
429 'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
430 'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
431 help="Reference of the document that generated this production order request."),
432 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority',
433 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
435 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
436 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
437 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
438 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
439 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
440 'progress': fields.function(_get_progress, type='float',
441 string='Production progress'),
443 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
444 readonly=True, states={'draft':[('readonly',False)]},
445 help="Location where the system will look for components."),
446 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
447 readonly=True, states={'draft':[('readonly',False)]},
448 help="Location where the system will stock the finished products."),
449 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft':[('readonly',False)]}),
450 'date_start': fields.datetime('Start Date', select=True, readonly=True),
451 'date_finished': fields.datetime('End Date', select=True, readonly=True),
452 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]},
453 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
454 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]},
455 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."),
456 'picking_id': fields.many2one('stock.picking', 'Picking List', readonly=True, ondelete="restrict",
457 help="This is the Internal Picking List that brings the finished product to the production plan"),
458 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
459 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume',
460 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
461 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products',
462 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
463 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
464 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
465 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
466 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
467 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
468 readonly=True, states={'draft':[('readonly',False)]}),
469 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
470 readonly=True, states={'draft':[('readonly',False)]}),
471 'state': fields.selection(
472 [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
473 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
474 string='Status', readonly=True,
475 track_visibility='onchange',
476 help="When the production order is created the status is set to 'Draft'.\n\
477 If the order is confirmed the status is set to 'Waiting Goods'.\n\
478 If any exceptions are there, the status is set to 'Picking Exception'.\n\
479 If the stock is available then the status is set to 'Ready to Produce'.\n\
480 When the production gets started then the status is set to 'In Production'.\n\
481 When the production is over, the status is set to 'Done'."),
482 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
483 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
484 'user_id':fields.many2one('res.users', 'Responsible'),
485 'company_id': fields.many2one('res.company','Company',required=True),
488 'priority': lambda *a: '1',
489 'state': lambda *a: 'draft',
490 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
491 'product_qty': lambda *a: 1.0,
492 'user_id': lambda self, cr, uid, c: uid,
493 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
494 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
495 'location_src_id': _src_id_default,
496 'location_dest_id': _dest_id_default
499 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
501 _order = 'priority desc, date_planned asc';
503 def _check_qty(self, cr, uid, ids, context=None):
504 for order in self.browse(cr, uid, ids, context=context):
505 if order.product_qty <= 0:
510 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
513 def unlink(self, cr, uid, ids, context=None):
514 for production in self.browse(cr, uid, ids, context=context):
515 if production.state not in ('draft', 'cancel'):
516 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
517 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
519 def copy(self, cr, uid, id, default=None, context=None):
523 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
526 'move_created_ids' : [],
527 'move_created_ids2' : [],
528 'product_lines' : [],
529 'move_prod_id' : False,
532 return super(mrp_production, self).copy(cr, uid, id, default, context)
534 def location_id_change(self, cr, uid, ids, src, dest, context=None):
535 """ Changes destination location if source location is changed.
536 @param src: Source location id.
537 @param dest: Destination location id.
538 @return: Dictionary of values.
543 return {'value': {'location_dest_id': src}}
546 def product_id_change(self, cr, uid, ids, product_id, context=None):
547 """ Finds UoM of changed product.
548 @param product_id: Id of changed product.
549 @return: Dictionary of values.
553 'product_uom': False,
557 bom_obj = self.pool.get('mrp.bom')
558 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
559 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
562 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
563 routing_id = bom_point.routing_id.id or False
565 product_uom_id = product.uom_id and product.uom_id.id or False
567 'product_uom': product_uom_id,
569 'routing_id': routing_id,
571 return {'value': result}
573 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
574 """ Finds routing for changed BoM.
575 @param product: Id of product.
576 @return: Dictionary of values.
582 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
583 routing_id = bom_point.routing_id.id or False
585 'routing_id': routing_id
587 return {'value': result}
589 def action_picking_except(self, cr, uid, ids):
590 """ Changes the state to Exception.
593 self.write(cr, uid, ids, {'state': 'picking_except'})
596 def action_compute(self, cr, uid, ids, properties=None, context=None):
597 """ Computes bills of material of a product.
598 @param properties: List containing dictionaries of properties.
599 @return: No. of products.
601 if properties is None:
604 bom_obj = self.pool.get('mrp.bom')
605 uom_obj = self.pool.get('product.uom')
606 prod_line_obj = self.pool.get('mrp.production.product.line')
607 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
608 for production in self.browse(cr, uid, ids):
610 p_ids = prod_line_obj.search(cr, SUPERUSER_ID, [('production_id', '=', production.id)], context=context)
611 prod_line_obj.unlink(cr, SUPERUSER_ID, p_ids, context=context)
612 w_ids = workcenter_line_obj.search(cr, SUPERUSER_ID, [('production_id', '=', production.id)], context=context)
613 workcenter_line_obj.unlink(cr, SUPERUSER_ID, w_ids, context=context)
615 bom_point = production.bom_id
616 bom_id = production.bom_id.id
618 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
620 bom_point = bom_obj.browse(cr, uid, bom_id)
621 routing_id = bom_point.routing_id.id or False
622 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
625 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
626 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
627 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
631 line['production_id'] = production.id
632 prod_line_obj.create(cr, uid, line)
633 for line in results2:
634 line['production_id'] = production.id
635 workcenter_line_obj.create(cr, uid, line)
638 def action_cancel(self, cr, uid, ids, context=None):
639 """ Cancels the production order and related stock moves.
644 move_obj = self.pool.get('stock.move')
645 for production in self.browse(cr, uid, ids, context=context):
646 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
647 raise osv.except_osv(
648 _('Cannot cancel manufacturing order!'),
649 _('You must first cancel related internal picking attached to this manufacturing order.'))
650 if production.move_created_ids:
651 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
652 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
653 self.write(cr, uid, ids, {'state': 'cancel'})
656 def action_ready(self, cr, uid, ids, context=None):
657 """ Changes the production state to Ready and location id of stock move.
660 move_obj = self.pool.get('stock.move')
661 self.write(cr, uid, ids, {'state': 'ready'})
663 for (production_id,name) in self.name_get(cr, uid, ids):
664 production = self.browse(cr, uid, production_id)
665 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
666 move_obj.write(cr, uid, [production.move_prod_id.id],
667 {'location_id': production.location_dest_id.id})
670 def action_production_end(self, cr, uid, ids, context=None):
671 """ Changes production state to Finish and writes finished date.
674 for production in self.browse(cr, uid, ids):
675 self._costs_generate(cr, uid, production)
676 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
679 def test_production_done(self, cr, uid, ids):
680 """ Tests whether production is done or not.
681 @return: True or False
684 for production in self.browse(cr, uid, ids):
685 if production.move_lines:
688 if production.move_created_ids:
692 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
693 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
694 it's always equal to the quantity encoded in the production order or the production wizard, but if the
695 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
697 :param production_id: ID of the mrp.order
698 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
699 :return: The factor to apply to the quantity that we should produce for the given production order.
703 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
704 """ To produce final product based on production mode (consume/consume&produce).
705 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
706 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
707 and stock move lines of final product will be also done/produced.
708 @param production_id: the ID of mrp.production object
709 @param production_qty: specify qty to produce
710 @param production_mode: specify production mode (consume/consume&produce).
713 stock_mov_obj = self.pool.get('stock.move')
714 production = self.browse(cr, uid, production_id, context=context)
717 for produced_product in production.move_created_ids2:
718 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
720 produced_qty += produced_product.product_qty
721 if production_mode in ['consume','consume_produce']:
724 # Calculate already consumed qtys
725 for consumed in production.move_lines2:
726 if consumed.scrapped:
728 if not consumed_data.get(consumed.product_id.id, False):
729 consumed_data[consumed.product_id.id] = 0
730 consumed_data[consumed.product_id.id] += consumed.product_qty
732 # Find product qty to be consumed and consume it
733 for scheduled in production.product_lines:
735 # total qty of consumed product we need after this consumption
736 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
738 # qty available for consume and produce
739 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
742 # there will be nothing to consume for this raw material
745 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
747 # qtys we have to consume
748 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
749 if float_compare(qty, qty_avail, precision_rounding=scheduled.product_id.uom_id.rounding) == 1:
750 # if qtys we have to consume is more than qtys available to consume
751 prod_name = scheduled.product_id.name_get()[0][1]
752 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))
754 # we already have more qtys consumed than we need
757 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
759 if production_mode == 'consume_produce':
760 # To produce remaining qty of final product
761 #vals = {'state':'confirmed'}
762 #final_product_todo = [x.id for x in production.move_created_ids]
763 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
764 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
765 produced_products = {}
766 for produced_product in production.move_created_ids2:
767 if produced_product.scrapped:
769 if not produced_products.get(produced_product.product_id.id, False):
770 produced_products[produced_product.product_id.id] = 0
771 produced_products[produced_product.product_id.id] += produced_product.product_qty
773 for produce_product in production.move_created_ids:
774 produced_qty = produced_products.get(produce_product.product_id.id, 0)
775 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
776 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
778 if rest_qty < production_qty:
779 prod_name = produce_product.product_id.name_get()[0][1]
780 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))
782 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
784 for raw_product in production.move_lines2:
786 parent_move_ids = [x.id for x in raw_product.move_history_ids]
787 for final_product in production.move_created_ids2:
788 if final_product.id not in parent_move_ids:
789 new_parent_ids.append(final_product.id)
790 for new_parent_id in new_parent_ids:
791 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
792 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
793 self.signal_button_produce_done(cr, uid, [production_id])
796 def _costs_generate(self, cr, uid, production):
797 """ Calculates total costs at the end of the production.
798 @param production: Id of production order.
799 @return: Calculated amount.
802 analytic_line_obj = self.pool.get('account.analytic.line')
803 for wc_line in production.workcenter_lines:
804 wc = wc_line.workcenter_id
805 if wc.costs_journal_id and wc.costs_general_account_id:
807 value = wc_line.hour * wc.costs_hour
808 account = wc.costs_hour_account_id.id
809 if value and account:
811 analytic_line_obj.create(cr, uid, {
812 'name': wc_line.name + ' (H)',
814 'account_id': account,
815 'general_account_id': wc.costs_general_account_id.id,
816 'journal_id': wc.costs_journal_id.id,
818 'product_id': wc.product_id.id,
819 'unit_amount': wc_line.hour,
820 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
823 value = wc_line.cycle * wc.costs_cycle
824 account = wc.costs_cycle_account_id.id
825 if value and account:
827 analytic_line_obj.create(cr, uid, {
828 'name': wc_line.name+' (C)',
830 'account_id': account,
831 'general_account_id': wc.costs_general_account_id.id,
832 'journal_id': wc.costs_journal_id.id,
834 'product_id': wc.product_id.id,
835 'unit_amount': wc_line.cycle,
836 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
840 def action_in_production(self, cr, uid, ids, context=None):
841 """ Changes state to In Production and writes starting date.
844 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
846 def test_if_product(self, cr, uid, ids):
848 @return: True or False
851 for production in self.browse(cr, uid, ids):
852 if not production.product_lines:
853 if not self.action_compute(cr, uid, [production.id]):
857 def _get_auto_picking(self, cr, uid, production):
860 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
861 procurement_order = self.pool.get('procurement.order')
862 production = production_line.production_id
863 location_id = production.location_src_id.id
864 date_planned = production.date_planned
865 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
866 procurement_id = procurement_order.create(cr, uid, {
867 'name': procurement_name,
868 'origin': procurement_name,
869 'date_planned': date_planned,
870 'product_id': production_line.product_id.id,
871 'product_qty': production_line.product_qty,
872 'product_uom': production_line.product_uom.id,
873 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
874 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
875 'location_id': location_id,
876 'procure_method': production_line.product_id.procure_method,
877 'move_id': shipment_move_id,
878 'company_id': production.company_id.id,
880 procurement_order.signal_button_confirm(cr, uid, [procurement_id])
881 return procurement_id
883 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
884 stock_move = self.pool.get('stock.move')
885 production = production_line.production_id
886 date_planned = production.date_planned
887 # Internal shipment is created for Stockable and Consumer Products
888 if production_line.product_id.type not in ('product', 'consu'):
890 source_location_id = production.location_src_id.id
891 if not destination_location_id:
892 destination_location_id = source_location_id
893 return stock_move.create(cr, uid, {
894 'name': production.name,
895 'picking_id': shipment_id,
896 'product_id': production_line.product_id.id,
897 'product_qty': production_line.product_qty,
898 'product_uom': production_line.product_uom.id,
899 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
900 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
901 'date': date_planned,
902 'move_dest_id': parent_move_id,
903 'location_id': source_location_id,
904 'location_dest_id': destination_location_id,
906 'company_id': production.company_id.id,
909 def _make_production_internal_shipment(self, cr, uid, production, context=None):
910 ir_sequence = self.pool.get('ir.sequence')
911 stock_picking = self.pool.get('stock.picking')
913 pick_type = 'internal'
916 # Take routing address as a Shipment Address.
917 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
918 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
919 routing_loc = production.bom_id.routing_id.location_id
920 if routing_loc.usage != 'internal':
922 partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
924 # Take next Sequence number of shipment base on type
925 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
927 picking_id = stock_picking.create(cr, uid, {
929 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
933 'partner_id': partner_id,
934 'auto_picking': self._get_auto_picking(cr, uid, production),
935 'company_id': production.company_id.id,
937 production.write({'picking_id': picking_id}, context=context)
940 def _make_production_produce_line(self, cr, uid, production, context=None):
941 stock_move = self.pool.get('stock.move')
942 source_location_id = production.product_id.property_stock_production.id
943 destination_location_id = production.location_dest_id.id
945 'name': production.name,
946 'date': production.date_planned,
947 'product_id': production.product_id.id,
948 'product_qty': production.product_qty,
949 'product_uom': production.product_uom.id,
950 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
951 'product_uos': production.product_uos and production.product_uos.id or False,
952 'location_id': source_location_id,
953 'location_dest_id': destination_location_id,
954 'move_dest_id': production.move_prod_id.id,
956 'company_id': production.company_id.id,
958 move_id = stock_move.create(cr, uid, data, context=context)
959 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
962 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
963 stock_move = self.pool.get('stock.move')
964 production = production_line.production_id
965 # Internal shipment is created for Stockable and Consumer Products
966 if production_line.product_id.type not in ('product', 'consu'):
968 destination_location_id = production.product_id.property_stock_production.id
969 if not source_location_id:
970 source_location_id = production.location_src_id.id
971 move_id = stock_move.create(cr, uid, {
972 'name': production.name,
973 'date': production.date_planned,
974 'product_id': production_line.product_id.id,
975 'product_qty': production_line.product_qty,
976 'product_uom': production_line.product_uom.id,
977 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
978 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
979 'location_id': source_location_id,
980 'location_dest_id': destination_location_id,
981 'move_dest_id': parent_move_id,
983 'company_id': production.company_id.id,
985 production.write({'move_lines': [(4, move_id)]}, context=context)
988 def action_confirm(self, cr, uid, ids, context=None):
989 """ Confirms production order.
990 @return: Newly generated Shipment Id.
993 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)])
994 self.action_compute(cr, uid, uncompute_ids, context=context)
995 for production in self.browse(cr, uid, ids, context=context):
996 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
997 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
999 # Take routing location as a Source Location.
1000 source_location_id = production.location_src_id.id
1001 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1002 source_location_id = production.bom_id.routing_id.location_id.id
1004 for line in production.product_lines:
1005 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1006 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1007 destination_location_id=source_location_id, context=context)
1008 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1010 self.pool.get('stock.picking').signal_button_confirm(cr, uid, [shipment_id])
1011 production.write({'state':'confirmed'}, context=context)
1014 def force_production(self, cr, uid, ids, *args):
1015 """ Assigns products.
1016 @param *args: Arguments
1019 pick_obj = self.pool.get('stock.picking')
1020 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1024 class mrp_production_workcenter_line(osv.osv):
1025 _name = 'mrp.production.workcenter.line'
1026 _description = 'Work Order'
1028 _inherit = ['mail.thread']
1031 'name': fields.char('Work Order', size=64, required=True),
1032 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1033 'cycle': fields.float('Number of Cycles', digits=(16,2)),
1034 'hour': fields.float('Number of Hours', digits=(16,2)),
1035 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1036 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1037 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1040 'sequence': lambda *a: 1,
1041 'hour': lambda *a: 0,
1042 'cycle': lambda *a: 0,
1045 class mrp_production_product_line(osv.osv):
1046 _name = 'mrp.production.product.line'
1047 _description = 'Production Scheduled Product'
1049 'name': fields.char('Name', size=64, required=True),
1050 'product_id': fields.many2one('product.product', 'Product', required=True),
1051 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1052 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1053 'product_uos_qty': fields.float('Product UOS Quantity'),
1054 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1055 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1058 class product_product(osv.osv):
1059 _inherit = "product.product"
1061 'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1064 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: