1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
23 from datetime import datetime
25 import openerp.addons.decimal_precision as dp
26 from openerp.osv import fields, osv, orm
27 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
28 from openerp.tools import float_compare
29 from openerp.tools.translate import _
30 from openerp import tools
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, product_qty=0, 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
272 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
273 res['value']['name'] = prod.name
274 res['value']['product_uom'] = prod.uom_id.id
276 #res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
277 res['value']['product_uos'] = prod.uos_id.id
279 res['value']['product_uos_qty'] = 0
282 def onchange_product_qty_change(self, cr, uid, ids, product_id, qty=0, context=None):
284 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
286 return {'value': {'product_uos': prod.uos_id.id, 'product_uos_qty': qty * prod.uos_coeff}}
289 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
291 if not product_uom or not product_id:
293 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
294 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
295 if uom.category_id.id != product.uom_id.category_id.id:
296 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
297 res['value'].update({'product_uom': product.uom_id.id})
300 def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
301 """ Finds BoM for particular product and product uom.
302 @param product_id: Selected product.
303 @param product_uom: Unit of measure of a product.
304 @param properties: List of related properties.
305 @return: False or BoM id.
307 if properties is None:
309 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
310 ids = map(lambda x: x[0], cr.fetchall())
313 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
315 for prop_id in bom.property_ids:
316 if prop_id.id in properties:
318 if (prop > max_prop) or ((max_prop == 0) and not result):
323 def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
324 """ Finds Products and Work Centers for related BoM for manufacturing order.
325 @param bom: BoM of particular product.
326 @param factor: Factor of product UoM.
327 @param properties: A List of properties Ids.
328 @param addthis: If BoM found then True else False.
329 @param level: Depth level to find BoM lines starts from 10.
330 @return: result: List of dictionaries containing product details.
331 result2: List of dictionaries containing Work Center details.
333 routing_obj = self.pool.get('mrp.routing')
334 factor = factor / (bom.product_efficiency or 1.0)
335 factor = rounding(factor, bom.product_rounding)
336 if factor < bom.product_rounding:
337 factor = bom.product_rounding
341 if bom.type == 'phantom' and not bom.bom_lines:
342 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
345 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
346 result = result + res[0]
347 result2 = result2 + res[1]
352 if addthis and not bom.bom_lines:
355 'name': bom.product_id.name,
356 'product_id': bom.product_id.id,
357 'product_qty': bom.product_qty * factor,
358 'product_uom': bom.product_uom.id,
359 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
360 'product_uos': bom.product_uos and bom.product_uos.id or False,
362 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
364 for wc_use in routing.workcenter_lines:
365 wc = wc_use.workcenter_id
366 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
367 mult = (d + (m and 1.0 or 0.0))
368 cycle = mult * wc_use.cycle_nbr
370 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
371 'workcenter_id': wc.id,
372 'sequence': level+(wc_use.sequence or 0),
374 '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)),
376 for bom2 in bom.bom_lines:
377 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
378 result = result + res[0]
379 result2 = result2 + res[1]
380 return result, result2
382 def copy_data(self, cr, uid, id, default=None, context=None):
385 bom_data = self.read(cr, uid, id, [], context=context)
386 default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
387 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
394 return math.ceil(f / r) * r
396 class mrp_production(osv.osv):
398 Production Orders / Manufacturing Orders
400 _name = 'mrp.production'
401 _description = 'Manufacturing Order'
402 _date_name = 'date_planned'
403 _inherit = ['mail.thread', 'ir.needaction_mixin']
405 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
406 """ Calculates total hours and total no. of cycles for a production order.
407 @param prop: Name of field.
409 @return: Dictionary of values.
412 for prod in self.browse(cr, uid, ids, context=context):
417 for wc in prod.workcenter_lines:
418 result[prod.id]['hour_total'] += wc.hour
419 result[prod.id]['cycle_total'] += wc.cycle
422 def _src_id_default(self, cr, uid, ids, context=None):
424 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
425 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
426 except (orm.except_orm, ValueError):
430 def _dest_id_default(self, cr, uid, ids, context=None):
432 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
433 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
434 except (orm.except_orm, ValueError):
438 def _get_progress(self, cr, uid, ids, name, arg, context=None):
439 """ Return product quantity percentage """
440 result = dict.fromkeys(ids, 100)
441 for mrp_production in self.browse(cr, uid, ids, context=context):
442 if mrp_production.product_qty:
444 for move in mrp_production.move_created_ids2:
445 if not move.scrapped and move.product_id == mrp_production.product_id:
446 done += move.product_qty
447 result[mrp_production.id] = done / mrp_production.product_qty * 100
451 'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
452 'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
453 help="Reference of the document that generated this production order request."),
454 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority',
455 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
457 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
458 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
459 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
460 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
461 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
462 'progress': fields.function(_get_progress, type='float',
463 string='Production progress'),
465 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
466 readonly=True, states={'draft':[('readonly',False)]},
467 help="Location where the system will look for components."),
468 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
469 readonly=True, states={'draft':[('readonly',False)]},
470 help="Location where the system will stock the finished products."),
471 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft':[('readonly',False)]}),
472 'date_start': fields.datetime('Start Date', select=True, readonly=True),
473 'date_finished': fields.datetime('End Date', select=True, readonly=True),
474 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]},
475 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
476 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]},
477 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."),
478 'picking_id': fields.many2one('stock.picking', 'Picking List', readonly=True, ondelete="restrict",
479 help="This is the Internal Picking List that brings the finished product to the production plan"),
480 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
481 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume',
482 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
483 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products',
484 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
485 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
486 domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
487 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
488 domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
489 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
490 readonly=True, states={'draft':[('readonly',False)]}),
491 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
492 readonly=True, states={'draft':[('readonly',False)]}),
493 'state': fields.selection(
494 [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
495 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
496 string='Status', readonly=True,
497 track_visibility='onchange',
498 help="When the production order is created the status is set to 'Draft'.\n\
499 If the order is confirmed the status is set to 'Waiting Goods'.\n\
500 If any exceptions are there, the status is set to 'Picking Exception'.\n\
501 If the stock is available then the status is set to 'Ready to Produce'.\n\
502 When the production gets started then the status is set to 'In Production'.\n\
503 When the production is over, the status is set to 'Done'."),
504 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
505 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
506 'user_id':fields.many2one('res.users', 'Responsible'),
507 'company_id': fields.many2one('res.company','Company',required=True),
510 'priority': lambda *a: '1',
511 'state': lambda *a: 'draft',
512 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
513 'product_qty': lambda *a: 1.0,
514 'user_id': lambda self, cr, uid, c: uid,
515 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
516 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
517 'location_src_id': _src_id_default,
518 'location_dest_id': _dest_id_default
521 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
523 _order = 'priority desc, date_planned asc';
525 def _check_qty(self, cr, uid, ids, context=None):
526 for order in self.browse(cr, uid, ids, context=context):
527 if order.product_qty <= 0:
532 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
535 def unlink(self, cr, uid, ids, context=None):
536 for production in self.browse(cr, uid, ids, context=context):
537 if production.state not in ('draft', 'cancel'):
538 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
539 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
541 def copy(self, cr, uid, id, default=None, context=None):
545 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
548 'move_created_ids' : [],
549 'move_created_ids2' : [],
550 'product_lines' : [],
551 'move_prod_id' : False,
554 return super(mrp_production, self).copy(cr, uid, id, default, context)
556 def location_id_change(self, cr, uid, ids, src, dest, context=None):
557 """ Changes destination location if source location is changed.
558 @param src: Source location id.
559 @param dest: Destination location id.
560 @return: Dictionary of values.
565 return {'value': {'location_dest_id': src}}
568 def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
569 print 'product_id_change>>>>>>>>>>\n\n\n'
570 """ Finds UoM of changed product.
571 @param product_id: Id of changed product.
572 @return: Dictionary of values.
576 'product_uom': False,
579 'product_uos_qty': False,
582 bom_obj = self.pool.get('mrp.bom')
583 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
584 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
587 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
588 routing_id = bom_point.routing_id.id or False
590 product_uom_id = product.uom_id and product.uom_id.id or False
591 product_uos_id = product.uos_id and product.uos_id.id or False
593 if product.uos_id.id:
594 result['value']['product_uos_qty'] = product_qty * product.uos_coeff
595 result['value']['product_uos'] = product.uos_id.id
597 result['value']['product_uos_qty'] = 0
599 result['value']['product_uom'] = product_uom_id
600 result['value']['bom_id'] = bom_id
601 result['value']['routing_id'] = routing_id
604 def onchange_product_qty_change(self, cr, uid, ids, product_id, qty=0, context=None):
606 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
608 return {'value': {'product_uos': prod.uos_id.id, 'product_uos_qty': qty * prod.uos_coeff}}
611 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
612 """ Finds routing for changed BoM.
613 @param product: Id of product.
614 @return: Dictionary of values.
620 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
621 routing_id = bom_point.routing_id.id or False
623 'routing_id': routing_id
625 return {'value': result}
627 def action_picking_except(self, cr, uid, ids):
628 """ Changes the state to Exception.
631 self.write(cr, uid, ids, {'state': 'picking_except'})
634 def action_compute(self, cr, uid, ids, properties=None, context=None):
635 """ Computes bills of material of a product.
636 @param properties: List containing dictionaries of properties.
637 @return: No. of products.
639 if properties is None:
642 bom_obj = self.pool.get('mrp.bom')
643 uom_obj = self.pool.get('product.uom')
644 prod_line_obj = self.pool.get('mrp.production.product.line')
645 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
646 for production in self.browse(cr, uid, ids):
648 p_ids = prod_line_obj.search(cr, SUPERUSER_ID, [('production_id', '=', production.id)], context=context)
649 prod_line_obj.unlink(cr, SUPERUSER_ID, p_ids, context=context)
650 w_ids = workcenter_line_obj.search(cr, SUPERUSER_ID, [('production_id', '=', production.id)], context=context)
651 workcenter_line_obj.unlink(cr, SUPERUSER_ID, w_ids, context=context)
653 bom_point = production.bom_id
654 bom_id = production.bom_id.id
656 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
658 bom_point = bom_obj.browse(cr, uid, bom_id)
659 routing_id = bom_point.routing_id.id or False
660 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
663 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
664 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
665 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
669 line['production_id'] = production.id
670 prod_line_obj.create(cr, uid, line)
671 for line in results2:
672 line['production_id'] = production.id
673 workcenter_line_obj.create(cr, uid, line)
676 def action_cancel(self, cr, uid, ids, context=None):
677 """ Cancels the production order and related stock moves.
682 move_obj = self.pool.get('stock.move')
683 for production in self.browse(cr, uid, ids, context=context):
684 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
685 raise osv.except_osv(
686 _('Cannot cancel manufacturing order!'),
687 _('You must first cancel related internal picking attached to this manufacturing order.'))
688 if production.move_created_ids:
689 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
690 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
691 self.write(cr, uid, ids, {'state': 'cancel'})
694 def action_ready(self, cr, uid, ids, context=None):
695 """ Changes the production state to Ready and location id of stock move.
698 move_obj = self.pool.get('stock.move')
699 self.write(cr, uid, ids, {'state': 'ready'})
701 for (production_id,name) in self.name_get(cr, uid, ids):
702 production = self.browse(cr, uid, production_id)
703 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
704 move_obj.write(cr, uid, [production.move_prod_id.id],
705 {'location_id': production.location_dest_id.id})
708 def action_production_end(self, cr, uid, ids, context=None):
709 """ Changes production state to Finish and writes finished date.
712 for production in self.browse(cr, uid, ids):
713 self._costs_generate(cr, uid, production)
714 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
717 def test_production_done(self, cr, uid, ids):
718 """ Tests whether production is done or not.
719 @return: True or False
722 for production in self.browse(cr, uid, ids):
723 if production.move_lines:
726 if production.move_created_ids:
730 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
731 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
732 it's always equal to the quantity encoded in the production order or the production wizard, but if the
733 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
735 :param production_id: ID of the mrp.order
736 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
737 :return: The factor to apply to the quantity that we should produce for the given production order.
741 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
742 """ To produce final product based on production mode (consume/consume&produce).
743 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
744 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
745 and stock move lines of final product will be also done/produced.
746 @param production_id: the ID of mrp.production object
747 @param production_qty: specify qty to produce
748 @param production_mode: specify production mode (consume/consume&produce).
751 stock_mov_obj = self.pool.get('stock.move')
752 production = self.browse(cr, uid, production_id, context=context)
755 for produced_product in production.move_created_ids2:
756 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
758 produced_qty += produced_product.product_qty
759 if production_mode in ['consume','consume_produce']:
762 # Calculate already consumed qtys
763 for consumed in production.move_lines2:
764 if consumed.scrapped:
766 if not consumed_data.get(consumed.product_id.id, False):
767 consumed_data[consumed.product_id.id] = 0
768 consumed_data[consumed.product_id.id] += consumed.product_qty
770 # Find product qty to be consumed and consume it
771 for scheduled in production.product_lines:
773 # total qty of consumed product we need after this consumption
774 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
776 # qty available for consume and produce
777 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
780 # there will be nothing to consume for this raw material
783 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
785 # qtys we have to consume
786 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
787 if float_compare(qty, qty_avail, precision_rounding=scheduled.product_id.uom_id.rounding) == 1:
788 # if qtys we have to consume is more than qtys available to consume
789 prod_name = scheduled.product_id.name_get()[0][1]
790 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))
792 # we already have more qtys consumed than we need
795 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
797 if production_mode == 'consume_produce':
798 # To produce remaining qty of final product
799 #vals = {'state':'confirmed'}
800 #final_product_todo = [x.id for x in production.move_created_ids]
801 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
802 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
803 produced_products = {}
804 for produced_product in production.move_created_ids2:
805 if produced_product.scrapped:
807 if not produced_products.get(produced_product.product_id.id, False):
808 produced_products[produced_product.product_id.id] = 0
809 produced_products[produced_product.product_id.id] += produced_product.product_qty
811 for produce_product in production.move_created_ids:
812 produced_qty = produced_products.get(produce_product.product_id.id, 0)
813 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
814 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
816 if rest_qty < production_qty:
817 prod_name = produce_product.product_id.name_get()[0][1]
818 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))
820 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
822 for raw_product in production.move_lines2:
824 parent_move_ids = [x.id for x in raw_product.move_history_ids]
825 for final_product in production.move_created_ids2:
826 if final_product.id not in parent_move_ids:
827 new_parent_ids.append(final_product.id)
828 for new_parent_id in new_parent_ids:
829 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
830 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
831 self.signal_button_produce_done(cr, uid, [production_id])
834 def _costs_generate(self, cr, uid, production):
835 """ Calculates total costs at the end of the production.
836 @param production: Id of production order.
837 @return: Calculated amount.
840 analytic_line_obj = self.pool.get('account.analytic.line')
841 for wc_line in production.workcenter_lines:
842 wc = wc_line.workcenter_id
843 if wc.costs_journal_id and wc.costs_general_account_id:
845 value = wc_line.hour * wc.costs_hour
846 account = wc.costs_hour_account_id.id
847 if value and account:
849 analytic_line_obj.create(cr, uid, {
850 'name': wc_line.name + ' (H)',
852 'account_id': account,
853 'general_account_id': wc.costs_general_account_id.id,
854 'journal_id': wc.costs_journal_id.id,
856 'product_id': wc.product_id.id,
857 'unit_amount': wc_line.hour,
858 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
861 value = wc_line.cycle * wc.costs_cycle
862 account = wc.costs_cycle_account_id.id
863 if value and account:
865 analytic_line_obj.create(cr, uid, {
866 'name': wc_line.name+' (C)',
868 'account_id': account,
869 'general_account_id': wc.costs_general_account_id.id,
870 'journal_id': wc.costs_journal_id.id,
872 'product_id': wc.product_id.id,
873 'unit_amount': wc_line.cycle,
874 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
878 def action_in_production(self, cr, uid, ids, context=None):
879 """ Changes state to In Production and writes starting date.
882 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
884 def test_if_product(self, cr, uid, ids):
886 @return: True or False
889 for production in self.browse(cr, uid, ids):
890 if not production.product_lines:
891 if not self.action_compute(cr, uid, [production.id]):
895 def _get_auto_picking(self, cr, uid, production):
898 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
899 procurement_order = self.pool.get('procurement.order')
900 production = production_line.production_id
901 location_id = production.location_src_id.id
902 date_planned = production.date_planned
903 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
904 procurement_id = procurement_order.create(cr, uid, {
905 'name': procurement_name,
906 'origin': procurement_name,
907 'date_planned': date_planned,
908 'product_id': production_line.product_id.id,
909 'product_qty': production_line.product_qty,
910 'product_uom': production_line.product_uom.id,
911 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
912 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
913 'location_id': location_id,
914 'procure_method': production_line.product_id.procure_method,
915 'move_id': shipment_move_id,
916 'company_id': production.company_id.id,
918 procurement_order.signal_button_confirm(cr, uid, [procurement_id])
919 return procurement_id
921 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
922 stock_move = self.pool.get('stock.move')
923 production = production_line.production_id
924 date_planned = production.date_planned
925 # Internal shipment is created for Stockable and Consumer Products
926 if production_line.product_id.type not in ('product', 'consu'):
928 source_location_id = production.location_src_id.id
929 if not destination_location_id:
930 destination_location_id = source_location_id
931 return stock_move.create(cr, uid, {
932 'name': production.name,
933 'picking_id': shipment_id,
934 'product_id': production_line.product_id.id,
935 'product_qty': production_line.product_qty,
936 'product_uom': production_line.product_uom.id,
937 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
938 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
939 'date': date_planned,
940 'move_dest_id': parent_move_id,
941 'location_id': source_location_id,
942 'location_dest_id': destination_location_id,
944 'company_id': production.company_id.id,
947 def _make_production_internal_shipment(self, cr, uid, production, context=None):
948 ir_sequence = self.pool.get('ir.sequence')
949 stock_picking = self.pool.get('stock.picking')
951 pick_type = 'internal'
954 # Take routing address as a Shipment Address.
955 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
956 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
957 routing_loc = production.bom_id.routing_id.location_id
958 if routing_loc.usage != 'internal':
960 partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
962 # Take next Sequence number of shipment base on type
963 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
965 picking_id = stock_picking.create(cr, uid, {
967 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
971 'partner_id': partner_id,
972 'auto_picking': self._get_auto_picking(cr, uid, production),
973 'company_id': production.company_id.id,
975 production.write({'picking_id': picking_id}, context=context)
978 def _make_production_produce_line(self, cr, uid, production, context=None):
979 stock_move = self.pool.get('stock.move')
980 source_location_id = production.product_id.property_stock_production.id
981 destination_location_id = production.location_dest_id.id
983 'name': production.name,
984 'date': production.date_planned,
985 'product_id': production.product_id.id,
986 'product_qty': production.product_qty,
987 'product_uom': production.product_uom.id,
988 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
989 'product_uos': production.product_uos and production.product_uos.id or False,
990 'location_id': source_location_id,
991 'location_dest_id': destination_location_id,
992 'move_dest_id': production.move_prod_id.id,
994 'company_id': production.company_id.id,
996 move_id = stock_move.create(cr, uid, data, context=context)
997 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
1000 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
1001 stock_move = self.pool.get('stock.move')
1002 production = production_line.production_id
1003 # Internal shipment is created for Stockable and Consumer Products
1004 if production_line.product_id.type not in ('product', 'consu'):
1006 destination_location_id = production.product_id.property_stock_production.id
1007 if not source_location_id:
1008 source_location_id = production.location_src_id.id
1009 move_id = stock_move.create(cr, uid, {
1010 'name': production.name,
1011 'date': production.date_planned,
1012 'product_id': production_line.product_id.id,
1013 'product_qty': production_line.product_qty,
1014 'product_uom': production_line.product_uom.id,
1015 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
1016 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
1017 'location_id': source_location_id,
1018 'location_dest_id': destination_location_id,
1019 'move_dest_id': parent_move_id,
1021 'company_id': production.company_id.id,
1023 production.write({'move_lines': [(4, move_id)]}, context=context)
1026 def action_confirm(self, cr, uid, ids, context=None):
1027 """ Confirms production order.
1028 @return: Newly generated Shipment Id.
1031 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)])
1032 self.action_compute(cr, uid, uncompute_ids, context=context)
1033 for production in self.browse(cr, uid, ids, context=context):
1034 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
1035 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
1037 # Take routing location as a Source Location.
1038 source_location_id = production.location_src_id.id
1039 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1040 source_location_id = production.bom_id.routing_id.location_id.id
1042 for line in production.product_lines:
1043 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1044 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1045 destination_location_id=source_location_id, context=context)
1046 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1048 self.pool.get('stock.picking').signal_button_confirm(cr, uid, [shipment_id])
1049 production.write({'state':'confirmed'}, context=context)
1052 def force_production(self, cr, uid, ids, *args):
1053 """ Assigns products.
1054 @param *args: Arguments
1057 pick_obj = self.pool.get('stock.picking')
1058 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1062 class mrp_production_workcenter_line(osv.osv):
1063 _name = 'mrp.production.workcenter.line'
1064 _description = 'Work Order'
1066 _inherit = ['mail.thread']
1069 'name': fields.char('Work Order', size=64, required=True),
1070 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1071 'cycle': fields.float('Number of Cycles', digits=(16,2)),
1072 'hour': fields.float('Number of Hours', digits=(16,2)),
1073 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1074 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1075 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1078 'sequence': lambda *a: 1,
1079 'hour': lambda *a: 0,
1080 'cycle': lambda *a: 0,
1083 class mrp_production_product_line(osv.osv):
1084 _name = 'mrp.production.product.line'
1085 _description = 'Production Scheduled Product'
1087 'name': fields.char('Name', size=64, required=True),
1088 'product_id': fields.many2one('product.product', 'Product', required=True),
1089 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1090 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1091 'product_uos_qty': fields.float('Product UOS Quantity'),
1092 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1093 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1096 class product_product(osv.osv):
1097 _inherit = "product.product"
1099 'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1102 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: