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 onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
411 if not product_uom or not product_id:
413 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
414 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
415 if uom.category_id.id != product.uom_id.category_id.id:
416 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
417 res['value'].update({'product_uom': product.uom_id.id})
420 def onchange_product_id(self, cr, uid, ids, product_id, product_qty=0, context=None):
421 """ Changes UoM if product_id changes.
422 @param product_id: Changed product_id
423 @return: Dictionary of changed values
427 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
429 'product_uom': prod.uom_id.id,
430 'product_uos_qty': 0,
434 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
435 res['value']['product_uos'] = prod.uos_id.id
438 class mrp_production(osv.osv):
440 Production Orders / Manufacturing Orders
442 _name = 'mrp.production'
443 _description = 'Manufacturing Order'
444 _date_name = 'date_planned'
445 _inherit = ['mail.thread', 'ir.needaction_mixin']
447 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
448 """ Calculates total hours and total no. of cycles for a production order.
449 @param prop: Name of field.
451 @return: Dictionary of values.
454 for prod in self.browse(cr, uid, ids, context=context):
459 for wc in prod.workcenter_lines:
460 result[prod.id]['hour_total'] += wc.hour
461 result[prod.id]['cycle_total'] += wc.cycle
464 def _src_id_default(self, cr, uid, ids, context=None):
466 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
467 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
468 except (orm.except_orm, ValueError):
472 def _dest_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 _get_progress(self, cr, uid, ids, name, arg, context=None):
481 """ Return product quantity percentage """
482 result = dict.fromkeys(ids, 100)
483 for mrp_production in self.browse(cr, uid, ids, context=context):
484 if mrp_production.product_qty:
486 for move in mrp_production.move_created_ids2:
487 if not move.scrapped and move.product_id == mrp_production.product_id:
488 done += move.product_qty
489 result[mrp_production.id] = done / mrp_production.product_qty * 100
492 def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
493 """ Test whether all the consume lines are assigned """
495 for production in self.browse(cr, uid, ids, context=context):
496 res[production.id] = True
497 states = [x.state != 'assigned' for x in production.move_lines if x]
498 if any(states) or len(states) == 0: #When no moves, ready_production will be False, but test_ready will pass
499 res[production.id] = False
502 def _mrp_from_move(self, cr, uid, ids, context=None):
505 for move in self.browse(cr, uid, ids, context=context):
506 res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
510 'name': fields.char('Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
511 'origin': fields.char('Source Document', readonly=True, states={'draft': [('readonly', False)]},
512 help="Reference of the document that generated this production order request.", copy=False),
513 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
514 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
516 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]},
517 domain=[('type','!=','service')]),
518 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
519 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
520 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
521 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
522 'progress': fields.function(_get_progress, type='float',
523 string='Production progress'),
525 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
526 readonly=True, states={'draft': [('readonly', False)]},
527 help="Location where the system will look for components."),
528 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
529 readonly=True, states={'draft': [('readonly', False)]},
530 help="Location where the system will stock the finished products."),
531 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
532 'date_start': fields.datetime('Start Date', select=True, readonly=True, copy=False),
533 'date_finished': fields.datetime('End Date', select=True, readonly=True, copy=False),
534 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', readonly=True, states={'draft': [('readonly', False)]},
535 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
536 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
537 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."),
538 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True, copy=False),
539 'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
540 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
541 'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
542 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
543 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
544 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
545 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
546 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
547 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
549 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
550 readonly=True, states={'draft': [('readonly', False)]}),
551 'state': fields.selection(
552 [('draft', 'New'), ('cancel', 'Cancelled'), ('confirmed', 'Awaiting Raw Materials'),
553 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
554 string='Status', readonly=True,
555 track_visibility='onchange', copy=False,
556 help="When the production order is created the status is set to 'Draft'.\n\
557 If the order is confirmed the status is set to 'Waiting Goods'.\n\
558 If any exceptions are there, the status is set to 'Picking Exception'.\n\
559 If the stock is available then the status is set to 'Ready to Produce'.\n\
560 When the production gets started then the status is set to 'In Production'.\n\
561 When the production is over, the status is set to 'Done'."),
562 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
563 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
564 'user_id': fields.many2one('res.users', 'Responsible'),
565 'company_id': fields.many2one('res.company', 'Company', required=True),
566 'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
570 'priority': lambda *a: '1',
571 'state': lambda *a: 'draft',
572 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
573 'product_qty': lambda *a: 1.0,
574 'user_id': lambda self, cr, uid, c: uid,
575 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
576 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
577 'location_src_id': _src_id_default,
578 'location_dest_id': _dest_id_default
582 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
585 _order = 'priority desc, date_planned asc'
587 def _check_qty(self, cr, uid, ids, context=None):
588 for order in self.browse(cr, uid, ids, context=context):
589 if order.product_qty <= 0:
594 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
597 def unlink(self, cr, uid, ids, context=None):
598 for production in self.browse(cr, uid, ids, context=context):
599 if production.state not in ('draft', 'cancel'):
600 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
601 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
603 def location_id_change(self, cr, uid, ids, src, dest, context=None):
604 """ Changes destination location if source location is changed.
605 @param src: Source location id.
606 @param dest: Destination location id.
607 @return: Dictionary of values.
612 return {'value': {'location_dest_id': src}}
615 def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
616 """ Finds UoM of changed product.
617 @param product_id: Id of changed product.
618 @return: Dictionary of values.
623 'product_uom': False,
626 'product_uos_qty': 0,
629 bom_obj = self.pool.get('mrp.bom')
630 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
631 bom_id = bom_obj._bom_find(cr, uid, product_id=product.id, properties=[], context=context)
634 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
635 routing_id = bom_point.routing_id.id or False
636 product_uom_id = product.uom_id and product.uom_id.id or False
637 result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
638 if product.uos_id.id:
639 result['value']['product_uos_qty'] = product_qty * product.uos_coeff
640 result['value']['product_uos'] = product.uos_id.id
643 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
644 """ Finds routing for changed BoM.
645 @param product: Id of product.
646 @return: Dictionary of values.
652 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
653 routing_id = bom_point.routing_id.id or False
655 'routing_id': routing_id
657 return {'value': result}
660 def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
661 """ Compute product_lines and workcenter_lines from BoM structure
662 @return: product_lines
664 if properties is None:
667 bom_obj = self.pool.get('mrp.bom')
668 uom_obj = self.pool.get('product.uom')
669 prod_line_obj = self.pool.get('mrp.production.product.line')
670 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
671 for production in self.browse(cr, uid, ids, context=context):
672 #unlink product_lines
673 prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
674 #unlink workcenter_lines
675 workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
676 # search BoM structure and route
677 bom_point = production.bom_id
678 bom_id = production.bom_id.id
680 bom_id = bom_obj._bom_find(cr, uid, product_id=production.product_id.id, properties=properties, context=context)
682 bom_point = bom_obj.browse(cr, uid, bom_id)
683 routing_id = bom_point.routing_id.id or False
684 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
687 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
689 # get components and workcenter_lines from BoM structure
690 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
691 # product_lines, workcenter_lines
692 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)
694 # reset product_lines in production order
696 line['production_id'] = production.id
697 prod_line_obj.create(cr, uid, line)
699 #reset workcenter_lines in production order
700 for line in results2:
701 line['production_id'] = production.id
702 workcenter_line_obj.create(cr, uid, line)
705 def action_compute(self, cr, uid, ids, properties=None, context=None):
706 """ Computes bills of material of a product.
707 @param properties: List containing dictionaries of properties.
708 @return: No. of products.
710 return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
712 def action_cancel(self, cr, uid, ids, context=None):
713 """ Cancels the production order and related stock moves.
718 move_obj = self.pool.get('stock.move')
719 for production in self.browse(cr, uid, ids, context=context):
720 if production.move_created_ids:
721 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
722 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
723 self.write(cr, uid, ids, {'state': 'cancel'})
724 # Put related procurements in exception
725 proc_obj = self.pool.get("procurement.order")
726 procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
728 proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
731 def action_ready(self, cr, uid, ids, context=None):
732 """ Changes the production state to Ready and location id of stock move.
735 move_obj = self.pool.get('stock.move')
736 self.write(cr, uid, ids, {'state': 'ready'})
738 for production in self.browse(cr, uid, ids, context=context):
739 if not production.move_created_ids:
740 self._make_production_produce_line(cr, uid, production, context=context)
742 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
743 move_obj.write(cr, uid, [production.move_prod_id.id],
744 {'location_id': production.location_dest_id.id})
747 def action_production_end(self, cr, uid, ids, context=None):
748 """ Changes production state to Finish and writes finished date.
751 for production in self.browse(cr, uid, ids):
752 self._costs_generate(cr, uid, production)
753 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
754 # Check related procurements
755 proc_obj = self.pool.get("procurement.order")
756 procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
757 proc_obj.check(cr, uid, procs, context=context)
760 def test_production_done(self, cr, uid, ids):
761 """ Tests whether production is done or not.
762 @return: True or False
765 for production in self.browse(cr, uid, ids):
766 if production.move_lines:
769 if production.move_created_ids:
773 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
774 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
775 it's always equal to the quantity encoded in the production order or the production wizard, but if the
776 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
778 :param production_id: ID of the mrp.order
779 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
780 :return: The factor to apply to the quantity that we should produce for the given production order.
784 def _get_produced_qty(self, cr, uid, production, context=None):
785 ''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
788 for produced_product in production.move_created_ids2:
789 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
791 produced_qty += produced_product.product_qty
794 def _get_consumed_data(self, cr, uid, production, context=None):
795 ''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
798 # Calculate already consumed qtys
799 for consumed in production.move_lines2:
800 if consumed.scrapped:
802 if not consumed_data.get(consumed.product_id.id, False):
803 consumed_data[consumed.product_id.id] = 0
804 consumed_data[consumed.product_id.id] += consumed.product_qty
807 def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
809 Calculates the quantity still needed to produce an extra number of products
810 product_qty is in the uom of the product
812 quant_obj = self.pool.get("stock.quant")
813 uom_obj = self.pool.get("product.uom")
814 produced_qty = self._get_produced_qty(cr, uid, production, context=context)
815 consumed_data = self._get_consumed_data(cr, uid, production, context=context)
817 #In case no product_qty is given, take the remaining qty to produce for the given production
819 product_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id) - produced_qty
820 production_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id)
823 for scheduled in production.product_lines:
824 if scheduled.product_id.type == 'service':
826 qty = uom_obj._compute_qty(cr, uid, scheduled.product_uom.id, scheduled.product_qty, scheduled.product_id.uom_id.id)
827 if scheduled_qty.get(scheduled.product_id.id):
828 scheduled_qty[scheduled.product_id.id] += qty
830 scheduled_qty[scheduled.product_id.id] = qty
832 # Find product qty to be consumed and consume it
833 for product_id in scheduled_qty.keys():
835 consumed_qty = consumed_data.get(product_id, 0.0)
837 # qty available for consume and produce
838 sched_product_qty = scheduled_qty[product_id]
839 qty_avail = sched_product_qty - consumed_qty
841 # there will be nothing to consume for this raw material
844 if not dicts.get(product_id):
845 dicts[product_id] = {}
847 # total qty of consumed product we need after this consumption
848 if product_qty + produced_qty <= production_qty:
849 total_consume = ((product_qty + produced_qty) * sched_product_qty / production_qty)
851 total_consume = sched_product_qty
852 qty = total_consume - consumed_qty
854 # Search for quants related to this related move
855 for move in production.move_lines:
858 if move.product_id.id != product_id:
861 q = min(move.product_qty, qty)
862 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, q, domain=[('qty', '>', 0.0)],
863 prefered_domain_list=[[('reservation_id', '=', move.id)]], context=context)
864 for quant, quant_qty in quants:
866 lot_id = quant.lot_id.id
867 if not product_id in dicts.keys():
868 dicts[product_id] = {lot_id: quant_qty}
869 elif lot_id in dicts[product_id].keys():
870 dicts[product_id][lot_id] += quant_qty
872 dicts[product_id][lot_id] = quant_qty
875 if dicts[product_id].get(False):
876 dicts[product_id][False] += qty
878 dicts[product_id][False] = qty
881 for prod in dicts.keys():
882 for lot, qty in dicts[prod].items():
883 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
886 def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
887 """ To produce final product based on production mode (consume/consume&produce).
888 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
889 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
890 and stock move lines of final product will be also done/produced.
891 @param production_id: the ID of mrp.production object
892 @param production_qty: specify qty to produce in the uom of the production order
893 @param production_mode: specify production mode (consume/consume&produce).
894 @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
897 stock_mov_obj = self.pool.get('stock.move')
898 uom_obj = self.pool.get("product.uom")
899 production = self.browse(cr, uid, production_id, context=context)
900 production_qty_uom = uom_obj._compute_qty(cr, uid, production.product_uom.id, production_qty, production.product_id.uom_id.id)
902 main_production_move = False
903 if production_mode == 'consume_produce':
904 # To produce remaining qty of final product
905 produced_products = {}
906 for produced_product in production.move_created_ids2:
907 if produced_product.scrapped:
909 if not produced_products.get(produced_product.product_id.id, False):
910 produced_products[produced_product.product_id.id] = 0
911 produced_products[produced_product.product_id.id] += produced_product.product_qty
913 for produce_product in production.move_created_ids:
914 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
917 lot_id = wiz.lot_id.id
918 new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty_uom),
919 location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
920 stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
921 if produce_product.product_id.id == production.product_id.id and new_moves:
922 main_production_move = new_moves[0]
924 if production_mode in ['consume', 'consume_produce']:
927 for cons in wiz.consume_lines:
928 consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
930 consume_lines = self._calculate_qty(cr, uid, production, production_qty_uom, context=context)
931 for consume in consume_lines:
932 remaining_qty = consume['product_qty']
933 for raw_material_line in production.move_lines:
934 if remaining_qty <= 0:
936 if consume['product_id'] != raw_material_line.product_id.id:
938 consumed_qty = min(remaining_qty, raw_material_line.product_qty)
939 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)
940 remaining_qty -= consumed_qty
942 #consumed more in wizard than previously planned
943 product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
944 extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
946 stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
948 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
949 self.signal_workflow(cr, uid, [production_id], 'button_produce_done')
952 def _costs_generate(self, cr, uid, production):
953 """ Calculates total costs at the end of the production.
954 @param production: Id of production order.
955 @return: Calculated amount.
958 analytic_line_obj = self.pool.get('account.analytic.line')
959 for wc_line in production.workcenter_lines:
960 wc = wc_line.workcenter_id
961 if wc.costs_journal_id and wc.costs_general_account_id:
963 value = wc_line.hour * wc.costs_hour
964 account = wc.costs_hour_account_id.id
965 if value and account:
967 # we user SUPERUSER_ID as we do not garantee an mrp user
968 # has access to account analytic lines but still should be
969 # able to produce orders
970 analytic_line_obj.create(cr, SUPERUSER_ID, {
971 'name': wc_line.name + ' (H)',
973 'account_id': account,
974 'general_account_id': wc.costs_general_account_id.id,
975 'journal_id': wc.costs_journal_id.id,
977 'product_id': wc.product_id.id,
978 'unit_amount': wc_line.hour,
979 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
982 value = wc_line.cycle * wc.costs_cycle
983 account = wc.costs_cycle_account_id.id
984 if value and account:
986 analytic_line_obj.create(cr, SUPERUSER_ID, {
987 'name': wc_line.name + ' (C)',
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.cycle,
995 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
999 def action_in_production(self, cr, uid, ids, context=None):
1000 """ Changes state to In Production and writes starting date.
1003 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
1005 def consume_lines_get(self, cr, uid, ids, *args):
1007 for order in self.browse(cr, uid, ids, context={}):
1008 res += [x.id for x in order.move_lines]
1011 def test_ready(self, cr, uid, ids):
1013 for production in self.browse(cr, uid, ids):
1014 if production.move_lines and not production.ready_production:
1020 def _make_production_produce_line(self, cr, uid, production, context=None):
1021 stock_move = self.pool.get('stock.move')
1022 source_location_id = production.product_id.property_stock_production.id
1023 destination_location_id = production.location_dest_id.id
1025 'name': production.name,
1026 'date': production.date_planned,
1027 'product_id': production.product_id.id,
1028 'product_uom': production.product_uom.id,
1029 'product_uom_qty': production.product_qty,
1030 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1031 'product_uos': production.product_uos and production.product_uos.id or False,
1032 'location_id': source_location_id,
1033 'location_dest_id': destination_location_id,
1034 'move_dest_id': production.move_prod_id.id,
1035 'company_id': production.company_id.id,
1036 'production_id': production.id,
1037 'origin': production.name,
1039 move_id = stock_move.create(cr, uid, data, context=context)
1040 #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1041 #is 1 element long, so we can take the first.
1042 return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1044 def _get_raw_material_procure_method(self, cr, uid, product, context=None):
1045 '''This method returns the procure_method to use when creating the stock move for the production raw materials'''
1046 warehouse_obj = self.pool['stock.warehouse']
1048 mto_route = warehouse_obj._get_mto_route(cr, uid, context=context)
1050 return "make_to_stock"
1051 routes = product.route_ids + product.categ_id.total_route_ids
1052 if mto_route in [x.id for x in routes]:
1053 return "make_to_order"
1054 return "make_to_stock"
1056 def _create_previous_move(self, cr, uid, move_id, product, source_location_id, dest_location_id, context=None):
1058 When the routing gives a different location than the raw material location of the production order,
1059 we should create an extra move from the raw material location to the location of the routing, which
1060 precedes the consumption line (chained). The picking type depends on the warehouse in which this happens
1061 and the type of locations.
1063 loc_obj = self.pool.get("stock.location")
1064 stock_move = self.pool.get('stock.move')
1065 type_obj = self.pool.get('stock.picking.type')
1066 # Need to search for a picking type
1067 move = stock_move.browse(cr, uid, move_id, context=context)
1068 src_loc = loc_obj.browse(cr, uid, source_location_id, context=context)
1069 dest_loc = loc_obj.browse(cr, uid, dest_location_id, context=context)
1070 code = stock_move.get_code_from_locs(cr, uid, move, src_loc, dest_loc, context=context)
1071 if code == 'outgoing':
1074 check_loc = dest_loc
1075 wh = loc_obj.get_warehouse(cr, uid, check_loc, context=context)
1076 domain = [('code', '=', code)]
1078 domain += [('warehouse_id', '=', wh)]
1079 types = type_obj.search(cr, uid, domain, context=context)
1080 move = stock_move.copy(cr, uid, move_id, default = {
1081 'location_id': source_location_id,
1082 'location_dest_id': dest_location_id,
1083 'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context),
1084 'raw_material_production_id': False,
1085 'move_dest_id': move_id,
1086 'picking_type_id': types and types[0] or False,
1090 def _make_consume_line_from_data(self, cr, uid, production, product, uom_id, qty, uos_id, uos_qty, context=None):
1091 stock_move = self.pool.get('stock.move')
1092 loc_obj = self.pool.get('stock.location')
1093 # Internal shipment is created for Stockable and Consumer Products
1094 if product.type not in ('product', 'consu'):
1096 # Take routing location as a Source Location.
1097 source_location_id = production.location_src_id.id
1098 prod_location_id = source_location_id
1100 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:
1101 source_location_id = production.bom_id.routing_id.location_id.id
1104 destination_location_id = production.product_id.property_stock_production.id
1105 move_id = stock_move.create(cr, uid, {
1106 'name': production.name,
1107 'date': production.date_planned,
1108 'product_id': product.id,
1109 'product_uom_qty': qty,
1110 'product_uom': uom_id,
1111 'product_uos_qty': uos_id and uos_qty or False,
1112 'product_uos': uos_id or False,
1113 'location_id': source_location_id,
1114 'location_dest_id': destination_location_id,
1115 'company_id': production.company_id.id,
1116 '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
1117 'raw_material_production_id': production.id,
1118 #this saves us a browse in create()
1119 'price_unit': product.standard_price,
1120 'origin': production.name,
1121 'warehouse_id': loc_obj.get_warehouse(cr, uid, production.location_src_id, context=context),
1125 prev_move = self._create_previous_move(cr, uid, move_id, product, prod_location_id, source_location_id, context=context)
1126 stock_move.action_confirm(cr, uid, [prev_move], context=context)
1129 def _make_production_consume_line(self, cr, uid, line, context=None):
1130 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)
1133 def _make_service_procurement(self, cr, uid, line, context=None):
1134 prod_obj = self.pool.get('product.product')
1135 if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
1137 'name': line.production_id.name,
1138 'origin': line.production_id.name,
1139 'company_id': line.production_id.company_id.id,
1140 'date_planned': line.production_id.date_planned,
1141 'product_id': line.product_id.id,
1142 'product_qty': line.product_qty,
1143 'product_uom': line.product_uom.id,
1144 'product_uos_qty': line.product_uos_qty,
1145 'product_uos': line.product_uos.id,
1147 proc_obj = self.pool.get("procurement.order")
1148 proc = proc_obj.create(cr, uid, vals, context=context)
1149 proc_obj.run(cr, uid, [proc], context=context)
1152 def action_confirm(self, cr, uid, ids, context=None):
1153 """ Confirms production order.
1154 @return: Newly generated Shipment Id.
1156 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)])
1157 self.action_compute(cr, uid, uncompute_ids, context=context)
1158 for production in self.browse(cr, uid, ids, context=context):
1159 self._make_production_produce_line(cr, uid, production, context=context)
1162 for line in production.product_lines:
1163 if line.product_id.type != 'service':
1164 stock_move_id = self._make_production_consume_line(cr, uid, line, context=context)
1165 stock_moves.append(stock_move_id)
1167 self._make_service_procurement(cr, uid, line, context=context)
1169 self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
1170 production.write({'state': 'confirmed'}, context=context)
1173 def action_assign(self, cr, uid, ids, context=None):
1175 Checks the availability on the consume lines of the production order
1177 from openerp import workflow
1178 move_obj = self.pool.get("stock.move")
1179 for production in self.browse(cr, uid, ids, context=context):
1180 move_obj.action_assign(cr, uid, [x.id for x in production.move_lines], context=context)
1181 if self.pool.get('mrp.production').test_ready(cr, uid, [production.id]):
1182 workflow.trg_validate(uid, 'mrp.production', production.id, 'moves_ready', cr)
1185 def force_production(self, cr, uid, ids, *args):
1186 """ Assigns products.
1187 @param *args: Arguments
1190 from openerp import workflow
1191 move_obj = self.pool.get('stock.move')
1192 for order in self.browse(cr, uid, ids):
1193 move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1194 if self.pool.get('mrp.production').test_ready(cr, uid, [order.id]):
1195 workflow.trg_validate(uid, 'mrp.production', order.id, 'moves_ready', cr)
1199 class mrp_production_workcenter_line(osv.osv):
1200 _name = 'mrp.production.workcenter.line'
1201 _description = 'Work Order'
1203 _inherit = ['mail.thread']
1206 'name': fields.char('Work Order', required=True),
1207 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1208 'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1209 'hour': fields.float('Number of Hours', digits=(16, 2)),
1210 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1211 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1212 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1215 'sequence': lambda *a: 1,
1216 'hour': lambda *a: 0,
1217 'cycle': lambda *a: 0,
1220 class mrp_production_product_line(osv.osv):
1221 _name = 'mrp.production.product.line'
1222 _description = 'Production Scheduled Product'
1224 'name': fields.char('Name', required=True),
1225 'product_id': fields.many2one('product.product', 'Product', required=True),
1226 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1227 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1228 'product_uos_qty': fields.float('Product UOS Quantity'),
1229 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1230 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1233 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: