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', 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', 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', 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', copy=True),
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', 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']
159 'name': fields.char('Name'),
160 'code': fields.char('Reference', size=16),
161 '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."),
162 'type': fields.selection([('normal','Manufacture this product as a normal bill of material'),('phantom','Sell and ship this product as a set of components(phantom)')], 'BoM Type', required=True,
163 help= "Set: When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product."),
164 'position': fields.char('Internal Reference', help="Reference to a position in an external plan."),
165 'product_tmpl_id': fields.many2one('product.template', 'Product', domain="[('type', '!=', 'service')]", required=True),
166 'product_id': fields.many2one('product.product', 'Product Variant',
167 domain="['&', ('product_tmpl_id','=',product_tmpl_id), ('type','!=', 'service')]",
168 help="If a product variant is defined the BOM is available only for this product."),
169 'bom_line_ids': fields.one2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True),
170 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
171 '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"),
172 'date_start': fields.date('Valid From', help="Validity of this BoM. Keep empty if it's always valid."),
173 'date_stop': fields.date('Valid Until', help="Validity of this BoM. Keep empty if it's always valid."),
174 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
175 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. "\
176 "The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
177 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
178 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% during the production process."),
179 'property_ids': fields.many2many('mrp.property', string='Properties'),
180 'company_id': fields.many2one('res.company', 'Company', required=True),
183 def _get_uom_id(self, cr, uid, *args):
184 return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
186 'active': lambda *a: 1,
187 'product_qty': lambda *a: 1.0,
188 'product_efficiency': lambda *a: 1.0,
189 'product_rounding': lambda *a: 0.0,
190 'type': lambda *a: 'normal',
191 'product_uom': _get_uom_id,
192 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
196 def _bom_find(self, cr, uid, product_tmpl_id=None, product_id=None, properties=None, context=None):
197 """ Finds BoM for particular product and product uom.
198 @param product_tmpl_id: Selected product.
199 @param product_uom: Unit of measure of a product.
200 @param properties: List of related properties.
201 @return: False or BoM id.
203 if properties is None:
206 if not product_tmpl_id:
207 product_tmpl_id = self.pool['product.product'].browse(cr, uid, product_id, context=context).product_tmpl_id.id
210 ('product_id', '=', product_id),
212 ('product_id', '=', False),
213 ('product_tmpl_id', '=', product_tmpl_id)
215 elif product_tmpl_id:
216 domain = [('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl_id)]
218 # neither product nor template, makes no sense to search
220 domain = domain + [ '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
221 '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
222 # order to prioritize bom with product_id over the one without
223 ids = self.search(cr, uid, domain, order='product_id', context=context)
224 # Search a BoM which has all properties specified, or if you can not find one, you could
225 # pass a BoM without any properties
226 bom_empty_prop = False
227 for bom in self.pool.get('mrp.bom').browse(cr, uid, ids, context=context):
228 if not set(map(int, bom.property_ids or [])) - set(properties or []):
229 if properties and not bom.property_ids:
230 bom_empty_prop = bom.id
233 return bom_empty_prop
235 def _bom_explode(self, cr, uid, bom, product, factor, properties=None, level=0, routing_id=False, previous_products=None, master_bom=None, context=None):
236 """ Finds Products and Work Centers for related BoM for manufacturing order.
237 @param bom: BoM of particular product template.
238 @param product: Select a particular variant of the BoM. If False use BoM without variants.
239 @param factor: Factor represents the quantity, but in UoM of the BoM, taking into account the numbers produced by the BoM
240 @param properties: A List of properties Ids.
241 @param level: Depth level to find BoM lines starts from 10.
242 @param previous_products: List of product previously use by bom explore to avoid recursion
243 @param master_bom: When recursion, used to display the name of the master bom
244 @return: result: List of dictionaries containing product details.
245 result2: List of dictionaries containing Work Center details.
247 uom_obj = self.pool.get("product.uom")
248 routing_obj = self.pool.get('mrp.routing')
249 master_bom = master_bom or bom
252 def _factor(factor, product_efficiency, product_rounding):
253 factor = factor / (product_efficiency or 1.0)
254 factor = _common.ceiling(factor, product_rounding)
255 if factor < product_rounding:
256 factor = product_rounding
259 factor = _factor(factor, bom.product_efficiency, bom.product_rounding)
264 routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
266 for wc_use in routing.workcenter_lines:
267 wc = wc_use.workcenter_id
268 d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
269 mult = (d + (m and 1.0 or 0.0))
270 cycle = mult * wc_use.cycle_nbr
272 'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_tmpl_id.name_get()[0][1]),
273 'workcenter_id': wc.id,
274 'sequence': level + (wc_use.sequence or 0),
276 '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)),
279 for bom_line_id in bom.bom_line_ids:
280 if bom_line_id.date_start and bom_line_id.date_start > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) or \
281 bom_line_id.date_stop and bom_line_id.date_stop < time.strftime(DEFAULT_SERVER_DATETIME_FORMAT):
283 # all bom_line_id variant values must be in the product
284 if bom_line_id.attribute_value_ids:
285 if not product or (set(map(int,bom_line_id.attribute_value_ids or [])) - set(map(int,product.attribute_value_ids))):
288 if previous_products and bom_line_id.product_id.product_tmpl_id.id in previous_products:
289 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]))
291 quantity = _factor(bom_line_id.product_qty * factor, bom_line_id.product_efficiency, bom_line_id.product_rounding)
292 bom_id = self._bom_find(cr, uid, product_id=bom_line_id.product_id.id, properties=properties, context=context)
294 #If BoM should not behave like PhantoM, just add the product, otherwise explode further
295 if bom_line_id.type != "phantom" and (not bom_id or self.browse(cr, uid, bom_id, context=context).type != "phantom"):
297 'name': bom_line_id.product_id.name,
298 'product_id': bom_line_id.product_id.id,
299 'product_qty': quantity,
300 'product_uom': bom_line_id.product_uom.id,
301 'product_uos_qty': bom_line_id.product_uos and _factor(bom_line_id.product_uos_qty * factor, bom_line_id.product_efficiency, bom_line_id.product_rounding) or False,
302 'product_uos': bom_line_id.product_uos and bom_line_id.product_uos.id or False,
305 all_prod = [bom.product_tmpl_id.id] + (previous_products or [])
306 bom2 = self.browse(cr, uid, bom_id, context=context)
307 # We need to convert to units/UoM of chosen BoM
308 factor2 = uom_obj._compute_qty(cr, uid, bom_line_id.product_uom.id, quantity, bom2.product_uom.id)
309 quantity2 = factor2 / bom2.product_qty
310 res = self._bom_explode(cr, uid, bom2, bom_line_id.product_id, quantity2,
311 properties=properties, level=level + 10, previous_products=all_prod, master_bom=master_bom, context=context)
312 result = result + res[0]
313 result2 = result2 + res[1]
315 raise osv.except_osv(_('Invalid Action!'), _('BoM "%s" contains a phantom BoM line but the product "%s" does not have any BoM defined.') % (master_bom.name,bom_line_id.product_id.name_get()[0][1]))
317 return result, result2
319 def copy_data(self, cr, uid, id, default=None, context=None):
322 bom_data = self.read(cr, uid, id, [], context=context)
323 default.update(name=_("%s (copy)") % (bom_data['name']))
324 return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
326 def onchange_uom(self, cr, uid, ids, product_tmpl_id, product_uom, context=None):
328 if not product_uom or not product_tmpl_id:
330 product = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
331 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
332 if uom.category_id.id != product.uom_id.category_id.id:
333 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
334 res['value'].update({'product_uom': product.uom_id.id})
337 def onchange_product_tmpl_id(self, cr, uid, ids, product_tmpl_id, product_qty=0, context=None):
338 """ Changes UoM and name if product_id changes.
339 @param product_id: Changed product_id
340 @return: Dictionary of changed values
344 prod = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
347 'product_uom': prod.uom_id.id,
351 class mrp_bom_line(osv.osv):
352 _name = 'mrp.bom.line'
355 def _get_child_bom_lines(self, cr, uid, ids, field_name, arg, context=None):
356 """If the BOM line refers to a BOM, return the ids of the child BOM lines"""
357 bom_obj = self.pool['mrp.bom']
359 for bom_line in self.browse(cr, uid, ids, context=context):
360 bom_id = bom_obj._bom_find(cr, uid,
361 product_tmpl_id=bom_line.product_id.product_tmpl_id.id,
362 product_id=bom_line.product_id.id, context=context)
364 child_bom = bom_obj.browse(cr, uid, bom_id, context=context)
365 res[bom_line.id] = [x.id for x in child_bom.bom_line_ids]
367 res[bom_line.id] = False
371 'type': fields.selection([('normal', 'Normal'), ('phantom', 'Phantom')], 'BoM Line Type', required=True,
372 help="Phantom: this product line will not appear in the raw materials of manufacturing orders,"
373 "it will be directly replaced by the raw materials of its own BoM, without triggering"
374 "an extra manufacturing order."),
375 'product_id': fields.many2one('product.product', 'Product', required=True),
376 'product_uos_qty': fields.float('Product UOS Qty'),
377 '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."),
378 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
379 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True,
380 help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"),
382 'date_start': fields.date('Valid From', help="Validity of component. Keep empty if it's always valid."),
383 'date_stop': fields.date('Valid Until', help="Validity of component. Keep empty if it's always valid."),
384 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying."),
385 '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."),
386 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
387 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
388 'property_ids': fields.many2many('mrp.property', string='Properties'), #Not used
390 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True, required=True),
391 'attribute_value_ids': fields.many2many('product.attribute.value', string='Variants', help="BOM Product Variants needed form apply this line."),
392 'child_line_ids': fields.function(_get_child_bom_lines, relation="mrp.bom.line", string="BOM lines of the referred bom", type="one2many")
395 def _get_uom_id(self, cr, uid, *args):
396 return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
398 'product_qty': lambda *a: 1.0,
399 'product_efficiency': lambda *a: 1.0,
400 'product_rounding': lambda *a: 0.0,
401 'type': lambda *a: 'normal',
402 'product_uom': _get_uom_id,
405 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
406 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
409 def create(self, cr, uid, values, context=None):
412 product_obj = self.pool.get('product.product')
413 if 'product_id' in values and not 'product_uom' in values:
414 values['product_uom'] = product_obj.browse(cr, uid, values.get('product_id'), context=context).uom_id.id
415 return super(mrp_bom_line, self).create(cr, uid, values, context=context)
417 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
419 if not product_uom or not product_id:
421 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
422 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
423 if uom.category_id.id != product.uom_id.category_id.id:
424 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
425 res['value'].update({'product_uom': product.uom_id.id})
428 def onchange_product_id(self, cr, uid, ids, product_id, product_qty=0, context=None):
429 """ Changes UoM if product_id changes.
430 @param product_id: Changed product_id
431 @return: Dictionary of changed values
435 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
437 'product_uom': prod.uom_id.id,
438 'product_uos_qty': 0,
442 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
443 res['value']['product_uos'] = prod.uos_id.id
446 class mrp_production(osv.osv):
448 Production Orders / Manufacturing Orders
450 _name = 'mrp.production'
451 _description = 'Manufacturing Order'
452 _date_name = 'date_planned'
453 _inherit = ['mail.thread', 'ir.needaction_mixin']
455 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
456 """ Calculates total hours and total no. of cycles for a production order.
457 @param prop: Name of field.
459 @return: Dictionary of values.
462 for prod in self.browse(cr, uid, ids, context=context):
467 for wc in prod.workcenter_lines:
468 result[prod.id]['hour_total'] += wc.hour
469 result[prod.id]['cycle_total'] += wc.cycle
472 def _src_id_default(self, cr, uid, ids, context=None):
474 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
475 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
476 except (orm.except_orm, ValueError):
480 def _dest_id_default(self, cr, uid, ids, context=None):
482 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
483 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
484 except (orm.except_orm, ValueError):
488 def _get_progress(self, cr, uid, ids, name, arg, context=None):
489 """ Return product quantity percentage """
490 result = dict.fromkeys(ids, 100)
491 for mrp_production in self.browse(cr, uid, ids, context=context):
492 if mrp_production.product_qty:
494 for move in mrp_production.move_created_ids2:
495 if not move.scrapped and move.product_id == mrp_production.product_id:
496 done += move.product_qty
497 result[mrp_production.id] = done / mrp_production.product_qty * 100
500 def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
501 """ Test whether all the consume lines are assigned """
503 for production in self.browse(cr, uid, ids, context=context):
504 res[production.id] = True
505 states = [x.state != 'assigned' for x in production.move_lines if x]
506 if any(states) or len(states) == 0: #When no moves, ready_production will be False, but test_ready will pass
507 res[production.id] = False
510 def _mrp_from_move(self, cr, uid, ids, context=None):
513 for move in self.browse(cr, uid, ids, context=context):
514 res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
518 'name': fields.char('Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
519 'origin': fields.char('Source Document', readonly=True, states={'draft': [('readonly', False)]},
520 help="Reference of the document that generated this production order request.", copy=False),
521 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
522 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
524 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]},
525 domain=[('type','!=','service')]),
526 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
527 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
528 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
529 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
530 'progress': fields.function(_get_progress, type='float',
531 string='Production progress'),
533 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
534 readonly=True, states={'draft': [('readonly', False)]},
535 help="Location where the system will look for components."),
536 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
537 readonly=True, states={'draft': [('readonly', False)]},
538 help="Location where the system will stock the finished products."),
539 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
540 'date_start': fields.datetime('Start Date', select=True, readonly=True, copy=False),
541 'date_finished': fields.datetime('End Date', select=True, readonly=True, copy=False),
542 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', readonly=True, states={'draft': [('readonly', False)]},
543 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
544 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
545 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."),
546 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True, copy=False),
547 'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
548 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
549 'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
550 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
551 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
552 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
553 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
554 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
555 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
557 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
558 readonly=True, states={'draft': [('readonly', False)]}),
559 'state': fields.selection(
560 [('draft', 'New'), ('cancel', 'Cancelled'), ('confirmed', 'Awaiting Raw Materials'),
561 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
562 string='Status', readonly=True,
563 track_visibility='onchange', copy=False,
564 help="When the production order is created the status is set to 'Draft'.\n\
565 If the order is confirmed the status is set to 'Waiting Goods'.\n\
566 If any exceptions are there, the status is set to 'Picking Exception'.\n\
567 If the stock is available then the status is set to 'Ready to Produce'.\n\
568 When the production gets started then the status is set to 'In Production'.\n\
569 When the production is over, the status is set to 'Done'."),
570 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
571 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
572 'user_id': fields.many2one('res.users', 'Responsible'),
573 'company_id': fields.many2one('res.company', 'Company', required=True),
574 'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
578 'priority': lambda *a: '1',
579 'state': lambda *a: 'draft',
580 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
581 'product_qty': lambda *a: 1.0,
582 'user_id': lambda self, cr, uid, c: uid,
583 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
584 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
585 'location_src_id': _src_id_default,
586 'location_dest_id': _dest_id_default
590 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
593 _order = 'priority desc, date_planned asc'
595 def _check_qty(self, cr, uid, ids, context=None):
596 for order in self.browse(cr, uid, ids, context=context):
597 if order.product_qty <= 0:
602 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
605 def create(self, cr, uid, values, context=None):
608 product_obj = self.pool.get('product.product')
609 if 'product_id' in values and not 'product_uom' in values:
610 values['product_uom'] = product_obj.browse(cr, uid, values.get('product_id'), context=context).uom_id.id
611 return super(mrp_production, self).create(cr, uid, values, context=context)
613 def unlink(self, cr, uid, ids, context=None):
614 for production in self.browse(cr, uid, ids, context=context):
615 if production.state not in ('draft', 'cancel'):
616 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
617 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
619 def location_id_change(self, cr, uid, ids, src, dest, context=None):
620 """ Changes destination location if source location is changed.
621 @param src: Source location id.
622 @param dest: Destination location id.
623 @return: Dictionary of values.
628 return {'value': {'location_dest_id': src}}
631 def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
632 """ Finds UoM of changed product.
633 @param product_id: Id of changed product.
634 @return: Dictionary of values.
639 'product_uom': False,
642 'product_uos_qty': 0,
645 bom_obj = self.pool.get('mrp.bom')
646 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
647 bom_id = bom_obj._bom_find(cr, uid, product_id=product.id, properties=[], context=context)
650 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
651 routing_id = bom_point.routing_id.id or False
652 product_uom_id = product.uom_id and product.uom_id.id or False
653 result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
654 if product.uos_id.id:
655 result['value']['product_uos_qty'] = product_qty * product.uos_coeff
656 result['value']['product_uos'] = product.uos_id.id
659 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
660 """ Finds routing for changed BoM.
661 @param product: Id of product.
662 @return: Dictionary of values.
668 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
669 routing_id = bom_point.routing_id.id or False
671 'routing_id': routing_id
673 return {'value': result}
676 def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
677 """ Compute product_lines and workcenter_lines from BoM structure
678 @return: product_lines
680 if properties is None:
683 bom_obj = self.pool.get('mrp.bom')
684 uom_obj = self.pool.get('product.uom')
685 prod_line_obj = self.pool.get('mrp.production.product.line')
686 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
687 for production in self.browse(cr, uid, ids, context=context):
688 #unlink product_lines
689 prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
690 #unlink workcenter_lines
691 workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
692 # search BoM structure and route
693 bom_point = production.bom_id
694 bom_id = production.bom_id.id
696 bom_id = bom_obj._bom_find(cr, uid, product_id=production.product_id.id, properties=properties, context=context)
698 bom_point = bom_obj.browse(cr, uid, bom_id)
699 routing_id = bom_point.routing_id.id or False
700 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
703 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
705 # get components and workcenter_lines from BoM structure
706 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
707 # product_lines, workcenter_lines
708 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, context=context)
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
826 product_qty is in the uom of the product
828 quant_obj = self.pool.get("stock.quant")
829 uom_obj = self.pool.get("product.uom")
830 produced_qty = self._get_produced_qty(cr, uid, production, context=context)
831 consumed_data = self._get_consumed_data(cr, uid, production, context=context)
833 #In case no product_qty is given, take the remaining qty to produce for the given production
835 product_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id) - produced_qty
836 production_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id)
839 for scheduled in production.product_lines:
840 if scheduled.product_id.type == 'service':
842 qty = uom_obj._compute_qty(cr, uid, scheduled.product_uom.id, scheduled.product_qty, scheduled.product_id.uom_id.id)
843 if scheduled_qty.get(scheduled.product_id.id):
844 scheduled_qty[scheduled.product_id.id] += qty
846 scheduled_qty[scheduled.product_id.id] = qty
848 # Find product qty to be consumed and consume it
849 for product_id in scheduled_qty.keys():
851 consumed_qty = consumed_data.get(product_id, 0.0)
853 # qty available for consume and produce
854 sched_product_qty = scheduled_qty[product_id]
855 qty_avail = sched_product_qty - consumed_qty
857 # there will be nothing to consume for this raw material
860 if not dicts.get(product_id):
861 dicts[product_id] = {}
863 # total qty of consumed product we need after this consumption
864 if product_qty + produced_qty <= production_qty:
865 total_consume = ((product_qty + produced_qty) * sched_product_qty / production_qty)
867 total_consume = sched_product_qty
868 qty = total_consume - consumed_qty
870 # Search for quants related to this related move
871 for move in production.move_lines:
874 if move.product_id.id != product_id:
877 q = min(move.product_qty, qty)
878 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, q, domain=[('qty', '>', 0.0)],
879 prefered_domain_list=[[('reservation_id', '=', move.id)]], context=context)
880 for quant, quant_qty in quants:
882 lot_id = quant.lot_id.id
883 if not product_id in dicts.keys():
884 dicts[product_id] = {lot_id: quant_qty}
885 elif lot_id in dicts[product_id].keys():
886 dicts[product_id][lot_id] += quant_qty
888 dicts[product_id][lot_id] = quant_qty
891 if dicts[product_id].get(False):
892 dicts[product_id][False] += qty
894 dicts[product_id][False] = qty
897 for prod in dicts.keys():
898 for lot, qty in dicts[prod].items():
899 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
902 def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
903 """ To produce final product based on production mode (consume/consume&produce).
904 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
905 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
906 and stock move lines of final product will be also done/produced.
907 @param production_id: the ID of mrp.production object
908 @param production_qty: specify qty to produce in the uom of the production order
909 @param production_mode: specify production mode (consume/consume&produce).
910 @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
913 stock_mov_obj = self.pool.get('stock.move')
914 uom_obj = self.pool.get("product.uom")
915 production = self.browse(cr, uid, production_id, context=context)
916 production_qty_uom = uom_obj._compute_qty(cr, uid, production.product_uom.id, production_qty, production.product_id.uom_id.id)
918 main_production_move = False
919 if production_mode == 'consume_produce':
920 # To produce remaining qty of final product
921 produced_products = {}
922 for produced_product in production.move_created_ids2:
923 if produced_product.scrapped:
925 if not produced_products.get(produced_product.product_id.id, False):
926 produced_products[produced_product.product_id.id] = 0
927 produced_products[produced_product.product_id.id] += produced_product.product_qty
929 for produce_product in production.move_created_ids:
930 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
933 lot_id = wiz.lot_id.id
934 new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty_uom),
935 location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
936 stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
937 if produce_product.product_id.id == production.product_id.id and new_moves:
938 main_production_move = new_moves[0]
940 if production_mode in ['consume', 'consume_produce']:
943 for cons in wiz.consume_lines:
944 consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
946 consume_lines = self._calculate_qty(cr, uid, production, production_qty_uom, context=context)
947 for consume in consume_lines:
948 remaining_qty = consume['product_qty']
949 for raw_material_line in production.move_lines:
950 if remaining_qty <= 0:
952 if consume['product_id'] != raw_material_line.product_id.id:
954 consumed_qty = min(remaining_qty, raw_material_line.product_qty)
955 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)
956 remaining_qty -= consumed_qty
958 #consumed more in wizard than previously planned
959 product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
960 extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
962 stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
964 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
965 self.signal_workflow(cr, uid, [production_id], 'button_produce_done')
968 def _costs_generate(self, cr, uid, production):
969 """ Calculates total costs at the end of the production.
970 @param production: Id of production order.
971 @return: Calculated amount.
974 analytic_line_obj = self.pool.get('account.analytic.line')
975 for wc_line in production.workcenter_lines:
976 wc = wc_line.workcenter_id
977 if wc.costs_journal_id and wc.costs_general_account_id:
979 value = wc_line.hour * wc.costs_hour
980 account = wc.costs_hour_account_id.id
981 if value and account:
983 # we user SUPERUSER_ID as we do not garantee an mrp user
984 # has access to account analytic lines but still should be
985 # able to produce orders
986 analytic_line_obj.create(cr, SUPERUSER_ID, {
987 'name': wc_line.name + ' (H)',
989 'account_id': account,
990 'general_account_id': wc.costs_general_account_id.id,
991 'journal_id': wc.costs_journal_id.id,
993 'product_id': wc.product_id.id,
994 'unit_amount': wc_line.hour,
995 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
998 value = wc_line.cycle * wc.costs_cycle
999 account = wc.costs_cycle_account_id.id
1000 if value and account:
1002 analytic_line_obj.create(cr, SUPERUSER_ID, {
1003 'name': wc_line.name + ' (C)',
1005 'account_id': account,
1006 'general_account_id': wc.costs_general_account_id.id,
1007 'journal_id': wc.costs_journal_id.id,
1009 'product_id': wc.product_id.id,
1010 'unit_amount': wc_line.cycle,
1011 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
1015 def action_in_production(self, cr, uid, ids, context=None):
1016 """ Changes state to In Production and writes starting date.
1019 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
1021 def consume_lines_get(self, cr, uid, ids, *args):
1023 for order in self.browse(cr, uid, ids, context={}):
1024 res += [x.id for x in order.move_lines]
1027 def test_ready(self, cr, uid, ids):
1029 for production in self.browse(cr, uid, ids):
1030 if production.move_lines and not production.ready_production:
1036 def _make_production_produce_line(self, cr, uid, production, context=None):
1037 stock_move = self.pool.get('stock.move')
1038 source_location_id = production.product_id.property_stock_production.id
1039 destination_location_id = production.location_dest_id.id
1041 'name': production.name,
1042 'date': production.date_planned,
1043 'product_id': production.product_id.id,
1044 'product_uom': production.product_uom.id,
1045 'product_uom_qty': production.product_qty,
1046 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1047 'product_uos': production.product_uos and production.product_uos.id or False,
1048 'location_id': source_location_id,
1049 'location_dest_id': destination_location_id,
1050 'move_dest_id': production.move_prod_id.id,
1051 'company_id': production.company_id.id,
1052 'production_id': production.id,
1053 'origin': production.name,
1055 move_id = stock_move.create(cr, uid, data, context=context)
1056 #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1057 #is 1 element long, so we can take the first.
1058 return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1060 def _get_raw_material_procure_method(self, cr, uid, product, context=None):
1061 '''This method returns the procure_method to use when creating the stock move for the production raw materials'''
1062 warehouse_obj = self.pool['stock.warehouse']
1064 mto_route = warehouse_obj._get_mto_route(cr, uid, context=context)
1066 return "make_to_stock"
1067 routes = product.route_ids + product.categ_id.total_route_ids
1068 if mto_route in [x.id for x in routes]:
1069 return "make_to_order"
1070 return "make_to_stock"
1072 def _create_previous_move(self, cr, uid, move_id, product, source_location_id, dest_location_id, context=None):
1074 When the routing gives a different location than the raw material location of the production order,
1075 we should create an extra move from the raw material location to the location of the routing, which
1076 precedes the consumption line (chained). The picking type depends on the warehouse in which this happens
1077 and the type of locations.
1079 loc_obj = self.pool.get("stock.location")
1080 stock_move = self.pool.get('stock.move')
1081 type_obj = self.pool.get('stock.picking.type')
1082 # Need to search for a picking type
1083 move = stock_move.browse(cr, uid, move_id, context=context)
1084 src_loc = loc_obj.browse(cr, uid, source_location_id, context=context)
1085 dest_loc = loc_obj.browse(cr, uid, dest_location_id, context=context)
1086 code = stock_move.get_code_from_locs(cr, uid, move, src_loc, dest_loc, context=context)
1087 if code == 'outgoing':
1090 check_loc = dest_loc
1091 wh = loc_obj.get_warehouse(cr, uid, check_loc, context=context)
1092 domain = [('code', '=', code)]
1094 domain += [('warehouse_id', '=', wh)]
1095 types = type_obj.search(cr, uid, domain, context=context)
1096 move = stock_move.copy(cr, uid, move_id, default = {
1097 'location_id': source_location_id,
1098 'location_dest_id': dest_location_id,
1099 'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context),
1100 'raw_material_production_id': False,
1101 'move_dest_id': move_id,
1102 'picking_type_id': types and types[0] or False,
1106 def _make_consume_line_from_data(self, cr, uid, production, product, uom_id, qty, uos_id, uos_qty, context=None):
1107 stock_move = self.pool.get('stock.move')
1108 loc_obj = self.pool.get('stock.location')
1109 # Internal shipment is created for Stockable and Consumer Products
1110 if product.type not in ('product', 'consu'):
1112 # Take routing location as a Source Location.
1113 source_location_id = production.location_src_id.id
1114 prod_location_id = source_location_id
1116 if production.bom_id.routing_id and production.bom_id.routing_id.location_id and production.bom_id.routing_id.location_id.id != source_location_id:
1117 source_location_id = production.bom_id.routing_id.location_id.id
1120 destination_location_id = production.product_id.property_stock_production.id
1121 move_id = stock_move.create(cr, uid, {
1122 'name': production.name,
1123 'date': production.date_planned,
1124 'product_id': product.id,
1125 'product_uom_qty': qty,
1126 'product_uom': uom_id,
1127 'product_uos_qty': uos_id and uos_qty or False,
1128 'product_uos': uos_id or False,
1129 'location_id': source_location_id,
1130 'location_dest_id': destination_location_id,
1131 'company_id': production.company_id.id,
1132 'procure_method': prev_move and 'make_to_stock' or self._get_raw_material_procure_method(cr, uid, product, context=context), #Make_to_stock avoids creating procurement
1133 'raw_material_production_id': production.id,
1134 #this saves us a browse in create()
1135 'price_unit': product.standard_price,
1136 'origin': production.name,
1137 'warehouse_id': loc_obj.get_warehouse(cr, uid, production.location_src_id, context=context),
1141 prev_move = self._create_previous_move(cr, uid, move_id, product, prod_location_id, source_location_id, context=context)
1142 stock_move.action_confirm(cr, uid, [prev_move], context=context)
1145 def _make_production_consume_line(self, cr, uid, line, context=None):
1146 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)
1149 def _make_service_procurement(self, cr, uid, line, context=None):
1150 prod_obj = self.pool.get('product.product')
1151 if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
1153 'name': line.production_id.name,
1154 'origin': line.production_id.name,
1155 'company_id': line.production_id.company_id.id,
1156 'date_planned': line.production_id.date_planned,
1157 'product_id': line.product_id.id,
1158 'product_qty': line.product_qty,
1159 'product_uom': line.product_uom.id,
1160 'product_uos_qty': line.product_uos_qty,
1161 'product_uos': line.product_uos.id,
1163 proc_obj = self.pool.get("procurement.order")
1164 proc = proc_obj.create(cr, uid, vals, context=context)
1165 proc_obj.run(cr, uid, [proc], context=context)
1168 def action_confirm(self, cr, uid, ids, context=None):
1169 """ Confirms production order.
1170 @return: Newly generated Shipment Id.
1172 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)])
1173 self.action_compute(cr, uid, uncompute_ids, context=context)
1174 for production in self.browse(cr, uid, ids, context=context):
1175 self._make_production_produce_line(cr, uid, production, context=context)
1178 for line in production.product_lines:
1179 if line.product_id.type != 'service':
1180 stock_move_id = self._make_production_consume_line(cr, uid, line, context=context)
1181 stock_moves.append(stock_move_id)
1183 self._make_service_procurement(cr, uid, line, context=context)
1185 self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
1186 production.write({'state': 'confirmed'}, context=context)
1189 def action_assign(self, cr, uid, ids, context=None):
1191 Checks the availability on the consume lines of the production order
1193 from openerp import workflow
1194 move_obj = self.pool.get("stock.move")
1195 for production in self.browse(cr, uid, ids, context=context):
1196 move_obj.action_assign(cr, uid, [x.id for x in production.move_lines], context=context)
1197 if self.pool.get('mrp.production').test_ready(cr, uid, [production.id]):
1198 workflow.trg_validate(uid, 'mrp.production', production.id, 'moves_ready', cr)
1201 def force_production(self, cr, uid, ids, *args):
1202 """ Assigns products.
1203 @param *args: Arguments
1206 from openerp import workflow
1207 move_obj = self.pool.get('stock.move')
1208 for order in self.browse(cr, uid, ids):
1209 move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1210 if self.pool.get('mrp.production').test_ready(cr, uid, [order.id]):
1211 workflow.trg_validate(uid, 'mrp.production', order.id, 'moves_ready', cr)
1215 class mrp_production_workcenter_line(osv.osv):
1216 _name = 'mrp.production.workcenter.line'
1217 _description = 'Work Order'
1219 _inherit = ['mail.thread']
1222 'name': fields.char('Work Order', required=True),
1223 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1224 'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1225 'hour': fields.float('Number of Hours', digits=(16, 2)),
1226 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1227 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1228 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1231 'sequence': lambda *a: 1,
1232 'hour': lambda *a: 0,
1233 'cycle': lambda *a: 0,
1236 class mrp_production_product_line(osv.osv):
1237 _name = 'mrp.production.product.line'
1238 _description = 'Production Scheduled Product'
1240 'name': fields.char('Name', required=True),
1241 'product_id': fields.many2one('product.product', 'Product', required=True),
1242 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1243 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1244 'product_uos_qty': fields.float('Product UOS Quantity'),
1245 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1246 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1249 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: