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 import openerp.addons.decimal_precision as dp
24 from openerp.osv import fields, osv, orm
25 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
26 from openerp.tools import float_compare
27 from openerp.tools.translate import _
28 from openerp import tools, SUPERUSER_ID
29 from openerp.addons.product import _common
32 class mrp_property_group(osv.osv):
34 Group of mrp properties.
36 _name = 'mrp.property.group'
37 _description = 'Property Group'
39 'name': fields.char('Property Group', size=64, required=True),
40 'description': fields.text('Description'),
43 class mrp_property(osv.osv):
47 _name = 'mrp.property'
48 _description = 'Property'
50 'name': fields.char('Name', size=64, required=True),
51 'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
52 'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
53 'description': fields.text('Description'),
56 'composition': lambda *a: 'min',
58 #----------------------------------------------------------
60 #----------------------------------------------------------
61 # capacity_hour : capacity per hour. default: 1.0.
62 # Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
63 # unit_per_cycle : how many units are produced for one cycle
65 class mrp_workcenter(osv.osv):
66 _name = 'mrp.workcenter'
67 _description = 'Work Center'
68 _inherits = {'resource.resource':"resource_id"}
70 'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
71 '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."),
72 'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
73 'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
74 'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
75 'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
76 'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
77 help="Fill this only if you want automatic analytic accounting entries on production orders."),
78 'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
79 'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
80 help="Fill this only if you want automatic analytic accounting entries on production orders."),
81 'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
82 'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
83 'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
84 'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
87 'capacity_per_cycle': 1.0,
88 'resource_type': 'material',
91 def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
95 cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
96 value = {'costs_hour': cost.standard_price}
97 return {'value': value}
99 class mrp_routing(osv.osv):
101 For specifying the routings of Work Centers.
103 _name = 'mrp.routing'
104 _description = 'Routing'
106 'name': fields.char('Name', size=64, required=True),
107 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
108 'code': fields.char('Code', size=8),
110 'note': fields.text('Description'),
111 'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
113 'location_id': fields.many2one('stock.location', 'Production Location',
114 help="Keep empty if you produce at the location where the finished products are needed." \
115 "Set a location if you produce at a fixed location. This can be a partner location " \
116 "if you subcontract the manufacturing operations."
118 'company_id': fields.many2one('res.company', 'Company'),
121 'active': lambda *a: 1,
122 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
125 class mrp_routing_workcenter(osv.osv):
127 Defines working cycles and hours of a Work Center using routings.
129 _name = 'mrp.routing.workcenter'
130 _description = 'Work Center Usage'
133 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
134 'name': fields.char('Name', size=64, required=True),
135 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
136 'cycle_nbr': fields.float('Number of Cycles', required=True,
137 help="Number of iterations this work center has to do in the specified operation of the routing."),
138 '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."),
139 'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
140 help="Routing indicates all the Work Centers used, for how long and/or cycles." \
141 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
142 'note': fields.text('Description'),
143 'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
146 'cycle_nbr': lambda *a: 1.0,
147 'hour_nbr': lambda *a: 0.0,
150 class mrp_bom(osv.osv):
152 Defines bills of material for a product.
155 _description = 'Bill of Material'
156 _inherit = ['mail.thread']
158 def _child_compute(self, cr, uid, ids, name, arg, context=None):
160 @param self: The object pointer
161 @param cr: The current row, from the database cursor,
162 @param uid: The current user ID for security checks
163 @param ids: List of selected IDs
164 @param name: Name of the field
165 @param arg: User defined argument
166 @param context: A standard dictionary for contextual values
167 @return: Dictionary of values
172 bom_obj = self.pool.get('mrp.bom')
173 bom_id = context and context.get('active_id', False) or False
174 cr.execute('select id from mrp_bom')
175 if all(bom_id != r[0] for r in cr.fetchall()):
178 bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
179 for bom in self.browse(cr, uid, ids, context=context):
180 if (bom_parent) or (bom.id == bom_id):
181 result[bom.id] = map(lambda x: x.id, bom.bom_line_ids)
186 ok = ((name=='child_complete_ids'))
187 if (bom.type=='phantom' or ok):
188 sids = bom_obj.search(cr, uid, [('product_tmpl_id','=',bom.product_tmpl_id.id)])
190 bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
191 result[bom.id] += map(lambda x: x.id, bom2.bom_line_ids)
195 'name': fields.char('Name', size=64),
196 'code': fields.char('Reference', size=16),
197 '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."),
198 'type': fields.selection([('normal', 'Normal'), ('phantom', 'Set')], 'BoM Type', required=True,
199 help= "Set: When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product."),
200 'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
201 'product_tmpl_id': fields.many2one('product.template', 'Product', required=True),
202 'product_id': fields.many2one('product.product', 'Product Variant',
203 domain="[('product_tmpl_id','=',product_tmpl_id)]",
204 help="If a product variant is defined the BOM is available only for this product."),
205 'bom_line_ids': fields.one2many('mrp.bom.line', 'bom_id', 'BoM Lines'),
207 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
208 '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"),
209 'date_start': fields.date('Valid From', help="Validity of this BoM. Keep empty if it's always valid."),
210 'date_stop': fields.date('Valid Until', help="Validity of this BoM. Keep empty if it's always valid."),
211 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
212 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. "\
213 "The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
214 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
215 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% during the production process."),
216 'property_ids': fields.many2many('mrp.property', string='Properties'),
217 'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
218 'company_id': fields.many2one('res.company', 'Company', required=True),
221 def _get_uom_id(self, cr, uid, *args):
222 return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
224 'active': lambda *a: 1,
225 'product_qty': lambda *a: 1.0,
226 'product_efficiency': lambda *a: 1.0,
227 'product_rounding': lambda *a: 0.0,
228 'type': lambda *a: 'normal',
229 'product_uom': _get_uom_id,
230 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
234 def _bom_find(self, cr, uid, product_uom, product_tmpl_id=None, product_id=None, properties=None):
235 """ Finds BoM for particular product and product uom.
236 @param product_tmpl_id: Selected product.
237 @param product_uom: Unit of measure of a product.
238 @param properties: List of related properties.
239 @return: False or BoM id.
241 if properties is None:
245 domain = ['|',('product_id', '=', product_id),('product_tmpl_id.product_variant_ids', '=', product_id)]
247 domain = [('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl_id)]
249 domain += [('product_uom','=',product_uom)]
250 domain = domain + [ '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
251 '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
252 ids = self.search(cr, uid, domain)
253 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
254 if not set(map(int,bom.property_ids or [])) - set(properties or []):
258 def _bom_explode(self, cr, uid, bom, product, factor, properties=None, level=0, routing_id=False, previous_products=None, master_bom=None):
259 """ Finds Products and Work Centers for related BoM for manufacturing order.
260 @param bom: BoM of particular product template.
261 @param product: Select a particular variant of the BoM. If False use BoM without variants.
262 @param factor: Factor of product UoM.
263 @param properties: A List of properties Ids.
264 @param level: Depth level to find BoM lines starts from 10.
265 @param previous_products: List of product previously use by bom explore to avoid recursion
266 @param master_bom: When recursion, used to display the name of the master bom
267 @return: result: List of dictionaries containing product details.
268 result2: List of dictionaries containing Work Center details.
270 routing_obj = self.pool.get('mrp.routing')
271 all_prod = [] + (previous_products or [])
272 master_bom = master_bom or bom
274 def _factor(factor, product_efficiency, product_rounding):
275 factor = factor / (product_efficiency or 1.0)
276 factor = _common.ceiling(factor, product_rounding)
277 if factor < product_rounding:
278 factor = product_rounding
281 factor = _factor(factor, bom.product_efficiency, bom.product_rounding)
286 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
288 for wc_use in routing.workcenter_lines:
289 wc = wc_use.workcenter_id
290 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
291 mult = (d + (m and 1.0 or 0.0))
292 cycle = mult * wc_use.cycle_nbr
294 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_tmpl_id.name_get()[0][1]),
295 'workcenter_id': wc.id,
296 'sequence': level + (wc_use.sequence or 0),
298 '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)),
301 for bom_line_id in bom.bom_line_ids:
302 if bom_line_id.date_start and bom_line_id.date_start > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) or \
303 bom_line_id.date_stop and bom_line_id.date_stop > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT):
306 if set(map(int,bom_line_id.property_ids or [])) - set(properties or []):
308 # all bom_line_id variant values must be in the product
309 if bom_line_id.attribute_value_ids:
310 if not product or (set(map(int,bom_line_id.attribute_value_ids or [])) - set(map(int,product.attribute_value_ids))):
313 if bom_line_id.product_id.id in all_prod:
314 raise osv.except_osv(_('Invalid Action!'), _('BoM "%s" contains a BoM line with a product recursion: "%s".') % (master_bom.name,bom_line_id.product_id.name_get()[0][1]))
315 all_prod.append(bom_line_id.product_id.id)
317 if bom_line_id.type != "phantom":
319 'name': bom_line_id.product_id.name,
320 'product_id': bom_line_id.product_id.id,
321 'product_qty': _factor(bom_line_id.product_qty * factor, bom_line_id.product_efficiency, bom_line_id.product_rounding),
322 'product_uom': bom_line_id.product_uom.id,
323 'product_uos_qty': bom_line_id.product_uos and bom_line_id.product_uos_qty * factor or False,
324 'product_uos': bom_line_id.product_uos and bom_line_id.product_uos.id or False,
327 bom_id = self._bom_find(cr, uid, bom_line_id.product_uom.id, product_id=bom_line_id.product_id.id, properties=properties)
329 bom2 = self.browse(cr, uid, bom_id)
330 res = self._bom_explode(cr, uid, bom2, bom_line_id.product_id, factor,
331 properties=properties, level=level + 10, previous_products=all_prod, master_bom=master_bom)
332 result = result + res[0]
333 result2 = result2 + res[1]
335 raise osv.except_osv(_('Invalid Action!'), _('BoM "%s" contains a phantom BoM line but the product "%s" don\'t have any BoM defined.') % (master_bom.name,bom_line_id.product_id.name_get()[0][1]))
337 return result, result2
339 def copy_data(self, cr, uid, id, default=None, context=None):
342 bom_data = self.read(cr, uid, id, [], context=context)
343 default.update(name=_("%s (copy)") % (bom_data['name']))
344 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
346 def onchange_uom(self, cr, uid, ids, product_tmpl_id, product_uom, context=None):
348 if not product_uom or not product_tmpl_id:
350 product = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
351 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
352 if uom.category_id.id != product.uom_id.category_id.id:
353 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
354 res['value'].update({'product_uom': product.uom_id.id})
357 def onchange_product_tmpl_id(self, cr, uid, ids, product_tmpl_id, product_qty=0, context=None):
358 """ Changes UoM and name if product_id changes.
359 @param product_id: Changed product_id
360 @return: Dictionary of changed values
364 prod = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
367 'product_uom': prod.uom_id.id,
371 class mrp_bom_line(osv.osv):
372 _name = 'mrp.bom.line'
376 'type': fields.selection([('normal', 'Normal'), ('phantom', 'Phantom')], 'BoM Line Type', required=True,
377 help="Phantom: this product line will not appear in the raw materials of manufacturing orders,"
378 "it will be directly replaced by the raw materials of its own BoM, without triggering"
379 "an extra manufacturing order."),
380 'product_id': fields.many2one('product.product', 'Product', required=True),
381 'product_uos_qty': fields.float('Product UOS Qty'),
382 '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."),
383 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
384 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True,
385 help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"),
387 'date_start': fields.date('Valid From', help="Validity of component. Keep empty if it's always valid."),
388 'date_stop': fields.date('Valid Until', help="Validity of component. Keep empty if it's always valid."),
389 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying."),
390 '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."),
391 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
392 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
393 'property_ids': fields.many2many('mrp.property', string='Properties'),
395 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True, required=True),
396 'attribute_value_ids': fields.many2many('product.attribute.value', string='Variants', help="BOM Product Variants needed form apply this line."),
399 def _get_uom_id(self, cr, uid, *args):
400 return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
402 'product_qty': lambda *a: 1.0,
403 'product_efficiency': lambda *a: 1.0,
404 'product_rounding': lambda *a: 0.0,
405 'type': lambda *a: 'normal',
406 'product_uom': _get_uom_id,
409 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
410 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
413 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
415 if not product_uom or not product_id:
417 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
418 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
419 if uom.category_id.id != product.uom_id.category_id.id:
420 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
421 res['value'].update({'product_uom': product.uom_id.id})
424 def onchange_product_id(self, cr, uid, ids, product_id, product_qty=0, context=None):
425 """ Changes UoM if product_id changes.
426 @param product_id: Changed product_id
427 @return: Dictionary of changed values
431 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
433 'product_uom': prod.uom_id.id,
434 'product_uos_qty': 0,
438 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
439 res['value']['product_uos'] = prod.uos_id.id
442 class mrp_production(osv.osv):
444 Production Orders / Manufacturing Orders
446 _name = 'mrp.production'
447 _description = 'Manufacturing Order'
448 _date_name = 'date_planned'
449 _inherit = ['mail.thread', 'ir.needaction_mixin']
451 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
452 """ Calculates total hours and total no. of cycles for a production order.
453 @param prop: Name of field.
455 @return: Dictionary of values.
458 for prod in self.browse(cr, uid, ids, context=context):
463 for wc in prod.workcenter_lines:
464 result[prod.id]['hour_total'] += wc.hour
465 result[prod.id]['cycle_total'] += wc.cycle
468 def _src_id_default(self, cr, uid, ids, context=None):
470 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
471 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
472 except (orm.except_orm, ValueError):
476 def _dest_id_default(self, cr, uid, ids, context=None):
478 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
479 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
480 except (orm.except_orm, ValueError):
484 def _get_progress(self, cr, uid, ids, name, arg, context=None):
485 """ Return product quantity percentage """
486 result = dict.fromkeys(ids, 100)
487 for mrp_production in self.browse(cr, uid, ids, context=context):
488 if mrp_production.product_qty:
490 for move in mrp_production.move_created_ids2:
491 if not move.scrapped and move.product_id == mrp_production.product_id:
492 done += move.product_qty
493 result[mrp_production.id] = done / mrp_production.product_qty * 100
496 def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
497 """ Test whether all the consume lines are assigned """
499 for production in self.browse(cr, uid, ids, context=context):
500 res[production.id] = True
501 states = [x.state != 'assigned' for x in production.move_lines if x]
502 if any(states) or len(states) == 0:
503 res[production.id] = False
506 def _mrp_from_move(self, cr, uid, ids, context=None):
509 for move in self.browse(cr, uid, ids, context=context):
510 res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
514 'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
515 'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
516 help="Reference of the document that generated this production order request."),
517 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
518 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
520 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
521 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
522 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
523 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
524 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
525 'progress': fields.function(_get_progress, type='float',
526 string='Production progress'),
528 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
529 readonly=True, states={'draft': [('readonly', False)]},
530 help="Location where the system will look for components."),
531 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
532 readonly=True, states={'draft': [('readonly', False)]},
533 help="Location where the system will stock the finished products."),
534 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}),
535 'date_start': fields.datetime('Start Date', select=True, readonly=True),
536 'date_finished': fields.datetime('End Date', select=True, readonly=True),
537 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', readonly=True, states={'draft': [('readonly', False)]},
538 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
539 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
540 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."),
541 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
542 'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
543 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
544 'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
545 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
546 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
547 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
548 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
549 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
550 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
552 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
553 readonly=True, states={'draft': [('readonly', False)]}),
554 'state': fields.selection(
555 [('draft', 'New'), ('cancel', 'Cancelled'), ('confirmed', 'Awaiting Raw Materials'),
556 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
557 string='Status', readonly=True,
558 track_visibility='onchange',
559 help="When the production order is created the status is set to 'Draft'.\n\
560 If the order is confirmed the status is set to 'Waiting Goods'.\n\
561 If any exceptions are there, the status is set to 'Picking Exception'.\n\
562 If the stock is available then the status is set to 'Ready to Produce'.\n\
563 When the production gets started then the status is set to 'In Production'.\n\
564 When the production is over, the status is set to 'Done'."),
565 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
566 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
567 'user_id': fields.many2one('res.users', 'Responsible'),
568 'company_id': fields.many2one('res.company', 'Company', required=True),
569 'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
573 'priority': lambda *a: '1',
574 'state': lambda *a: 'draft',
575 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
576 'product_qty': lambda *a: 1.0,
577 'user_id': lambda self, cr, uid, c: uid,
578 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
579 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
580 'location_src_id': _src_id_default,
581 'location_dest_id': _dest_id_default
585 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
588 _order = 'priority desc, date_planned asc'
590 def _check_qty(self, cr, uid, ids, context=None):
591 for order in self.browse(cr, uid, ids, context=context):
592 if order.product_qty <= 0:
597 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
600 def unlink(self, cr, uid, ids, context=None):
601 for production in self.browse(cr, uid, ids, context=context):
602 if production.state not in ('draft', 'cancel'):
603 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
604 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
606 def copy(self, cr, uid, id, default=None, context=None):
610 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
613 'move_created_ids': [],
614 'move_created_ids2': [],
616 'move_prod_id': False,
618 return super(mrp_production, self).copy(cr, uid, id, default, context)
620 def location_id_change(self, cr, uid, ids, src, dest, context=None):
621 """ Changes destination location if source location is changed.
622 @param src: Source location id.
623 @param dest: Destination location id.
624 @return: Dictionary of values.
629 return {'value': {'location_dest_id': src}}
632 def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
633 """ Finds UoM of changed product.
634 @param product_id: Id of changed product.
635 @return: Dictionary of values.
640 'product_uom': False,
643 'product_uos_qty': 0,
646 bom_obj = self.pool.get('mrp.bom')
647 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
648 bom_id = bom_obj._bom_find(cr, uid, product.uom_id and product.uom_id.id, product_id=product.id, properties=[])
651 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
652 routing_id = bom_point.routing_id.id or False
653 product_uom_id = product.uom_id and product.uom_id.id or False
654 result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
655 if product.uos_id.id:
656 result['value']['product_uos_qty'] = product_qty * product.uos_coeff
657 result['value']['product_uos'] = product.uos_id.id
660 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
661 """ Finds routing for changed BoM.
662 @param product: Id of product.
663 @return: Dictionary of values.
669 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
670 routing_id = bom_point.routing_id.id or False
672 'routing_id': routing_id
674 return {'value': result}
677 def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
678 """ Compute product_lines and workcenter_lines from BoM structure
679 @return: product_lines
681 if properties is None:
684 bom_obj = self.pool.get('mrp.bom')
685 uom_obj = self.pool.get('product.uom')
686 prod_line_obj = self.pool.get('mrp.production.product.line')
687 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
688 for production in self.browse(cr, uid, ids, context=context):
689 #unlink product_lines
690 prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
691 #unlink workcenter_lines
692 workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
693 # search BoM structure and route
694 bom_point = production.bom_id
695 bom_id = production.bom_id.id
697 bom_id = bom_obj._bom_find(cr, uid, production.product_uom.id, product_id=production.product_id.id, properties=properties)
699 bom_point = bom_obj.browse(cr, uid, bom_id)
700 routing_id = bom_point.routing_id.id or False
701 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
704 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
706 # get components and workcenter_lines from BoM structure
707 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
708 # product_lines, workcenter_lines
709 results, results2 = bom_obj._bom_explode(cr, uid, bom_point, production.product_id, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
710 # reset product_lines in production order
712 line['production_id'] = production.id
713 prod_line_obj.create(cr, uid, line)
715 #reset workcenter_lines in production order
716 for line in results2:
717 line['production_id'] = production.id
718 workcenter_line_obj.create(cr, uid, line)
721 def action_compute(self, cr, uid, ids, properties=None, context=None):
722 """ Computes bills of material of a product.
723 @param properties: List containing dictionaries of properties.
724 @return: No. of products.
726 return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
728 def action_cancel(self, cr, uid, ids, context=None):
729 """ Cancels the production order and related stock moves.
734 move_obj = self.pool.get('stock.move')
735 for production in self.browse(cr, uid, ids, context=context):
736 if production.move_created_ids:
737 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
738 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
739 self.write(cr, uid, ids, {'state': 'cancel'})
740 # Put related procurements in exception
741 proc_obj = self.pool.get("procurement.order")
742 procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
744 proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
747 def action_ready(self, cr, uid, ids, context=None):
748 """ Changes the production state to Ready and location id of stock move.
751 move_obj = self.pool.get('stock.move')
752 self.write(cr, uid, ids, {'state': 'ready'})
754 for production in self.browse(cr, uid, ids, context=context):
755 if not production.move_created_ids:
756 self._make_production_produce_line(cr, uid, production, context=context)
758 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
759 move_obj.write(cr, uid, [production.move_prod_id.id],
760 {'location_id': production.location_dest_id.id})
763 def action_production_end(self, cr, uid, ids, context=None):
764 """ Changes production state to Finish and writes finished date.
767 for production in self.browse(cr, uid, ids):
768 self._costs_generate(cr, uid, production)
769 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
770 # Check related procurements
771 proc_obj = self.pool.get("procurement.order")
772 procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
773 proc_obj.check(cr, uid, procs, context=context)
776 def test_production_done(self, cr, uid, ids):
777 """ Tests whether production is done or not.
778 @return: True or False
781 for production in self.browse(cr, uid, ids):
782 if production.move_lines:
785 if production.move_created_ids:
789 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
790 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
791 it's always equal to the quantity encoded in the production order or the production wizard, but if the
792 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
794 :param production_id: ID of the mrp.order
795 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
796 :return: The factor to apply to the quantity that we should produce for the given production order.
800 def _get_produced_qty(self, cr, uid, production, context=None):
801 ''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
804 for produced_product in production.move_created_ids2:
805 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
807 produced_qty += produced_product.product_qty
810 def _get_consumed_data(self, cr, uid, production, context=None):
811 ''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
814 # Calculate already consumed qtys
815 for consumed in production.move_lines2:
816 if consumed.scrapped:
818 if not consumed_data.get(consumed.product_id.id, False):
819 consumed_data[consumed.product_id.id] = 0
820 consumed_data[consumed.product_id.id] += consumed.product_qty
823 def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
825 Calculates the quantity still needed to produce an extra number of products
827 quant_obj = self.pool.get("stock.quant")
828 produced_qty = self._get_produced_qty(cr, uid, production, context=context)
829 consumed_data = self._get_consumed_data(cr, uid, production, context=context)
831 #In case no product_qty is given, take the remaining qty to produce for the given production
833 product_qty = production.product_qty - produced_qty
836 # Find product qty to be consumed and consume it
837 for scheduled in production.product_lines:
838 product_id = scheduled.product_id.id
840 consumed_qty = consumed_data.get(product_id, 0.0)
842 # qty available for consume and produce
843 qty_avail = scheduled.product_qty - consumed_qty
845 # there will be nothing to consume for this raw material
848 if not dicts.get(product_id):
849 dicts[product_id] = {}
851 # total qty of consumed product we need after this consumption
852 total_consume = ((product_qty + produced_qty) * scheduled.product_qty / production.product_qty)
853 qty = total_consume - consumed_qty
855 # Search for quants related to this related move
856 for move in production.move_lines:
859 if move.product_id.id != product_id:
862 q = min(move.product_qty, qty)
863 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, scheduled.product_id, q, domain=[('qty', '>', 0.0)],
864 prefered_domain_list=[[('reservation_id', '=', move.id)], [('reservation_id', '=', False)]], context=context)
865 for quant, quant_qty in quants:
867 lot_id = quant.lot_id.id
868 if not product_id in dicts.keys():
869 dicts[product_id] = {lot_id: quant_qty}
870 elif lot_id in dicts[product_id].keys():
871 dicts[product_id][lot_id] += quant_qty
873 dicts[product_id][lot_id] = quant_qty
876 if dicts[product_id].get(False):
877 dicts[product_id][False] += qty
879 dicts[product_id][False] = qty
882 for prod in dicts.keys():
883 for lot, qty in dicts[prod].items():
884 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
887 def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
888 """ To produce final product based on production mode (consume/consume&produce).
889 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
890 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
891 and stock move lines of final product will be also done/produced.
892 @param production_id: the ID of mrp.production object
893 @param production_qty: specify qty to produce
894 @param production_mode: specify production mode (consume/consume&produce).
895 @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
898 stock_mov_obj = self.pool.get('stock.move')
899 production = self.browse(cr, uid, production_id, context=context)
900 if not production.move_lines and production.state == 'ready':
901 # trigger workflow if not products to consume (eg: services)
902 self.signal_button_produce(cr, uid, [production_id])
904 produced_qty = self._get_produced_qty(cr, uid, production, context=context)
906 main_production_move = False
907 if production_mode == 'consume_produce':
908 # To produce remaining qty of final product
909 #vals = {'state':'confirmed'}
910 #final_product_todo = [x.id for x in production.move_created_ids]
911 #stock_mov_obj.write(cr, uid, final_product_todo, vals)
912 #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
913 produced_products = {}
914 for produced_product in production.move_created_ids2:
915 if produced_product.scrapped:
917 if not produced_products.get(produced_product.product_id.id, False):
918 produced_products[produced_product.product_id.id] = 0
919 produced_products[produced_product.product_id.id] += produced_product.product_qty
921 for produce_product in production.move_created_ids:
922 produced_qty = produced_products.get(produce_product.product_id.id, 0)
923 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
924 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
925 if float_compare(rest_qty, (subproduct_factor * production_qty), precision_rounding=produce_product.product_id.uom_id.rounding) < 0:
926 prod_name = produce_product.product_id.name_get()[0][1]
927 raise osv.except_osv(_('Warning!'), _('You are going to produce total %s quantities of "%s".\nBut you can only produce up to total %s quantities.') % ((subproduct_factor * production_qty), prod_name, rest_qty))
928 if float_compare(rest_qty, 0, precision_rounding=produce_product.product_id.uom_id.rounding) > 0:
931 lot_id = wiz.lot_id.id
932 new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
933 stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
934 if produce_product.product_id.id == production.product_id.id and new_moves:
935 main_production_move = new_moves[0]
937 if production_mode in ['consume', 'consume_produce']:
940 for cons in wiz.consume_lines:
941 consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
943 consume_lines = self._calculate_qty(cr, uid, production, production_qty, context=context)
944 for consume in consume_lines:
945 remaining_qty = consume['product_qty']
946 for raw_material_line in production.move_lines:
947 if remaining_qty <= 0:
949 if consume['product_id'] != raw_material_line.product_id.id:
951 consumed_qty = min(remaining_qty, raw_material_line.product_qty)
952 stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id, restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
953 remaining_qty -= consumed_qty
955 #consumed more in wizard than previously planned
956 product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
957 extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
959 stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
961 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
962 self.signal_button_produce_done(cr, uid, [production_id])
965 def _costs_generate(self, cr, uid, production):
966 """ Calculates total costs at the end of the production.
967 @param production: Id of production order.
968 @return: Calculated amount.
971 analytic_line_obj = self.pool.get('account.analytic.line')
972 for wc_line in production.workcenter_lines:
973 wc = wc_line.workcenter_id
974 if wc.costs_journal_id and wc.costs_general_account_id:
976 value = wc_line.hour * wc.costs_hour
977 account = wc.costs_hour_account_id.id
978 if value and account:
980 # we user SUPERUSER_ID as we do not garantee an mrp user
981 # has access to account analytic lines but still should be
982 # able to produce orders
983 analytic_line_obj.create(cr, SUPERUSER_ID, {
984 'name': wc_line.name + ' (H)',
986 'account_id': account,
987 'general_account_id': wc.costs_general_account_id.id,
988 'journal_id': wc.costs_journal_id.id,
990 'product_id': wc.product_id.id,
991 'unit_amount': wc_line.hour,
992 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
995 value = wc_line.cycle * wc.costs_cycle
996 account = wc.costs_cycle_account_id.id
997 if value and account:
999 analytic_line_obj.create(cr, SUPERUSER_ID, {
1000 'name': wc_line.name + ' (C)',
1002 'account_id': account,
1003 'general_account_id': wc.costs_general_account_id.id,
1004 'journal_id': wc.costs_journal_id.id,
1006 'product_id': wc.product_id.id,
1007 'unit_amount': wc_line.cycle,
1008 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
1012 def action_in_production(self, cr, uid, ids, context=None):
1013 """ Changes state to In Production and writes starting date.
1016 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
1018 def consume_lines_get(self, cr, uid, ids, *args):
1020 for order in self.browse(cr, uid, ids, context={}):
1021 res += [x.id for x in order.move_lines]
1024 def test_ready(self, cr, uid, ids):
1026 for production in self.browse(cr, uid, ids):
1027 if production.ready_production:
1032 def _make_production_produce_line(self, cr, uid, production, context=None):
1033 stock_move = self.pool.get('stock.move')
1034 source_location_id = production.product_id.property_stock_production.id
1035 destination_location_id = production.location_dest_id.id
1037 'name': production.name,
1038 'date': production.date_planned,
1039 'product_id': production.product_id.id,
1040 'product_uom': production.product_uom.id,
1041 'product_uom_qty': production.product_qty,
1042 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1043 'product_uos': production.product_uos and production.product_uos.id or False,
1044 'location_id': source_location_id,
1045 'location_dest_id': destination_location_id,
1046 'move_dest_id': production.move_prod_id.id,
1047 'company_id': production.company_id.id,
1048 'production_id': production.id,
1049 'origin': production.name,
1051 move_id = stock_move.create(cr, uid, data, context=context)
1052 #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1053 #is 1 element long, so we can take the first.
1054 return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1056 def _get_raw_material_procure_method(self, cr, uid, product, context=None):
1057 '''This method returns the procure_method to use when creating the stock move for the production raw materials'''
1059 mto_route = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
1061 return "make_to_stock"
1062 routes = product.route_ids + product.categ_id.total_route_ids
1063 if mto_route in [x.id for x in routes]:
1064 return "make_to_order"
1065 return "make_to_stock"
1067 def _make_consume_line_from_data(self, cr, uid, production, product, uom_id, qty, uos_id, uos_qty, context=None):
1068 stock_move = self.pool.get('stock.move')
1069 # Internal shipment is created for Stockable and Consumer Products
1070 if product.type not in ('product', 'consu'):
1072 # Take routing location as a Source Location.
1073 source_location_id = production.location_src_id.id
1074 if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1075 source_location_id = production.bom_id.routing_id.location_id.id
1077 destination_location_id = production.product_id.property_stock_production.id
1078 if not source_location_id:
1079 source_location_id = production.location_src_id.id
1080 move_id = stock_move.create(cr, uid, {
1081 'name': production.name,
1082 'date': production.date_planned,
1083 'product_id': product.id,
1084 'product_uom_qty': qty,
1085 'product_uom': uom_id,
1086 'product_uos_qty': uos_id and uos_qty or False,
1087 'product_uos': uos_id or False,
1088 'location_id': source_location_id,
1089 'location_dest_id': destination_location_id,
1090 'company_id': production.company_id.id,
1091 'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context),
1092 'raw_material_production_id': production.id,
1093 #this saves us a browse in create()
1094 'price_unit': product.standard_price,
1095 'origin': production.name,
1099 def _make_production_consume_line(self, cr, uid, line, context=None):
1100 return self._make_consume_line_from_data(cr, uid, line.production_id, line.product_id, line.product_uom.id, line.product_qty, line.product_uos.id, line.product_uos_qty, context=context)
1102 def action_confirm(self, cr, uid, ids, context=None):
1103 """ Confirms production order.
1104 @return: Newly generated Shipment Id.
1106 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)])
1107 self.action_compute(cr, uid, uncompute_ids, context=context)
1108 for production in self.browse(cr, uid, ids, context=context):
1109 self._make_production_produce_line(cr, uid, production, context=context)
1112 for line in production.product_lines:
1113 stock_move_id = self._make_production_consume_line(cr, uid, line, context=context)
1115 stock_moves.append(stock_move_id)
1117 self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
1118 production.write({'state': 'confirmed'}, context=context)
1121 def action_assign(self, cr, uid, ids, context=None):
1123 Checks the availability on the consume lines of the production order
1125 move_obj = self.pool.get("stock.move")
1126 for production in self.browse(cr, uid, ids, context=context):
1127 move_obj.action_assign(cr, uid, [x.id for x in production.move_lines], context=context)
1130 def force_production(self, cr, uid, ids, *args):
1131 """ Assigns products.
1132 @param *args: Arguments
1135 move_obj = self.pool.get('stock.move')
1136 for order in self.browse(cr, uid, ids):
1137 move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1141 class mrp_production_workcenter_line(osv.osv):
1142 _name = 'mrp.production.workcenter.line'
1143 _description = 'Work Order'
1145 _inherit = ['mail.thread']
1148 'name': fields.char('Work Order', size=64, required=True),
1149 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1150 'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1151 'hour': fields.float('Number of Hours', digits=(16, 2)),
1152 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1153 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1154 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1157 'sequence': lambda *a: 1,
1158 'hour': lambda *a: 0,
1159 'cycle': lambda *a: 0,
1162 class mrp_production_product_line(osv.osv):
1163 _name = 'mrp.production.product.line'
1164 _description = 'Production Scheduled Product'
1166 'name': fields.char('Name', size=64, required=True),
1167 'product_id': fields.many2one('product.product', 'Product', required=True),
1168 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1169 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1170 'product_uos_qty': fields.float('Product UOS Quantity'),
1171 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1172 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1175 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: