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 ##############################################################################
22 from datetime import datetime
23 from osv import osv, fields
24 from tools.translate import _
30 #----------------------------------------------------------
32 #----------------------------------------------------------
33 # capacity_hour : capacity per hour. default: 1.0.
34 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
35 # unit_per_cycle : how many units are produced for one cycle
37 class mrp_workcenter(osv.osv):
38 _name = 'mrp.workcenter'
39 _description = 'Work Center'
40 _inherits = {'resource.resource':"resource_id"}
42 'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
43 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this workcenter can do in parallel. If this workcenter represents a team of 5 workers, the capacity per cycle is 5."),
44 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
45 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
46 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
47 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Workcenter per hour."),
48 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
49 help="Complete this only if you want automatic analytic accounting entries on production orders."),
50 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Workcenter per cycle."),
51 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
52 help="Complete this only if you want automatic analytic accounting entries on production orders."),
53 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
54 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
55 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
56 'product_id': fields.many2one('product.product','Product'),
59 'capacity_per_cycle': 1.0,
60 'resource_type': 'material',
63 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
69 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
70 res = {'value' : {'costs_hour' :cost.standard_price }}
76 class mrp_routing(osv.osv):
78 For specifying the routings of workcenters.
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 true, 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)
103 class mrp_routing_workcenter(osv.osv):
105 Defines working cycles and hours of a workcenter using routings.
107 _name = 'mrp.routing.workcenter'
108 _description = 'Workcenter 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 workcenters."),
113 'cycle_nbr': fields.float('Number of Cycles', required=True,
114 help="Number of operations this workcenter can do."),
115 'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for doing one cycle."),
116 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
117 help="Routing indicates all the workcenters used, for how long and/or cycles." \
118 "If Routing is indicated then,the third tab of a production order (workcenters) 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'),
123 'cycle_nbr': lambda *a: 1.0,
124 'hour_nbr': lambda *a: 0.0,
126 mrp_routing_workcenter()
128 class mrp_bom(osv.osv):
130 Defines bills of material for a product.
133 _description = 'Bill of Material'
135 def _child_compute(self, cr, uid, ids, name, arg, context={}):
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
147 bom_obj = self.pool.get('mrp.bom')
148 bom_id = context and context.get('active_id', False) or False
149 cr.execute('select id from mrp_bom')
150 if all(bom_id != r[0] for r in cr.fetchall()):
153 bom_parent = bom_obj.browse(cr, uid, bom_id)
154 for bom in self.browse(cr, uid, ids, context=context):
155 if (bom_parent) or (bom.id == bom_id):
156 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
161 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
162 if (bom.type=='phantom' or ok):
163 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
165 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
166 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
170 def _compute_type(self, cr, uid, ids, field_name, arg, context):
171 """ Sets particular method for the selected bom type.
172 @param field_name: Name of the field
173 @param arg: User defined argument
174 @return: Dictionary of values
176 res = dict(map(lambda x: (x,''), ids))
177 for line in self.browse(cr, uid, ids):
178 if line.type == 'phantom' and not line.bom_id:
181 if line.bom_lines or line.type == 'phantom':
183 if line.product_id.supply_method == 'produce':
184 if line.product_id.procure_method == 'make_to_stock':
185 res[line.id] = 'stock'
187 res[line.id] = 'order'
191 'name': fields.char('Name', size=64, required=True),
192 'code': fields.char('Reference', size=16),
193 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the bills of material without removing it."),
194 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
195 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
196 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
197 "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."),
198 'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
199 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
200 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
201 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
202 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
203 'product_id': fields.many2one('product.product', 'Product', required=True),
204 'product_uos_qty': fields.float('Product UOS Qty'),
205 '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."),
206 'product_qty': fields.float('Product Qty', required=True),
207 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
208 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
209 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
210 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
211 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
212 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of workcenters) to produce the finished product. The routing is mainly used to compute workcenter costs during operations and to plan future loads on workcenters based on production planning."),
213 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
214 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
215 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
216 'company_id': fields.many2one('res.company','Company',required=True),
219 'active': lambda *a: 1,
220 'product_efficiency': lambda *a: 1.0,
221 'product_qty': lambda *a: 1.0,
222 'product_rounding': lambda *a: 0.0,
223 'type': lambda *a: 'normal',
224 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
228 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
229 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
232 def _check_recursion(self, cr, uid, ids):
235 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
236 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
242 (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
246 def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
247 """ Changes UoM and name if product_id changes.
248 @param name: Name of the field
249 @param product_id: Changed product_id
250 @return: Dictionary of changed values
253 prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
254 v = {'product_uom': prod.uom_id.id}
256 v['name'] = prod.name
260 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
261 """ Finds BoM for particular product and product uom.
262 @param product_id: Selected product.
263 @param product_uom: Unit of measure of a product.
264 @param properties: List of related properties.
265 @return: False or BoM id.
267 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
268 ids = map(lambda x: x[0], cr.fetchall())
271 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
273 for prop_id in bom.property_ids:
274 if prop_id.id in properties:
276 if (prop > max_prop) or ((max_prop == 0) and not result):
281 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
282 """ Finds Products and Workcenters for related BoM for manufacturing order.
283 @param bom: BoM of particular product.
284 @param factor: Factor of product UoM.
285 @param properties: A List of properties Ids.
286 @param addthis: If BoM found then True else False.
287 @param level: Depth level to find BoM lines starts from 10.
288 @return: result: List of dictionaries containing product details.
289 result2: List of dictionaries containing workcenter details.
291 factor = factor / (bom.product_efficiency or 1.0)
292 factor = rounding(factor, bom.product_rounding)
293 if factor < bom.product_rounding:
294 factor = bom.product_rounding
298 if bom.type == 'phantom' and not bom.bom_lines:
299 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
301 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
302 result = result + res[0]
303 result2 = result2 + res[1]
308 if addthis and not bom.bom_lines:
311 'name': bom.product_id.name,
312 'product_id': bom.product_id.id,
313 'product_qty': bom.product_qty * factor,
314 'product_uom': bom.product_uom.id,
315 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
316 'product_uos': bom.product_uos and bom.product_uos.id or False,
319 for wc_use in bom.routing_id.workcenter_lines:
320 wc = wc_use.workcenter_id
321 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
322 mult = (d + (m and 1.0 or 0.0))
323 cycle = mult * wc_use.cycle_nbr
325 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
326 'workcenter_id': wc.id,
327 'sequence': level+(wc_use.sequence or 0),
329 '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)),
331 for bom2 in bom.bom_lines:
332 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
333 result = result + res[0]
334 result2 = result2 + res[1]
335 return result, result2
339 class mrp_bom_revision(osv.osv):
340 _name = 'mrp.bom.revision'
341 _description = 'Bill of Material Revision'
343 'name': fields.char('Modification name', size=64, required=True),
344 'description': fields.text('Description'),
345 'date': fields.date('Modification Date'),
346 'indice': fields.char('Revision', size=16),
347 'last_indice': fields.char('last indice', size=64),
348 'author_id': fields.many2one('res.users', 'Author'),
349 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
353 'author_id': lambda x, y, z, c: z,
354 'date': lambda *a: time.strftime('%Y-%m-%d'),
362 return round(f / r) * r
364 class mrp_production(osv.osv):
366 Production Orders / Manufacturing Orders
368 _name = 'mrp.production'
369 _description = 'Manufacturing Order'
370 _date_name = 'date_planned'
372 def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
373 """ Calculates total hours and total no. of cycles for a production order.
374 @param prop: Name of field.
376 @return: Dictionary of values.
379 for prod in self.browse(cr, uid, ids, context=context):
384 for wc in prod.workcenter_lines:
385 result[prod.id]['hour_total'] += wc.hour
386 result[prod.id]['cycle_total'] += wc.cycle
389 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
390 """ Finds production end date.
391 @param prop: Name of field.
393 @return: Dictionary of values.
396 for prod in self.browse(cr, uid, ids, context=context):
397 result[prod.id] = prod.date_planned
400 def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
401 """ Finds production planned date.
402 @param prop: Name of field.
404 @return: Dictionary of values.
407 for prod in self.browse(cr, uid, ids, context=context):
408 result[prod.id] = prod.date_planned[:10]
412 'name': fields.char('Reference', size=64, required=True),
413 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
414 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
416 'product_id': fields.many2one('product.product', 'Product', required=True, ),
417 'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
418 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
419 'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
420 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
422 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
423 help="Location where the system will look for components."),
424 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
425 help="Location where the system will stock the finished products."),
427 'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
428 'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
429 'date_planned': fields.datetime('Scheduled date', required=True, select=1),
430 'date_start': fields.datetime('Start Date'),
431 'date_finished': fields.datetime('End Date'),
433 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
434 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', help="The list of operations (list of workcenters) to produce the finished product. The routing is mainly used to compute workcenter costs during operations and to plan future loads on workcenters based on production plannification."),
436 'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
437 help="This is the internal picking list that brings the finished product to the production plan"),
438 'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
439 'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
440 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
441 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
442 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
443 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
444 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
445 'state': fields.selection([('draft','Draft'),('picking_except', 'Picking Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','In Production'),('cancel','Cancelled'),('done','Done')],'State', readonly=True,
446 help='When the production order is created the state is set to \'Draft\'.\n If the order is confirmed the state is set to \'Waiting Goods\'.\n If any exceptions are there, the state is set to \'Picking Exception\'.\
447 \nIf the stock is available then the state is set to \'Ready to Produce\'.\n When the production gets started then the state is set to \'In Production\'.\n When the production is over, the state is set to \'Done\'.'),
448 'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
449 'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
450 'company_id': fields.many2one('res.company','Company',required=True),
453 'priority': lambda *a: '1',
454 'state': lambda *a: 'draft',
455 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
456 'product_qty': lambda *a: 1.0,
457 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
458 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
460 _order = 'priority desc, date_planned asc';
462 def _check_qty(self, cr, uid, ids):
463 orders = self.browse(cr, uid, ids)
465 if order.product_qty <= 0:
470 (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
473 def unlink(self, cr, uid, ids, context=None):
474 productions = self.read(cr, uid, ids, ['state'])
476 for s in productions:
477 if s['state'] in ['draft','cancel']:
478 unlink_ids.append(s['id'])
480 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!') % s['state'])
481 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
483 def copy(self, cr, uid, id, default=None, context=None):
487 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
490 'move_created_ids' : [],
491 'move_created_ids2' : [],
492 'product_lines' : [],
495 return super(mrp_production, self).copy(cr, uid, id, default, context)
497 def location_id_change(self, cr, uid, ids, src, dest, context={}):
498 """ Changes destination location if source location is changed.
499 @param src: Source location id.
500 @param dest: Destination location id.
501 @return: Dictionary of values.
506 return {'value': {'location_dest_id': src}}
509 def product_id_change(self, cr, uid, ids, product_id, context=None):
510 """ Finds UoM of changed product.
511 @param product_id: Id of changed product.
512 @return: Dictionary of values.
516 'product_uom': False,
520 bom_obj = self.pool.get('mrp.bom')
521 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
522 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
525 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
526 routing_id = bom_point.routing_id.id or False
528 'product_uom': product.uom_id and product.uom_id.id or False,
530 'routing_id': routing_id
532 return {'value': result}
534 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
535 """ Finds routing for changed BoM.
536 @param product: Id of product.
537 @return: Dictionary of values.
543 bom_pool = self.pool.get('mrp.bom')
544 bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
545 routing_id = bom_point.routing_id.id or False
547 'routing_id': routing_id
549 return {'value': result}
551 def action_picking_except(self, cr, uid, ids):
552 """ Changes the state to Exception.
555 self.write(cr, uid, ids, {'state': 'picking_except'})
558 def action_compute(self, cr, uid, ids, properties=[]):
559 """ Computes bills of material of a product.
560 @param properties: List containing dictionaries of properties.
561 @return: No. of products.
564 bom_obj = self.pool.get('mrp.bom')
565 prod_line_obj = self.pool.get('mrp.production.product.line')
566 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
567 for production in self.browse(cr, uid, ids):
568 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
569 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
570 bom_point = production.bom_id
571 bom_id = production.bom_id.id
573 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
575 bom_point = bom_obj.browse(cr, uid, bom_id)
576 routing_id = bom_point.routing_id.id or False
577 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
580 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
582 factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
583 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
587 line['production_id'] = production.id
588 prod_line_obj.create(cr, uid, line)
589 for line in results2:
590 line['production_id'] = production.id
591 workcenter_line_obj.create(cr, uid, line)
594 def action_cancel(self, cr, uid, ids):
595 """ Cancels the production order and related stock moves.
598 move_obj = self.pool.get('stock.move')
599 for production in self.browse(cr, uid, ids):
600 if production.move_created_ids:
601 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
602 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
603 self.write(cr, uid, ids, {'state': 'cancel'})
606 def action_ready(self, cr, uid, ids):
607 """ Changes the production state to Ready and location id of stock move.
610 move_obj = self.pool.get('stock.move')
611 self.write(cr, uid, ids, {'state': 'ready'})
613 for (production_id,name) in self.name_get(cr, uid, ids):
614 production = self.browse(cr, uid, production_id)
615 if production.move_prod_id:
616 move_obj.write(cr, uid, [production.move_prod_id.id],
617 {'location_id': production.location_dest_id.id})
619 message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
620 self.log(cr, uid, production_id, message)
623 def action_production_end(self, cr, uid, ids):
624 """ Changes production state to Finish and writes finished date.
627 for production in self.browse(cr, uid, ids):
628 self._costs_generate(cr, uid, production)
629 return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
631 def test_production_done(self, cr, uid, ids):
632 """ Tests whether production is done or not.
633 @return: True or False
636 for production in self.browse(cr, uid, ids):
637 if production.move_lines:
640 if production.move_created_ids:
644 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
645 """ To produce final product based on production mode (consume/consume&produce).
646 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
647 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
648 and stock move lines of final product will be also done/produced.
649 @param production_id: the ID of mrp.production object
650 @param production_qty: specify qty to produce
651 @param production_mode: specify production mode (consume/consume&produce).
655 stock_mov_obj = self.pool.get('stock.move')
656 production = self.browse(cr, uid, production_id)
658 final_product_todo = []
661 if production_mode == 'consume_produce':
662 produced_qty = production_qty
664 for produced_product in production.move_created_ids2:
665 if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
667 produced_qty += produced_product.product_qty
669 if production_mode in ['consume','consume_produce']:
670 consumed_products = {}
672 scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
674 for consumed_product in production.move_lines2:
675 consumed = consumed_product.product_qty
676 if consumed_product.scrapped:
678 if not consumed_products.get(consumed_product.product_id.id, False):
679 consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
680 check[consumed_product.product_id.id] = 0
681 for f in production.product_lines:
682 if f.product_id.id == consumed_product.product_id.id:
683 if (len(production.move_lines2) - scrapped) > len(production.product_lines):
684 check[consumed_product.product_id.id] += consumed_product.product_qty
685 consumed = check[consumed_product.product_id.id]
686 rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
687 consumed_products[consumed_product.product_id.id] = rest_consumed
689 for raw_product in production.move_lines:
690 for f in production.product_lines:
691 if f.product_id.id == raw_product.product_id.id:
692 consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
693 if consumed_qty == 0:
694 consumed_qty = production_qty * f.product_qty / production.product_qty
696 stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
698 if production_mode == 'consume_produce':
699 # To produce remaining qty of final product
700 vals = {'state':'confirmed'}
701 final_product_todo = [x.id for x in production.move_created_ids]
702 stock_mov_obj.write(cr, uid, final_product_todo, vals)
703 produced_products = {}
704 for produced_product in production.move_created_ids2:
705 if produced_product.scrapped:
707 if not produced_products.get(produced_product.product_id.id, False):
708 produced_products[produced_product.product_id.id] = 0
709 produced_products[produced_product.product_id.id] += produced_product.product_qty
711 for produce_product in production.move_created_ids:
712 produced_qty = produced_products.get(produce_product.product_id.id, 0)
713 rest_qty = production.product_qty - produced_qty
714 if rest_qty <= production_qty:
715 production_qty = rest_qty
717 stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
719 for raw_product in production.move_lines2:
721 parent_move_ids = [x.id for x in raw_product.move_history_ids]
722 for final_product in production.move_created_ids2:
723 if final_product.id not in parent_move_ids:
724 new_parent_ids.append(final_product.id)
725 for new_parent_id in new_parent_ids:
726 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
728 wf_service = netsvc.LocalService("workflow")
729 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
732 def _costs_generate(self, cr, uid, production):
733 """ Calculates total costs at the end of the production.
734 @param production: Id of production order.
735 @return: Calculated amount.
738 analytic_line_obj = self.pool.get('account.analytic.line')
739 for wc_line in production.workcenter_lines:
740 wc = wc_line.workcenter_id
741 if wc.costs_journal_id and wc.costs_general_account_id:
742 value = wc_line.hour * wc.costs_hour
743 account = wc.costs_hour_account_id.id
744 if value and account:
746 analytic_line_obj.create(cr, uid, {
747 'name': wc_line.name + ' (H)',
749 'account_id': account,
750 'general_account_id': wc.costs_general_account_id.id,
751 'journal_id': wc.costs_journal_id.id,
753 'product_id': wc.product_id.id,
754 'unit_amount': wc.costs_hour * wc.time_cycle,
755 'product_uom_id': wc.product_id.uom_id.id
757 if wc.costs_journal_id and wc.costs_general_account_id:
758 value = wc_line.cycle * wc.costs_cycle
759 account = wc.costs_cycle_account_id.id
760 if value and account:
762 analytic_line_obj.create(cr, uid, {
763 'name': wc_line.name+' (C)',
765 'account_id': account,
766 'general_account_id': wc.costs_general_account_id.id,
767 'journal_id': wc.costs_journal_id.id,
769 'product_id': wc.product_id.id,
770 'unit_amount': wc.costs_hour * wc.time_cycle,
771 'product_uom_id': wc.product_id.uom_id.id
775 def action_in_production(self, cr, uid, ids):
776 """ Changes state to In Production and writes starting date.
779 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
782 def test_if_product(self, cr, uid, ids):
784 @return: True or False
787 for production in self.browse(cr, uid, ids):
788 if not production.product_lines:
789 if not self.action_compute(cr, uid, [production.id]):
793 def _get_auto_picking(self, cr, uid, production):
796 def action_confirm(self, cr, uid, ids):
797 """ Confirms production order.
798 @return: Newly generated picking Id.
802 seq_obj = self.pool.get('ir.sequence')
803 pick_obj = self.pool.get('stock.picking')
804 move_obj = self.pool.get('stock.move')
805 proc_obj = self.pool.get('procurement.order')
806 wf_service = netsvc.LocalService("workflow")
807 for production in self.browse(cr, uid, ids):
808 if not production.product_lines:
809 self.action_compute(cr, uid, [production.id])
810 production = self.browse(cr, uid, [production.id])[0]
812 pick_type = 'internal'
814 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
815 routing_loc = production.bom_id.routing_id.location_id
816 if routing_loc.usage <> 'internal':
818 address_id = routing_loc.address_id and routing_loc.address_id.id or False
819 routing_loc = routing_loc.id
820 pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
821 picking_id = pick_obj.create(cr, uid, {
823 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
827 'address_id': address_id,
828 'auto_picking': self._get_auto_picking(cr, uid, production),
829 'company_id': production.company_id.id,
832 source = production.product_id.product_tmpl_id.property_stock_production.id
834 'name':'PROD:' + production.name,
835 'date': production.date_planned,
836 'product_id': production.product_id.id,
837 'product_qty': production.product_qty,
838 'product_uom': production.product_uom.id,
839 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
840 'product_uos': production.product_uos and production.product_uos.id or False,
841 'location_id': source,
842 'location_dest_id': production.location_dest_id.id,
843 'move_dest_id': production.move_prod_id.id,
845 'company_id': production.company_id.id,
847 res_final_id = move_obj.create(cr, uid, data)
849 self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
851 for line in production.product_lines:
853 newdate = production.date_planned
854 if line.product_id.type in ('product', 'consu'):
855 res_dest_id = move_obj.create(cr, uid, {
856 'name':'PROD:' + production.name,
857 'date': production.date_planned,
858 'product_id': line.product_id.id,
859 'product_qty': line.product_qty,
860 'product_uom': line.product_uom.id,
861 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
862 'product_uos': line.product_uos and line.product_uos.id or False,
863 'location_id': routing_loc or production.location_src_id.id,
864 'location_dest_id': source,
865 'move_dest_id': res_final_id,
867 'company_id': production.company_id.id,
869 moves.append(res_dest_id)
870 move_id = move_obj.create(cr, uid, {
871 'name':'PROD:' + production.name,
872 'picking_id':picking_id,
873 'product_id': line.product_id.id,
874 'product_qty': line.product_qty,
875 'product_uom': line.product_uom.id,
876 'product_uos_qty': line.product_uos and line.product_uos_qty or False,
877 'product_uos': line.product_uos and line.product_uos.id or False,
879 'move_dest_id': res_dest_id,
880 'location_id': production.location_src_id.id,
881 'location_dest_id': routing_loc or production.location_src_id.id,
883 'company_id': production.company_id.id,
885 proc_id = proc_obj.create(cr, uid, {
886 'name': (production.origin or '').split(':')[0] + ':' + production.name,
887 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
888 'date_planned': newdate,
889 'product_id': line.product_id.id,
890 'product_qty': line.product_qty,
891 'product_uom': line.product_uom.id,
892 'product_uos_qty': line.product_uos and line.product_qty or False,
893 'product_uos': line.product_uos and line.product_uos.id or False,
894 'location_id': production.location_src_id.id,
895 'procure_method': line.product_id.procure_method,
897 'company_id': production.company_id.id,
899 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
900 proc_ids.append(proc_id)
901 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
902 self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
903 message = _("Manufacturing order '%s' is scheduled for the %s.") % (
905 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
907 self.log(cr, uid, production.id, message)
910 def force_production(self, cr, uid, ids, *args):
911 """ Assigns products.
912 @param *args: Arguments
915 pick_obj = self.pool.get('stock.picking')
916 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
921 class mrp_production_workcenter_line(osv.osv):
922 _name = 'mrp.production.workcenter.line'
923 _description = 'Work Order'
927 'name': fields.char('Work Order', size=64, required=True),
928 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
929 'cycle': fields.float('Nbr of cycles', digits=(16,2)),
930 'hour': fields.float('Nbr of hours', digits=(16,2)),
931 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
932 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
935 'sequence': lambda *a: 1,
936 'hour': lambda *a: 0,
937 'cycle': lambda *a: 0,
939 mrp_production_workcenter_line()
941 class mrp_production_product_line(osv.osv):
942 _name = 'mrp.production.product.line'
943 _description = 'Production Scheduled Product'
945 'name': fields.char('Name', size=64, required=True),
946 'product_id': fields.many2one('product.product', 'Product', required=True),
947 'product_qty': fields.float('Product Qty', required=True),
948 'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
949 'product_uos_qty': fields.float('Product UOS Qty'),
950 'product_uos': fields.many2one('product.uom', 'Product UOS'),
951 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
953 mrp_production_product_line()
955 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: