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
24 import openerp.addons.decimal_precision as dp
25 from openerp.osv import fields, osv, orm
26 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
27 from openerp.tools import float_compare
28 from openerp.tools.translate import _
29 from openerp import tools, SUPERUSER_ID
30 from openerp import SUPERUSER_ID
31 from openerp.addons.product import _common
34 class mrp_property_group(osv.osv):
36 Group of mrp properties.
38 _name = 'mrp.property.group'
39 _description = 'Property Group'
41 'name': fields.char('Property Group', size=64, required=True),
42 'description': fields.text('Description'),
45 class mrp_property(osv.osv):
49 _name = 'mrp.property'
50 _description = 'Property'
52 'name': fields.char('Name', size=64, required=True),
53 'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
54 'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
55 'description': fields.text('Description'),
58 'composition': lambda *a: 'min',
60 #----------------------------------------------------------
62 #----------------------------------------------------------
63 # capacity_hour : capacity per hour. default: 1.0.
64 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
65 # unit_per_cycle : how many units are produced for one cycle
67 class mrp_workcenter(osv.osv):
68 _name = 'mrp.workcenter'
69 _description = 'Work Center'
70 _inherits = {'resource.resource':"resource_id"}
72 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
73 '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."),
74 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
75 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
76 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
77 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
78 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
79 help="Fill this only if you want automatic analytic accounting entries on production orders."),
80 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
81 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
82 help="Fill this only if you want automatic analytic accounting entries on production orders."),
83 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
84 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
85 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
86 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
89 'capacity_per_cycle': 1.0,
90 'resource_type': 'material',
93 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
97 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
98 value = {'costs_hour': cost.standard_price}
99 return {'value': value}
101 class mrp_routing(osv.osv):
103 For specifying the routings of Work Centers.
105 _name = 'mrp.routing'
106 _description = 'Routing'
108 'name': fields.char('Name', size=64, required=True),
109 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
110 'code': fields.char('Code', size=8),
112 'note': fields.text('Description'),
113 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
115 'location_id': fields.many2one('stock.location', 'Production Location',
116 help="Keep empty if you produce at the location where the finished products are needed." \
117 "Set a location if you produce at a fixed location. This can be a partner location " \
118 "if you subcontract the manufacturing operations."
120 'company_id': fields.many2one('res.company', 'Company'),
123 'active': lambda *a: 1,
124 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
127 class mrp_routing_workcenter(osv.osv):
129 Defines working cycles and hours of a Work Center using routings.
131 _name = 'mrp.routing.workcenter'
132 _description = 'Work Center Usage'
135 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
136 'name': fields.char('Name', size=64, required=True),
137 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
138 'cycle_nbr': fields.float('Number of Cycles', required=True,
139 help="Number of iterations this work center has to do in the specified operation of the routing."),
140 '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."),
141 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
142 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
143 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
144 'note': fields.text('Description'),
145 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
148 'cycle_nbr': lambda *a: 1.0,
149 'hour_nbr': lambda *a: 0.0,
152 class mrp_bom(osv.osv):
154 Defines bills of material for a product.
157 _description = 'Bill of Material'
158 _inherit = ['mail.thread']
160 def _child_compute(self, cr, uid, ids, name, arg, context=None):
162 @param self: The object pointer
163 @param cr: The current row, from the database cursor,
164 @param uid: The current user ID for security checks
165 @param ids: List of selected IDs
166 @param name: Name of the field
167 @param arg: User defined argument
168 @param context: A standard dictionary for contextual values
169 @return: Dictionary of values
174 bom_obj = self.pool.get('mrp.bom')
175 bom_id = context and context.get('active_id', False) or False
176 cr.execute('select id from mrp_bom')
177 if all(bom_id != r[0] for r in cr.fetchall()):
180 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
181 for bom in self.browse(cr, uid, ids, context=context):
182 if (bom_parent) or (bom.id == bom_id):
183 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
188 ok = ((name=='child_complete_ids'))
189 if (bom.type=='phantom' or ok):
190 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
192 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
193 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
197 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
198 """ Sets particular method for the selected bom type.
199 @param field_name: Name of the field
200 @param arg: User defined argument
201 @return: Dictionary of values
203 res = dict.fromkeys(ids, False)
204 for line in self.browse(cr, uid, ids, context=context):
205 if line.type == 'phantom' and not line.bom_id:
208 if line.bom_lines or line.type == 'phantom':
210 if line.product_id.procure_method == 'make_to_stock':
211 res[line.id] = 'stock'
213 res[line.id] = 'order'
217 'name': fields.char('Name', size=64),
218 'code': fields.char('Reference', size=16),
219 '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."),
220 'type': fields.selection([('normal', 'Normal BoM'), ('phantom', 'Sets / Phantom')], 'BoM Type', required=True,
221 help= "If a by-product is used in several products, it can be useful to create its own BoM. "\
222 "Though if you don't want separated production orders for this by-product, select Set/Phantom as BoM type. "\
223 "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."),
224 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('', ''), ('stock', 'On Stock'), ('order', 'On Order'), ('set', 'Set / Pack')]),
225 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
226 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
227 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
228 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
229 'product_id': fields.many2one('product.product', 'Product', required=True),
230 'product_uos_qty': fields.float('Product UOS Qty'),
231 '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."),
232 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
233 '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"),
234 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
235 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
236 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
237 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
238 '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."),
239 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id', 'property_id', 'Properties'),
240 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
241 'company_id': fields.many2one('res.company', 'Company', required=True),
244 'active': lambda *a: 1,
245 'product_efficiency': lambda *a: 1.0,
246 'product_qty': lambda *a: 1.0,
247 'product_rounding': lambda *a: 0.0,
248 'type': lambda *a: 'normal',
249 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
252 _parent_name = "bom_id"
254 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
255 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
258 def _check_recursion(self, cr, uid, ids, context=None):
261 cr.execute('select distinct bom_id from mrp_bom where id IN %s', (tuple(ids),))
262 ids = filter(None, map(lambda x: x[0], cr.fetchall()))
268 def _check_product(self, cr, uid, ids, context=None):
270 boms = self.browse(cr, uid, ids, context=context)
274 if bom.product_id.id in all_prod:
276 all_prod.append(bom.product_id.id)
277 lines = bom.bom_lines
279 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
281 return check_bom(boms)
284 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
285 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
288 def onchange_product_id(self, cr, uid, ids, product_id, name, product_qty=0, context=None):
289 """ Changes UoM and name if product_id changes.
290 @param name: Name of the field
291 @param product_id: Changed product_id
292 @return: Dictionary of changed values
296 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
297 res['value'] = {'name': prod.name, 'product_uom': prod.uom_id.id, 'product_uos_qty': 0, 'product_uos': False}
299 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
300 res['value']['product_uos'] = prod.uos_id.id
303 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
305 if not product_uom or not product_id:
307 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
308 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
309 if uom.category_id.id != product.uom_id.category_id.id:
310 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
311 res['value'].update({'product_uom': product.uom_id.id})
314 def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
315 """ Finds BoM for particular product and product uom.
316 @param product_id: Selected product.
317 @param product_uom: Unit of measure of a product.
318 @param properties: List of related properties.
319 @return: False or BoM id.
321 if properties is None:
323 domain = [('product_id', '=', product_id), ('bom_id', '=', False),
324 '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
325 '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
326 ids = self.search(cr, uid, domain)
329 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
331 for prop_id in bom.property_ids:
332 if prop_id.id in properties:
334 if (prop > max_prop) or ((max_prop == 0) and not result):
339 def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
340 """ Finds Products and Work Centers for related BoM for manufacturing order.
341 @param bom: BoM of particular product.
342 @param factor: Factor of product UoM.
343 @param properties: A List of properties Ids.
344 @param addthis: If BoM found then True else False.
345 @param level: Depth level to find BoM lines starts from 10.
346 @return: result: List of dictionaries containing product details.
347 result2: List of dictionaries containing Work Center details.
349 routing_obj = self.pool.get('mrp.routing')
350 factor = factor / (bom.product_efficiency or 1.0)
351 factor = _common.ceiling(factor, bom.product_rounding)
352 if factor < bom.product_rounding:
353 factor = bom.product_rounding
357 if bom.type == 'phantom' and not bom.bom_lines:
358 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
361 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor * bom.product_qty, properties, addthis=True, level=level + 10)
362 result = result + res[0]
363 result2 = result2 + res[1]
368 if addthis and not bom.bom_lines:
370 'name': bom.product_id.name,
371 'product_id': bom.product_id.id,
372 'product_qty': bom.product_qty * factor,
373 'product_uom': bom.product_uom.id,
374 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
375 'product_uos': bom.product_uos and bom.product_uos.id or False,
377 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
379 for wc_use in routing.workcenter_lines:
380 wc = wc_use.workcenter_id
381 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
382 mult = (d + (m and 1.0 or 0.0))
383 cycle = mult * wc_use.cycle_nbr
385 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
386 'workcenter_id': wc.id,
387 'sequence': level + (wc_use.sequence or 0),
389 '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)),
391 for bom2 in bom.bom_lines:
392 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level + 10)
393 result = result + res[0]
394 result2 = result2 + res[1]
395 return result, result2
397 def copy_data(self, cr, uid, id, default=None, context=None):
400 bom_data = self.read(cr, uid, id, [], context=context)
401 default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
402 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
405 class mrp_production(osv.osv):
407 Production Orders / Manufacturing Orders
409 _name = 'mrp.production'
410 _description = 'Manufacturing Order'
411 _date_name = 'date_planned'
412 _inherit = ['mail.thread', 'ir.needaction_mixin']
414 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
415 """ Calculates total hours and total no. of cycles for a production order.
416 @param prop: Name of field.
418 @return: Dictionary of values.
421 for prod in self.browse(cr, uid, ids, context=context):
426 for wc in prod.workcenter_lines:
427 result[prod.id]['hour_total'] += wc.hour
428 result[prod.id]['cycle_total'] += wc.cycle
431 def _src_id_default(self, cr, uid, ids, context=None):
433 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
434 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
435 except (orm.except_orm, ValueError):
439 def _dest_id_default(self, cr, uid, ids, context=None):
441 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
442 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
443 except (orm.except_orm, ValueError):
447 def _get_progress(self, cr, uid, ids, name, arg, context=None):
448 """ Return product quantity percentage """
449 result = dict.fromkeys(ids, 100)
450 for mrp_production in self.browse(cr, uid, ids, context=context):
451 if mrp_production.product_qty:
453 for move in mrp_production.move_created_ids2:
454 if not move.scrapped and move.product_id == mrp_production.product_id:
455 done += move.product_qty
456 result[mrp_production.id] = done / mrp_production.product_qty * 100
459 def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
460 """ Test whether all the consume lines are assigned """
462 for production in self.browse(cr, uid, ids, context=context):
463 res[production.id] = True
464 states = [x.state != 'assigned' for x in production.move_lines if x]
465 if any(states) or len(states) == 0:
466 res[production.id] = False
469 def _mrp_from_move(self, cr, uid, ids, context=None):
472 for move in self.browse(cr, uid, ids, context=context):
473 res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
477 'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
478 'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
479 help="Reference of the document that generated this production order request."),
480 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
481 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
483 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
484 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
485 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
486 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
487 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
488 'progress': fields.function(_get_progress, type='float',
489 string='Production progress'),
491 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
492 readonly=True, states={'draft': [('readonly', False)]},
493 help="Location where the system will look for components."),
494 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
495 readonly=True, states={'draft': [('readonly', False)]},
496 help="Location where the system will stock the finished products."),
497 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}),
498 'date_start': fields.datetime('Start Date', select=True, readonly=True),
499 'date_finished': fields.datetime('End Date', select=True, readonly=True),
500 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id', '=', False)], readonly=True, states={'draft': [('readonly', False)]},
501 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
502 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
503 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."),
504 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
505 'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
506 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
507 'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
508 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
509 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
510 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
511 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
512 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
513 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
515 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
516 readonly=True, states={'draft': [('readonly', False)]}),
517 'state': fields.selection(
518 [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
519 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
520 string='Status', readonly=True,
521 track_visibility='onchange',
522 help="When the production order is created the status is set to 'Draft'.\n\
523 If the order is confirmed the status is set to 'Waiting Goods'.\n\
524 If any exceptions are there, the status is set to 'Picking Exception'.\n\
525 If the stock is available then the status is set to 'Ready to Produce'.\n\
526 When the production gets started then the status is set to 'In Production'.\n\
527 When the production is over, the status is set to 'Done'."),
528 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
529 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
530 'user_id': fields.many2one('res.users', 'Responsible'),
531 'company_id': fields.many2one('res.company', 'Company', required=True),
532 'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
536 'priority': lambda *a: '1',
537 'state': lambda *a: 'draft',
538 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
539 'product_qty': lambda *a: 1.0,
540 'user_id': lambda self, cr, uid, c: uid,
541 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
542 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
543 'location_src_id': _src_id_default,
544 'location_dest_id': _dest_id_default
548 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
551 _order = 'priority desc, date_planned asc'
553 def _check_qty(self, cr, uid, ids, context=None):
554 for order in self.browse(cr, uid, ids, context=context):
555 if order.product_qty <= 0:
560 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
563 def unlink(self, cr, uid, ids, context=None):
564 for production in self.browse(cr, uid, ids, context=context):
565 if production.state not in ('draft', 'cancel'):
566 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
567 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
569 def copy(self, cr, uid, id, default=None, context=None):
573 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
576 'move_created_ids': [],
577 'move_created_ids2': [],
579 'move_prod_id': False,
581 return super(mrp_production, self).copy(cr, uid, id, default, context)
583 def location_id_change(self, cr, uid, ids, src, dest, context=None):
584 """ Changes destination location if source location is changed.
585 @param src: Source location id.
586 @param dest: Destination location id.
587 @return: Dictionary of values.
592 return {'value': {'location_dest_id': src}}
595 def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
596 """ Finds UoM of changed product.
597 @param product_id: Id of changed product.
598 @return: Dictionary of values.
603 'product_uom': False,
606 'product_uos_qty': 0,
609 bom_obj = self.pool.get('mrp.bom')
610 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
611 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
614 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
615 routing_id = bom_point.routing_id.id or False
616 product_uom_id = product.uom_id and product.uom_id.id or False
617 product_uos_id = product.uos_id and product.uos_id.id or False
618 result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
619 if product.uos_id.id:
620 result['value']['product_uos_qty'] = product_qty * product.uos_coeff
621 result['value']['product_uos'] = product.uos_id.id
624 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
625 """ Finds routing for changed BoM.
626 @param product: Id of product.
627 @return: Dictionary of values.
633 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
634 routing_id = bom_point.routing_id.id or False
636 'routing_id': routing_id
638 return {'value': result}
640 def action_picking_except(self, cr, uid, ids):
641 """ Changes the state to Exception.
644 self.write(cr, uid, ids, {'state': 'picking_except'})
647 def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
648 """ Compute product_lines and workcenter_lines from BoM structure
649 @return: product_lines
651 if properties is None:
654 bom_obj = self.pool.get('mrp.bom')
655 uom_obj = self.pool.get('product.uom')
656 prod_line_obj = self.pool.get('mrp.production.product.line')
657 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
658 for production in self.browse(cr, uid, ids, context=context):
659 #unlink product_lines
660 prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
661 #unlink workcenter_lines
662 workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
663 # search BoM structure and route
664 bom_point = production.bom_id
665 bom_id = production.bom_id.id
667 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
669 bom_point = bom_obj.browse(cr, uid, bom_id)
670 routing_id = bom_point.routing_id.id or False
671 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
674 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
676 # get components and workcenter_lines from BoM structure
677 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
678 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
679 results = res[0] # product_lines
680 results2 = res[1] # workcenter_lines
681 # reset product_lines in production order
683 line['production_id'] = production.id
684 prod_line_obj.create(cr, uid, line)
686 #reset workcenter_lines in production order
687 for line in results2:
688 line['production_id'] = production.id
689 workcenter_line_obj.create(cr, uid, line)
692 def action_compute(self, cr, uid, ids, properties=None, context=None):
693 """ Computes bills of material of a product.
694 @param properties: List containing dictionaries of properties.
695 @return: No. of products.
697 return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
699 def action_cancel(self, cr, uid, ids, context=None):
700 """ Cancels the production order and related stock moves.
705 move_obj = self.pool.get('stock.move')
706 for production in self.browse(cr, uid, ids, context=context):
707 if production.move_created_ids:
708 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
709 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
710 self.write(cr, uid, ids, {'state': 'cancel'})
713 def action_ready(self, cr, uid, ids, context=None):
714 """ Changes the production state to Ready and location id of stock move.
717 move_obj = self.pool.get('stock.move')
718 self.write(cr, uid, ids, {'state': 'ready'})
720 for production in self.browse(cr, uid, ids, context=context):
721 if not production.move_created_ids:
722 self._make_production_produce_line(cr, uid, production, context=context)
723 for scheduled in production.product_lines:
724 self._make_production_line_procurement(cr, uid, scheduled, False, context=context)
726 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
727 move_obj.write(cr, uid, [production.move_prod_id.id],
728 {'location_id': production.location_dest_id.id})
731 def action_production_end(self, cr, uid, ids, context=None):
732 """ Changes production state to Finish and writes finished date.
735 for production in self.browse(cr, uid, ids):
736 self._costs_generate(cr, uid, production)
737 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
740 def test_production_done(self, cr, uid, ids):
741 """ Tests whether production is done or not.
742 @return: True or False
745 for production in self.browse(cr, uid, ids):
746 if production.move_lines:
749 if production.move_created_ids:
753 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
754 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
755 it's always equal to the quantity encoded in the production order or the production wizard, but if the
756 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
758 :param production_id: ID of the mrp.order
759 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
760 :return: The factor to apply to the quantity that we should produce for the given production order.
764 def _get_produced_qty(self, cr, uid, production, context=None):
765 ''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
768 for produced_product in production.move_created_ids2:
769 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
771 produced_qty += produced_product.product_qty
774 def _get_consumed_data(self, cr, uid, production, context=None):
775 ''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
778 # Calculate already consumed qtys
779 for consumed in production.move_lines2:
780 if consumed.scrapped:
782 if not consumed_data.get(consumed.product_id.id, False):
783 consumed_data[consumed.product_id.id] = 0
784 consumed_data[consumed.product_id.id] += consumed.product_qty
787 def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
789 Calculates the quantity still needed to produce an extra number of products
791 quant_obj = self.pool.get("stock.quant")
792 produced_qty = self._get_produced_qty(cr, uid, production, context=context)
793 consumed_data = self._get_consumed_data(cr, uid, production, context=context)
795 #In case no product_qty is given, take the remaining qty to produce for the given production
797 product_qty = production.product_qty - produced_qty
800 # Find product qty to be consumed and consume it
801 for scheduled in production.product_lines:
802 consumed_qty = consumed_data.get(scheduled.product_id.id, 0.0)
803 # qty available for consume and produce
804 qty_avail = scheduled.product_qty - consumed_qty
806 # there will be nothing to consume for this raw material
809 if not dicts.get(scheduled.product_id.id):
810 dicts[scheduled.product_id.id] = {}
812 # total qty of consumed product we need after this consumption
813 total_consume = ((product_qty + produced_qty) * scheduled.product_qty / production.product_qty)
814 qty = total_consume - consumed_qty
816 # Search for quants related to this related move
817 for move in production.move_lines:
820 if move.product_id.id != scheduled.product_id.id:
822 product_id = scheduled.product_id.id
824 q = min(move.product_qty, qty)
825 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, scheduled.product_id, q, domain=[('qty', '>', 0.0)],
826 prefered_domain=[('reservation_id', '=', move.id)], fallback_domain=[('reservation_id', '=', False)], context=context)
827 for quant, quant_qty in quants:
829 lot_id = quant.lot_id.id
830 if not product_id in dicts.keys():
831 dicts[product_id] = {lot_id: quant_qty}
832 elif lot_id in dicts[product_id].keys():
833 dicts[product_id][lot_id] += quant_qty
835 dicts[product_id][lot_id] = quant_qty
838 if dicts[product_id].get(False):
839 dicts[product_id][False] += qty
841 dicts[product_id][False] = qty
844 for prod in dicts.keys():
845 for lot, qty in dicts[prod].items():
846 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
849 def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
850 """ To produce final product based on production mode (consume/consume&produce).
851 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
852 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
853 and stock move lines of final product will be also done/produced.
854 @param production_id: the ID of mrp.production object
855 @param production_qty: specify qty to produce
856 @param production_mode: specify production mode (consume/consume&produce).
857 @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
860 stock_mov_obj = self.pool.get('stock.move')
861 production = self.browse(cr, uid, production_id, context=context)
862 if not production.move_lines and production.state == 'ready':
863 # trigger workflow if not products to consume (eg: services)
864 self.signal_button_produce(cr, uid, [production_id])
866 produced_qty = self._get_produced_qty(cr, uid, production, context=context)
868 main_production_move = False
869 if production_mode == 'consume_produce':
870 # To produce remaining qty of final product
871 #vals = {'state':'confirmed'}
872 #final_product_todo = [x.id for x in production.move_created_ids]
873 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
874 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
875 produced_products = {}
876 for produced_product in production.move_created_ids2:
877 if produced_product.scrapped:
879 if not produced_products.get(produced_product.product_id.id, False):
880 produced_products[produced_product.product_id.id] = 0
881 produced_products[produced_product.product_id.id] += produced_product.product_qty
883 for produce_product in production.move_created_ids:
884 produced_qty = produced_products.get(produce_product.product_id.id, 0)
885 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
886 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
887 if float_compare(rest_qty, (subproduct_factor * production_qty), precision_rounding=produce_product.product_id.uom_id.rounding) < 0:
888 prod_name = produce_product.product_id.name_get()[0][1]
889 raise osv.except_osv(_('Warning!'), _('You are going to produce total %s quantities of "%s".\nBut you can only produce up to total %s quantities.') % ((subproduct_factor * production_qty), prod_name, rest_qty))
890 if float_compare(rest_qty, 0, precision_rounding=produce_product.product_id.uom_id.rounding) > 0:
893 lot_id = wiz.lot_id.id
894 new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
895 if produce_product.product_id.id == production.product_id.id and new_moves:
896 main_production_move = new_moves[0]
898 if production_mode in ['consume', 'consume_produce']:
901 for cons in wiz.consume_lines:
902 consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
904 consume_lines = self._calculate_qty(cr, uid, production, production_qty, context=context)
905 for consume in consume_lines:
906 remaining_qty = consume['product_qty']
907 for raw_material_line in production.move_lines:
908 if remaining_qty <= 0:
910 if consume['product_id'] != raw_material_line.product_id.id:
912 consumed_qty = min(remaining_qty, raw_material_line.product_qty)
913 stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id, restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
914 remaining_qty -= consumed_qty
916 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
917 self.signal_button_produce_done(cr, uid, [production_id])
920 def _costs_generate(self, cr, uid, production):
921 """ Calculates total costs at the end of the production.
922 @param production: Id of production order.
923 @return: Calculated amount.
926 analytic_line_obj = self.pool.get('account.analytic.line')
927 for wc_line in production.workcenter_lines:
928 wc = wc_line.workcenter_id
929 if wc.costs_journal_id and wc.costs_general_account_id:
931 value = wc_line.hour * wc.costs_hour
932 account = wc.costs_hour_account_id.id
933 if value and account:
935 # we user SUPERUSER_ID as we do not garantee an mrp user
936 # has access to account analytic lines but still should be
937 # able to produce orders
938 analytic_line_obj.create(cr, SUPERUSER_ID, {
939 'name': wc_line.name + ' (H)',
941 'account_id': account,
942 'general_account_id': wc.costs_general_account_id.id,
943 'journal_id': wc.costs_journal_id.id,
945 'product_id': wc.product_id.id,
946 'unit_amount': wc_line.hour,
947 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
950 value = wc_line.cycle * wc.costs_cycle
951 account = wc.costs_cycle_account_id.id
952 if value and account:
954 analytic_line_obj.create(cr, SUPERUSER_ID, {
955 'name': wc_line.name + ' (C)',
957 'account_id': account,
958 'general_account_id': wc.costs_general_account_id.id,
959 'journal_id': wc.costs_journal_id.id,
961 'product_id': wc.product_id.id,
962 'unit_amount': wc_line.cycle,
963 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
967 def action_in_production(self, cr, uid, ids, context=None):
968 """ Changes state to In Production and writes starting date.
971 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
973 def consume_lines_get(self, cr, uid, ids, *args):
975 for order in self.browse(cr, uid, ids, context={}):
976 res += [x.id for x in order.move_lines]
979 def test_ready(self, cr, uid, ids):
981 for production in self.browse(cr, uid, ids):
982 if production.ready_production:
986 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
987 procurement_order = self.pool.get('procurement.order')
988 production = production_line.production_id
989 location_id = production.location_src_id.id
990 date_planned = production.date_planned
991 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
992 procurement_id = procurement_order.create(cr, uid, {
993 'name': procurement_name,
994 'origin': procurement_name,
995 'date_planned': date_planned,
996 'product_id': production_line.product_id.id,
997 'product_qty': production_line.product_qty,
998 'product_uom': production_line.product_uom.id,
999 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
1000 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
1001 'location_id': location_id,
1002 'procure_method': production_line.product_id.procure_method,
1003 'move_id': shipment_move_id,
1004 'company_id': production.company_id.id,
1006 procurement_order.signal_button_confirm(cr, uid, [procurement_id])
1007 return procurement_id
1009 def _make_production_produce_line(self, cr, uid, production, context=None):
1010 stock_move = self.pool.get('stock.move')
1011 source_location_id = production.product_id.property_stock_production.id
1012 destination_location_id = production.location_dest_id.id
1014 'name': production.name,
1015 'date': production.date_planned,
1016 'product_id': production.product_id.id,
1017 'product_qty': production.product_qty,
1018 'product_uom': production.product_uom.id,
1019 'product_uom_qty': production.product_qty,
1020 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1021 'product_uos': production.product_uos and production.product_uos.id or False,
1022 'location_id': source_location_id,
1023 'location_dest_id': destination_location_id,
1024 'move_dest_id': production.move_prod_id.id,
1025 'company_id': production.company_id.id,
1026 'production_id': production.id,
1028 move_id = stock_move.create(cr, uid, data, context=context)
1029 #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1030 #is 1 element long, so we can take the first.
1031 return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1033 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
1034 stock_move = self.pool.get('stock.move')
1035 production = production_line.production_id
1036 # Internal shipment is created for Stockable and Consumer Products
1037 if production_line.product_id.type not in ('product', 'consu'):
1039 destination_location_id = production.product_id.property_stock_production.id
1040 if not source_location_id:
1041 source_location_id = production.location_src_id.id
1042 move_id = stock_move.create(cr, uid, {
1043 'name': production.name,
1044 'date': production.date_planned,
1045 'product_id': production_line.product_id.id,
1046 'product_uom_qty': production_line.product_qty,
1047 'product_uom': production_line.product_uom.id,
1048 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
1049 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
1050 'location_id': source_location_id,
1051 'location_dest_id': destination_location_id,
1052 'company_id': production.company_id.id,
1053 'procure_method': 'make_to_order',
1054 'raw_material_production_id': production.id,
1056 stock_move.action_confirm(cr, uid, [move_id], context=context)
1059 def action_confirm(self, cr, uid, ids, context=None):
1060 """ Confirms production order.
1061 @return: Newly generated Shipment Id.
1063 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)])
1064 self.action_compute(cr, uid, uncompute_ids, context=context)
1065 for production in self.browse(cr, uid, ids, context=context):
1066 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
1068 # Take routing location as a Source Location.
1069 source_location_id = production.location_src_id.id
1070 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1071 source_location_id = production.bom_id.routing_id.location_id.id
1073 for line in production.product_lines:
1074 self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1075 production.write({'state': 'confirmed'}, context=context)
1078 def force_production(self, cr, uid, ids, *args):
1079 """ Assigns products.
1080 @param *args: Arguments
1083 move_obj = self.pool.get('stock.move')
1084 for order in self.browse(cr, uid, ids):
1085 move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1089 class mrp_production_workcenter_line(osv.osv):
1090 _name = 'mrp.production.workcenter.line'
1091 _description = 'Work Order'
1093 _inherit = ['mail.thread']
1096 'name': fields.char('Work Order', size=64, required=True),
1097 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1098 'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1099 'hour': fields.float('Number of Hours', digits=(16, 2)),
1100 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1101 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1102 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1105 'sequence': lambda *a: 1,
1106 'hour': lambda *a: 0,
1107 'cycle': lambda *a: 0,
1110 class mrp_production_product_line(osv.osv):
1111 _name = 'mrp.production.product.line'
1112 _description = 'Production Scheduled Product'
1114 'name': fields.char('Name', size=64, required=True),
1115 'product_id': fields.many2one('product.product', 'Product', required=True),
1116 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1117 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1118 'product_uos_qty': fields.float('Product UOS Quantity'),
1119 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1120 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1123 class product_product(osv.osv):
1124 _inherit = "product.product"
1126 'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1129 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: