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 import decimal_precision as dp
25 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
26 from tools.translate import _
32 #----------------------------------------------------------
34 #----------------------------------------------------------
35 # capacity_hour : capacity per hour. default: 1.0.
36 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
37 # unit_per_cycle : how many units are produced for one cycle
39 class mrp_workcenter(osv.osv):
40 _name = 'mrp.workcenter'
41 _description = 'Work Center'
42 _inherits = {'resource.resource':"resource_id"}
44 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
45 'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this Work Center can do in parallel. If this Work Center represents a team of 5 workers, the capacity per cycle is 5."),
46 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
47 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
48 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
49 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
50 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
51 help="Fill this only if you want automatic analytic accounting entries on production orders."),
52 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
53 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
54 help="Fill this only if you want automatic analytic accounting entries on production orders."),
55 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
56 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
57 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
58 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
61 'capacity_per_cycle': 1.0,
62 'resource_type': 'material',
65 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
69 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
70 value = {'costs_hour': cost.standard_price}
71 return {'value': value}
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)
103 class mrp_routing_workcenter(osv.osv):
105 Defines working cycles and hours of a Work Center using routings.
107 _name = 'mrp.routing.workcenter'
108 _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,
126 mrp_routing_workcenter()
128 class mrp_bom(osv.osv):
130 Defines bills of material for a product.
133 _description = 'Bill of Material'
134 _inherit = ['mail.thread']
136 def _child_compute(self, cr, uid, ids, name, arg, context=None):
138 @param self: The object pointer
139 @param cr: The current row, from the database cursor,
140 @param uid: The current user ID for security checks
141 @param ids: List of selected IDs
142 @param name: Name of the field
143 @param arg: User defined argument
144 @param context: A standard dictionary for contextual values
145 @return: Dictionary of values
150 bom_obj = self.pool.get('mrp.bom')
151 bom_id = context and context.get('active_id', False) or False
152 cr.execute('select id from mrp_bom')
153 if all(bom_id != r[0] for r in cr.fetchall()):
156 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
157 for bom in self.browse(cr, uid, ids, context=context):
158 if (bom_parent) or (bom.id == bom_id):
159 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
164 ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
165 if (bom.type=='phantom' or ok):
166 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
168 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
169 result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
173 def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
174 """ Sets particular method for the selected bom type.
175 @param field_name: Name of the field
176 @param arg: User defined argument
177 @return: Dictionary of values
179 res = dict.fromkeys(ids, False)
180 for line in self.browse(cr, uid, ids, context=context):
181 if line.type == 'phantom' and not line.bom_id:
184 if line.bom_lines or line.type == 'phantom':
186 if line.product_id.supply_method == 'produce':
187 if line.product_id.procure_method == 'make_to_stock':
188 res[line.id] = 'stock'
190 res[line.id] = 'order'
194 'name': fields.char('Name', size=64, required=True),
195 'code': fields.char('Reference', size=16),
196 '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."),
197 'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
198 help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
199 "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
200 "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."),
201 'method': fields.function(_compute_type, string='Method', type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
202 'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
203 'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
204 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
205 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
206 'product_id': fields.many2one('product.product', 'Product', required=True),
207 'product_uos_qty': fields.float('Product UOS Qty'),
208 '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."),
209 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
210 '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"),
211 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
212 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
213 'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
214 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
215 '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."),
216 'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
217 'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
218 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
219 'company_id': fields.many2one('res.company','Company',required=True),
222 'active': lambda *a: 1,
223 'product_efficiency': lambda *a: 1.0,
224 'product_qty': lambda *a: 1.0,
225 'product_rounding': lambda *a: 0.0,
226 'type': lambda *a: 'normal',
227 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
231 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
232 'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
235 def _check_recursion(self, cr, uid, ids, context=None):
238 cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
239 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
245 def _check_product(self, cr, uid, ids, context=None):
247 boms = self.browse(cr, uid, ids, context=context)
251 if bom.product_id.id in all_prod:
253 all_prod.append(bom.product_id.id)
254 lines = bom.bom_lines
256 res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
258 return check_bom(boms)
261 (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
262 (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
265 def onchange_product_id(self, cr, uid, ids, product_id, name, context=None):
266 """ Changes UoM and name if product_id changes.
267 @param name: Name of the field
268 @param product_id: Changed product_id
269 @return: Dictionary of changed values
272 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
273 return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}}
276 def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
277 """ Finds BoM for particular product and product uom.
278 @param product_id: Selected product.
279 @param product_uom: Unit of measure of a product.
280 @param properties: List of related properties.
281 @return: False or BoM id.
283 cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
284 ids = map(lambda x: x[0], cr.fetchall())
287 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
289 for prop_id in bom.property_ids:
290 if prop_id.id in properties:
292 if (prop > max_prop) or ((max_prop == 0) and not result):
297 def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0, routing_id=False):
298 """ Finds Products and Work Centers for related BoM for manufacturing order.
299 @param bom: BoM of particular product.
300 @param factor: Factor of product UoM.
301 @param properties: A List of properties Ids.
302 @param addthis: If BoM found then True else False.
303 @param level: Depth level to find BoM lines starts from 10.
304 @return: result: List of dictionaries containing product details.
305 result2: List of dictionaries containing Work Center details.
307 routing_obj = self.pool.get('mrp.routing')
308 factor = factor / (bom.product_efficiency or 1.0)
309 factor = rounding(factor, bom.product_rounding)
310 if factor < bom.product_rounding:
311 factor = bom.product_rounding
315 if bom.type == 'phantom' and not bom.bom_lines:
316 newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
319 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
320 result = result + res[0]
321 result2 = result2 + res[1]
326 if addthis and not bom.bom_lines:
329 'name': bom.product_id.name,
330 'product_id': bom.product_id.id,
331 'product_qty': bom.product_qty * factor,
332 'product_uom': bom.product_uom.id,
333 'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
334 'product_uos': bom.product_uos and bom.product_uos.id or False,
336 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
338 for wc_use in routing.workcenter_lines:
339 wc = wc_use.workcenter_id
340 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
341 mult = (d + (m and 1.0 or 0.0))
342 cycle = mult * wc_use.cycle_nbr
344 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
345 'workcenter_id': wc.id,
346 'sequence': level+(wc_use.sequence or 0),
348 '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)),
350 for bom2 in bom.bom_lines:
351 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
352 result = result + res[0]
353 result2 = result2 + res[1]
354 return result, result2
356 def copy_data(self, cr, uid, id, default=None, context=None):
359 bom_data = self.read(cr, uid, id, [], context=context)
360 default.update({'name': bom_data['name'] + ' ' + _('Copy'), 'bom_id':False})
361 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
363 def create(self, cr, uid, vals, context=None):
364 obj_id = super(mrp_bom, self).create(cr, uid, vals, context=context)
365 self.create_send_note(cr, uid, [obj_id], context=context)
368 def create_send_note(self, cr, uid, ids, context=None):
369 prod_obj = self.pool.get('product.product')
370 for obj in self.browse(cr, uid, ids, context=context):
371 for prod in prod_obj.browse(cr, uid, [obj.product_id], context=context):
372 self.message_append_note(cr, uid, [obj.id], body=_("Bill of Material has been <b>created</b> for <em>%s</em> product.") % (prod.id.name_template), context=context)
377 class mrp_bom_revision(osv.osv):
378 _name = 'mrp.bom.revision'
379 _description = 'Bill of Material Revision'
381 'name': fields.char('Modification name', size=64, required=True),
382 'description': fields.text('Description'),
383 'date': fields.date('Modification Date'),
384 'indice': fields.char('Revision', size=16),
385 'last_indice': fields.char('last indice', size=64),
386 'author_id': fields.many2one('res.users', 'Author'),
387 'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
391 'author_id': lambda x, y, z, c: z,
392 'date': fields.date.context_today,
401 return math.ceil(f / r) * r
403 class mrp_production(osv.osv):
405 Production Orders / Manufacturing Orders
407 _name = 'mrp.production'
408 _description = 'Manufacturing Order'
409 _date_name = 'date_planned'
410 _inherit = ['ir.needaction_mixin', 'mail.thread']
412 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
413 """ Calculates total hours and total no. of cycles for a production order.
414 @param prop: Name of field.
416 @return: Dictionary of values.
419 for prod in self.browse(cr, uid, ids, context=context):
424 for wc in prod.workcenter_lines:
425 result[prod.id]['hour_total'] += wc.hour
426 result[prod.id]['cycle_total'] += wc.cycle
429 def _production_date_end(self, cr, uid, ids, prop, unknow_none, context=None):
430 """ Finds production end date.
431 @param prop: Name of field.
433 @return: Dictionary of values.
436 for prod in self.browse(cr, uid, ids, context=context):
437 result[prod.id] = prod.date_planned
440 def _production_date(self, cr, uid, ids, prop, unknow_none, context=None):
441 """ Finds production planned date.
442 @param prop: Name of field.
444 @return: Dictionary of values.
447 for prod in self.browse(cr, uid, ids, context=context):
448 result[prod.id] = prod.date_planned[:10]
451 def _src_id_default(self, cr, uid, ids, context=None):
452 src_location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
453 return src_location_id.id
455 def _dest_id_default(self, cr, uid, ids, context=None):
456 dest_location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
457 return dest_location_id.id
460 'name': fields.char('Reference', size=64, required=True),
461 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
462 'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', select=True),
464 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft':[('readonly',False)]}),
465 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'draft':[('readonly',False)]}, readonly=True),
466 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, states={'draft':[('readonly',False)]}, readonly=True),
467 'product_uos_qty': fields.float('Product UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
468 'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
470 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
471 readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will look for components."),
472 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
473 readonly=True, states={'draft':[('readonly',False)]}, help="Location where the system will stock the finished products."),
475 'date_planned_end': fields.function(_production_date_end, type='date', string='Scheduled End Date'),
476 'date_planned_date': fields.function(_production_date, type='date', string='Scheduled Date'),
477 'date_planned': fields.datetime('Scheduled Date', required=True, select=1),
478 'date_start': fields.datetime('Start Date', select=True),
479 'date_finished': fields.datetime('End Date', select=True),
481 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]}),
482 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]}, 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."),
483 'picking_id': fields.many2one('stock.picking', 'Picking List', readonly=True, ondelete="restrict",
484 help="This is the Internal Picking List that brings the finished product to the production plan"),
485 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
486 '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)]}),
487 'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
488 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
489 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products', domain=[('state','in', ('done', 'cancel'))]),
490 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
491 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
492 'state': fields.selection([('draft','New'),('cancel','Cancelled'),('picking_except', 'Picking Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','Production Started'),('done','Done')],'Status', readonly=True,
493 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\'.\
494 \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\'.'),
495 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
496 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
497 'user_id':fields.many2one('res.users', 'Responsible'),
498 'company_id': fields.many2one('res.company','Company',required=True),
501 'priority': lambda *a: '1',
502 'state': lambda *a: 'draft',
503 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
504 'product_qty': lambda *a: 1.0,
505 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
506 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
507 'location_src_id': _src_id_default,
508 'location_dest_id': _dest_id_default
511 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
513 _order = 'priority desc, date_planned asc';
515 def _check_qty(self, cr, uid, ids, context=None):
516 for order in self.browse(cr, uid, ids, context=context):
517 if order.product_qty <= 0:
522 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
525 def create(self, cr, uid, vals, context=None):
526 obj_id = super(mrp_production, self).create(cr, uid, vals, context=context)
527 self.create_send_note(cr, uid, [obj_id], context=context)
530 def unlink(self, cr, uid, ids, context=None):
531 for production in self.browse(cr, uid, ids, context=context):
532 if production.state not in ('draft', 'cancel'):
533 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
534 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
536 def copy(self, cr, uid, id, default=None, context=None):
540 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
543 'move_created_ids' : [],
544 'move_created_ids2' : [],
545 'product_lines' : [],
548 return super(mrp_production, self).copy(cr, uid, id, default, context)
550 def location_id_change(self, cr, uid, ids, src, dest, context=None):
551 """ Changes destination location if source location is changed.
552 @param src: Source location id.
553 @param dest: Destination location id.
554 @return: Dictionary of values.
559 return {'value': {'location_dest_id': src}}
562 def product_id_change(self, cr, uid, ids, product_id, context=None):
563 """ Finds UoM of changed product.
564 @param product_id: Id of changed product.
565 @return: Dictionary of values.
569 'product_uom': False,
573 bom_obj = self.pool.get('mrp.bom')
574 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
575 bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
578 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
579 routing_id = bom_point.routing_id.id or False
581 product_uom_id = product.uom_id and product.uom_id.id or False
583 'product_uom': product_uom_id,
585 'routing_id': routing_id,
587 return {'value': result}
589 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
590 """ Finds routing for changed BoM.
591 @param product: Id of product.
592 @return: Dictionary of values.
598 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
599 routing_id = bom_point.routing_id.id or False
601 'routing_id': routing_id
603 return {'value': result}
605 def action_picking_except(self, cr, uid, ids):
606 """ Changes the state to Exception.
609 self.write(cr, uid, ids, {'state': 'picking_except'})
612 def action_compute(self, cr, uid, ids, properties=[], context=None):
613 """ Computes bills of material of a product.
614 @param properties: List containing dictionaries of properties.
615 @return: No. of products.
618 bom_obj = self.pool.get('mrp.bom')
619 uom_obj = self.pool.get('product.uom')
620 prod_line_obj = self.pool.get('mrp.production.product.line')
621 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
622 for production in self.browse(cr, uid, ids):
623 cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
624 cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
625 bom_point = production.bom_id
626 bom_id = production.bom_id.id
628 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
630 bom_point = bom_obj.browse(cr, uid, bom_id)
631 routing_id = bom_point.routing_id.id or False
632 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
635 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
636 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
637 res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
641 line['production_id'] = production.id
642 prod_line_obj.create(cr, uid, line)
643 for line in results2:
644 line['production_id'] = production.id
645 workcenter_line_obj.create(cr, uid, line)
648 def action_cancel(self, cr, uid, ids, context=None):
649 """ Cancels the production order and related stock moves.
654 move_obj = self.pool.get('stock.move')
655 for production in self.browse(cr, uid, ids, context=context):
656 if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
657 raise osv.except_osv(
658 _('Cannot cancel manufacturing order!'),
659 _('You must first cancel related internal picking attached to this manufacturing order.'))
660 if production.move_created_ids:
661 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
662 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
663 self.write(cr, uid, ids, {'state': 'cancel'})
664 self.action_cancel_send_note(cr, uid, ids, context)
667 def action_ready(self, cr, uid, ids, context=None):
668 """ Changes the production state to Ready and location id of stock move.
671 move_obj = self.pool.get('stock.move')
672 self.write(cr, uid, ids, {'state': 'ready'})
674 for (production_id,name) in self.name_get(cr, uid, ids):
675 production = self.browse(cr, uid, production_id)
676 if production.move_prod_id:
677 move_obj.write(cr, uid, [production.move_prod_id.id],
678 {'location_id': production.location_dest_id.id})
679 self.action_ready_send_note(cr, uid, [production_id], context)
682 def action_production_end(self, cr, uid, ids, context=None):
683 """ Changes production state to Finish and writes finished date.
686 for production in self.browse(cr, uid, ids):
687 self._costs_generate(cr, uid, production)
688 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
689 self.action_done_send_note(cr, uid, ids, context)
692 def test_production_done(self, cr, uid, ids):
693 """ Tests whether production is done or not.
694 @return: True or False
697 for production in self.browse(cr, uid, ids):
698 if production.move_lines:
701 if production.move_created_ids:
705 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
706 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
707 it's always equal to the quantity encoded in the production order or the production wizard, but if the
708 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
710 :param production_id: ID of the mrp.order
711 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
712 :return: The factor to apply to the quantity that we should produce for the given production order.
716 def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
717 """ To produce final product based on production mode (consume/consume&produce).
718 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
719 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
720 and stock move lines of final product will be also done/produced.
721 @param production_id: the ID of mrp.production object
722 @param production_qty: specify qty to produce
723 @param production_mode: specify production mode (consume/consume&produce).
726 stock_mov_obj = self.pool.get('stock.move')
727 production = self.browse(cr, uid, production_id, context=context)
730 for produced_product in production.move_created_ids2:
731 if (produced_product.scrapped) or (produced_product.product_id.id <> production.product_id.id):
733 produced_qty += produced_product.product_qty
734 if production_mode in ['consume','consume_produce']:
737 # Calculate already consumed qtys
738 for consumed in production.move_lines2:
739 if consumed.scrapped:
741 if not consumed_data.get(consumed.product_id.id, False):
742 consumed_data[consumed.product_id.id] = 0
743 consumed_data[consumed.product_id.id] += consumed.product_qty
745 # Find product qty to be consumed and consume it
746 for scheduled in production.product_lines:
748 # total qty of consumed product we need after this consumption
749 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
751 # qty available for consume and produce
752 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
755 # there will be nothing to consume for this raw material
758 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
760 # qtys we have to consume
761 qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
764 # if qtys we have to consume is more than qtys available to consume
765 prod_name = scheduled.product_id.name_get()[0][1]
766 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))
768 # we already have more qtys consumed than we need
771 raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
773 if production_mode == 'consume_produce':
774 # To produce remaining qty of final product
775 #vals = {'state':'confirmed'}
776 #final_product_todo = [x.id for x in production.move_created_ids]
777 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
778 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
779 produced_products = {}
780 for produced_product in production.move_created_ids2:
781 if produced_product.scrapped:
783 if not produced_products.get(produced_product.product_id.id, False):
784 produced_products[produced_product.product_id.id] = 0
785 produced_products[produced_product.product_id.id] += produced_product.product_qty
787 for produce_product in production.move_created_ids:
788 produced_qty = produced_products.get(produce_product.product_id.id, 0)
789 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
790 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
792 if rest_qty < production_qty:
793 prod_name = produce_product.product_id.name_get()[0][1]
794 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))
796 stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
798 for raw_product in production.move_lines2:
800 parent_move_ids = [x.id for x in raw_product.move_history_ids]
801 for final_product in production.move_created_ids2:
802 if final_product.id not in parent_move_ids:
803 new_parent_ids.append(final_product.id)
804 for new_parent_id in new_parent_ids:
805 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
807 wf_service = netsvc.LocalService("workflow")
808 wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
811 def _costs_generate(self, cr, uid, production):
812 """ Calculates total costs at the end of the production.
813 @param production: Id of production order.
814 @return: Calculated amount.
817 analytic_line_obj = self.pool.get('account.analytic.line')
818 for wc_line in production.workcenter_lines:
819 wc = wc_line.workcenter_id
820 if wc.costs_journal_id and wc.costs_general_account_id:
821 value = wc_line.hour * wc.costs_hour
822 account = wc.costs_hour_account_id.id
823 if value and account:
825 analytic_line_obj.create(cr, uid, {
826 'name': wc_line.name + ' (H)',
828 'account_id': account,
829 'general_account_id': wc.costs_general_account_id.id,
830 'journal_id': wc.costs_journal_id.id,
832 'product_id': wc.product_id.id,
833 'unit_amount': wc_line.hour,
834 'product_uom_id': wc.product_id.uom_id.id
836 if wc.costs_journal_id and wc.costs_general_account_id:
837 value = wc_line.cycle * wc.costs_cycle
838 account = wc.costs_cycle_account_id.id
839 if value and account:
841 analytic_line_obj.create(cr, uid, {
842 'name': wc_line.name+' (C)',
844 'account_id': account,
845 'general_account_id': wc.costs_general_account_id.id,
846 'journal_id': wc.costs_journal_id.id,
848 'product_id': wc.product_id.id,
849 'unit_amount': wc_line.cycle,
850 'product_uom_id': wc.product_id.uom_id.id
854 def action_in_production(self, cr, uid, ids, context=None):
855 """ Changes state to In Production and writes starting date.
858 self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
859 self.action_in_production_send_note(cr, uid, ids, context)
862 def test_if_product(self, cr, uid, ids):
864 @return: True or False
867 for production in self.browse(cr, uid, ids):
868 if not production.product_lines:
869 if not self.action_compute(cr, uid, [production.id]):
873 def _get_auto_picking(self, cr, uid, production):
876 def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
877 wf_service = netsvc.LocalService("workflow")
878 procurement_order = self.pool.get('procurement.order')
879 production = production_line.production_id
880 location_id = production.location_src_id.id
881 date_planned = production.date_planned
882 procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
883 procurement_id = procurement_order.create(cr, uid, {
884 'name': procurement_name,
885 'origin': procurement_name,
886 'date_planned': date_planned,
887 'product_id': production_line.product_id.id,
888 'product_qty': production_line.product_qty,
889 'product_uom': production_line.product_uom.id,
890 'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
891 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
892 'location_id': location_id,
893 'procure_method': production_line.product_id.procure_method,
894 'move_id': shipment_move_id,
895 'company_id': production.company_id.id,
897 wf_service.trg_validate(uid, procurement_order._name, procurement_id, 'button_confirm', cr)
898 return procurement_id
900 def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
901 stock_move = self.pool.get('stock.move')
902 production = production_line.production_id
903 date_planned = production.date_planned
904 # Internal shipment is created for Stockable and Consumer Products
905 if production_line.product_id.type not in ('product', 'consu'):
907 move_name = _('PROD: %s') % production.name
908 source_location_id = production.location_src_id.id
909 if not destination_location_id:
910 destination_location_id = source_location_id
911 return stock_move.create(cr, uid, {
913 'picking_id': shipment_id,
914 'product_id': production_line.product_id.id,
915 'product_qty': production_line.product_qty,
916 'product_uom': production_line.product_uom.id,
917 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
918 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
919 'date': date_planned,
920 'move_dest_id': parent_move_id,
921 'location_id': source_location_id,
922 'location_dest_id': destination_location_id,
924 'company_id': production.company_id.id,
927 def _make_production_internal_shipment(self, cr, uid, production, context=None):
928 ir_sequence = self.pool.get('ir.sequence')
929 stock_picking = self.pool.get('stock.picking')
931 pick_type = 'internal'
934 # Take routing address as a Shipment Address.
935 # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
936 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
937 routing_loc = production.bom_id.routing_id.location_id
938 if routing_loc.usage <> 'internal':
940 partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
942 # Take next Sequence number of shipment base on type
943 pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
945 picking_id = stock_picking.create(cr, uid, {
947 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
951 'partner_id': partner_id,
952 'auto_picking': self._get_auto_picking(cr, uid, production),
953 'company_id': production.company_id.id,
955 production.write({'picking_id': picking_id}, context=context)
958 def _make_production_produce_line(self, cr, uid, production, context=None):
959 stock_move = self.pool.get('stock.move')
960 source_location_id = production.product_id.product_tmpl_id.property_stock_production.id
961 destination_location_id = production.location_dest_id.id
962 move_name = _('PROD: %s') + production.name
965 'date': production.date_planned,
966 'product_id': production.product_id.id,
967 'product_qty': production.product_qty,
968 'product_uom': production.product_uom.id,
969 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
970 'product_uos': production.product_uos and production.product_uos.id or False,
971 'location_id': source_location_id,
972 'location_dest_id': destination_location_id,
973 'move_dest_id': production.move_prod_id.id,
975 'company_id': production.company_id.id,
977 move_id = stock_move.create(cr, uid, data, context=context)
978 production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
981 def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
982 stock_move = self.pool.get('stock.move')
983 production = production_line.production_id
984 # Internal shipment is created for Stockable and Consumer Products
985 if production_line.product_id.type not in ('product', 'consu'):
987 move_name = _('PROD: %s') % production.name
988 destination_location_id = production.product_id.product_tmpl_id.property_stock_production.id
989 if not source_location_id:
990 source_location_id = production.location_src_id.id
991 move_id = stock_move.create(cr, uid, {
993 'date': production.date_planned,
994 'product_id': production_line.product_id.id,
995 'product_qty': production_line.product_qty,
996 'product_uom': production_line.product_uom.id,
997 'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
998 'product_uos': production_line.product_uos and production_line.product_uos.id or False,
999 'location_id': source_location_id,
1000 'location_dest_id': destination_location_id,
1001 'move_dest_id': parent_move_id,
1003 'company_id': production.company_id.id,
1005 production.write({'move_lines': [(4, move_id)]}, context=context)
1008 def action_confirm(self, cr, uid, ids, context=None):
1009 """ Confirms production order.
1010 @return: Newly generated Shipment Id.
1013 wf_service = netsvc.LocalService("workflow")
1014 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)])
1015 self.action_compute(cr, uid, uncompute_ids, context=context)
1016 for production in self.browse(cr, uid, ids, context=context):
1017 shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
1018 produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
1020 # Take routing location as a Source Location.
1021 source_location_id = production.location_src_id.id
1022 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1023 source_location_id = production.bom_id.routing_id.location_id.id
1025 for line in production.product_lines:
1026 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1027 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1028 destination_location_id=source_location_id, context=context)
1029 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1031 wf_service.trg_validate(uid, 'stock.picking', shipment_id, 'button_confirm', cr)
1032 production.write({'state':'confirmed'}, context=context)
1033 self.action_confirm_send_note(cr, uid, [production.id], context);
1036 def force_production(self, cr, uid, ids, *args):
1037 """ Assigns products.
1038 @param *args: Arguments
1041 pick_obj = self.pool.get('stock.picking')
1042 pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1045 # ---------------------------------------------------
1046 # OpenChatter methods and notifications
1047 # ---------------------------------------------------
1049 def message_get_subscribers(self, cr, uid, ids, context=None):
1050 """ Override to add responsible user. """
1051 user_ids = super(mrp_production, self).message_get_subscribers(cr, uid, ids, context=context)
1052 for obj in self.browse(cr, uid, ids, context=context):
1053 if obj.user_id and not obj.user_id.id in user_ids:
1054 user_ids.append(obj.user_id.id)
1057 def create_send_note(self, cr, uid, ids, context=None):
1058 self.message_append_note(cr, uid, ids, body=_("Manufacturing order has been <b>created</b>."), context=context)
1061 def action_cancel_send_note(self, cr, uid, ids, context=None):
1062 message = _("Manufacturing order has been <b>canceled</b>.")
1063 self.message_append_note(cr, uid, ids, body=message, context=context)
1066 def action_ready_send_note(self, cr, uid, ids, context=None):
1067 message = _("Manufacturing order is <b>ready to produce</b>.")
1068 self.message_append_note(cr, uid, ids, body=message, context=context)
1071 def action_in_production_send_note(self, cr, uid, ids, context=None):
1072 message = _("Manufacturing order is <b>in production</b>.")
1073 self.message_append_note(cr, uid, ids, body=message, context=context)
1076 def action_done_send_note(self, cr, uid, ids, context=None):
1077 message = _("Manufacturing order has been <b>done</b>.")
1078 self.message_append_note(cr, uid, ids, body=message, context=context)
1081 def action_confirm_send_note(self, cr, uid, ids, context=None):
1082 for obj in self.browse(cr, uid, ids, context=context):
1083 # convert datetime field to a datetime, using server format, then
1084 # convert it to the user TZ and re-render it with %Z to add the timezone
1085 obj_datetime = fields.DT.datetime.strptime(obj.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
1086 obj_date_str = fields.datetime.context_timestamp(cr, uid, obj_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
1087 message = _("Manufacturing order has been <b>confirmed</b> and is <b>scheduled</b> for the <em>%s</em>.") % (obj_date_str)
1088 self.message_append_note(cr, uid, [obj.id], body=message, context=context)
1094 class mrp_production_workcenter_line(osv.osv):
1095 _name = 'mrp.production.workcenter.line'
1096 _description = 'Work Order'
1098 _inherit = ['mail.thread']
1101 'name': fields.char('Work Order', size=64, required=True),
1102 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1103 'cycle': fields.float('Number of Cycles', digits=(16,2)),
1104 'hour': fields.float('Number of Hours', digits=(16,2)),
1105 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1106 'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
1109 'sequence': lambda *a: 1,
1110 'hour': lambda *a: 0,
1111 'cycle': lambda *a: 0,
1113 mrp_production_workcenter_line()
1115 class mrp_production_product_line(osv.osv):
1116 _name = 'mrp.production.product.line'
1117 _description = 'Production Scheduled Product'
1119 'name': fields.char('Name', size=64, required=True),
1120 'product_id': fields.many2one('product.product', 'Product', required=True),
1121 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1122 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1123 'product_uos_qty': fields.float('Product UOS Quantity'),
1124 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1125 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1127 mrp_production_product_line()
1129 class product_product(osv.osv):
1130 _inherit = "product.product"
1132 'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1135 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: