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 unlink(self, cr, uid, ids, context=None):
338 if self.pool['mrp.production'].search(cr, uid, [('bom_id', 'in', ids), ('state', 'not in', ['done', 'cancel'])], context=context):
339 raise osv.except_osv(_('Warning!'), _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.'))
340 return super(mrp_bom, self).unlink(cr, uid, ids, context=context)
342 def onchange_product_tmpl_id(self, cr, uid, ids, product_tmpl_id, product_qty=0, context=None):
343 """ Changes UoM and name if product_id changes.
344 @param product_id: Changed product_id
345 @return: Dictionary of changed values
349 prod = self.pool.get('product.template').browse(cr, uid, product_tmpl_id, context=context)
352 'product_uom': prod.uom_id.id,
356 class mrp_bom_line(osv.osv):
357 _name = 'mrp.bom.line'
360 def _get_child_bom_lines(self, cr, uid, ids, field_name, arg, context=None):
361 """If the BOM line refers to a BOM, return the ids of the child BOM lines"""
362 bom_obj = self.pool['mrp.bom']
364 for bom_line in self.browse(cr, uid, ids, context=context):
365 bom_id = bom_obj._bom_find(cr, uid,
366 product_tmpl_id=bom_line.product_id.product_tmpl_id.id,
367 product_id=bom_line.product_id.id, context=context)
369 child_bom = bom_obj.browse(cr, uid, bom_id, context=context)
370 res[bom_line.id] = [x.id for x in child_bom.bom_line_ids]
372 res[bom_line.id] = False
376 'type': fields.selection([('normal', 'Normal'), ('phantom', 'Phantom')], 'BoM Line Type', required=True,
377 help="Phantom: this product line will not appear in the raw materials of manufacturing orders,"
378 "it will be directly replaced by the raw materials of its own BoM, without triggering"
379 "an extra manufacturing order."),
380 'product_id': fields.many2one('product.product', 'Product', required=True),
381 'product_uos_qty': fields.float('Product UOS Qty'),
382 'product_uos': fields.many2one('product.uom', 'Product UOS', help="Product UOS (Unit of Sale) is the unit of measurement for the invoicing and promotion of stock."),
383 'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
384 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True,
385 help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"),
387 'date_start': fields.date('Valid From', help="Validity of component. Keep empty if it's always valid."),
388 'date_stop': fields.date('Valid Until', help="Validity of component. Keep empty if it's always valid."),
389 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying."),
390 'routing_id': fields.many2one('mrp.routing', 'Routing', help="The list of operations (list of work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
391 'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
392 'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
393 'property_ids': fields.many2many('mrp.property', string='Properties'), #Not used
395 'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True, required=True),
396 'attribute_value_ids': fields.many2many('product.attribute.value', string='Variants', help="BOM Product Variants needed form apply this line."),
397 'child_line_ids': fields.function(_get_child_bom_lines, relation="mrp.bom.line", string="BOM lines of the referred bom", type="one2many")
400 def _get_uom_id(self, cr, uid, *args):
401 return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
403 'product_qty': lambda *a: 1.0,
404 'product_efficiency': lambda *a: 1.0,
405 'product_rounding': lambda *a: 0.0,
406 'type': lambda *a: 'normal',
407 'product_uom': _get_uom_id,
410 ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
411 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
414 def create(self, cr, uid, values, context=None):
417 product_obj = self.pool.get('product.product')
418 if 'product_id' in values and not 'product_uom' in values:
419 values['product_uom'] = product_obj.browse(cr, uid, values.get('product_id'), context=context).uom_id.id
420 return super(mrp_bom_line, self).create(cr, uid, values, context=context)
422 def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
424 if not product_uom or not product_id:
426 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
427 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
428 if uom.category_id.id != product.uom_id.category_id.id:
429 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
430 res['value'].update({'product_uom': product.uom_id.id})
433 def onchange_product_id(self, cr, uid, ids, product_id, product_qty=0, context=None):
434 """ Changes UoM if product_id changes.
435 @param product_id: Changed product_id
436 @return: Dictionary of changed values
440 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
442 'product_uom': prod.uom_id.id,
443 'product_uos_qty': 0,
447 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
448 res['value']['product_uos'] = prod.uos_id.id
451 class mrp_production(osv.osv):
453 Production Orders / Manufacturing Orders
455 _name = 'mrp.production'
456 _description = 'Manufacturing Order'
457 _date_name = 'date_planned'
458 _inherit = ['mail.thread', 'ir.needaction_mixin']
460 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
461 """ Calculates total hours and total no. of cycles for a production order.
462 @param prop: Name of field.
464 @return: Dictionary of values.
467 for prod in self.browse(cr, uid, ids, context=context):
472 for wc in prod.workcenter_lines:
473 result[prod.id]['hour_total'] += wc.hour
474 result[prod.id]['cycle_total'] += wc.cycle
477 def _src_id_default(self, cr, uid, ids, context=None):
479 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
480 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
481 except (orm.except_orm, ValueError):
485 def _dest_id_default(self, cr, uid, ids, context=None):
487 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
488 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
489 except (orm.except_orm, ValueError):
493 def _get_progress(self, cr, uid, ids, name, arg, context=None):
494 """ Return product quantity percentage """
495 result = dict.fromkeys(ids, 100)
496 for mrp_production in self.browse(cr, uid, ids, context=context):
497 if mrp_production.product_qty:
499 for move in mrp_production.move_created_ids2:
500 if not move.scrapped and move.product_id == mrp_production.product_id:
501 done += move.product_qty
502 result[mrp_production.id] = done / mrp_production.product_qty * 100
505 def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
506 """ Test whether all the consume lines are assigned """
508 for production in self.browse(cr, uid, ids, context=context):
509 res[production.id] = True
510 states = [x.state != 'assigned' for x in production.move_lines if x]
511 if any(states) or len(states) == 0: #When no moves, ready_production will be False, but test_ready will pass
512 res[production.id] = False
515 def _mrp_from_move(self, cr, uid, ids, context=None):
518 for move in self.browse(cr, uid, ids, context=context):
519 res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
523 'name': fields.char('Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
524 'origin': fields.char('Source Document', readonly=True, states={'draft': [('readonly', False)]},
525 help="Reference of the document that generated this production order request.", copy=False),
526 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
527 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
529 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]},
530 domain=[('type','!=','service')]),
531 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
532 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
533 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
534 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
535 'progress': fields.function(_get_progress, type='float',
536 string='Production progress'),
538 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
539 readonly=True, states={'draft': [('readonly', False)]},
540 help="Location where the system will look for components."),
541 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
542 readonly=True, states={'draft': [('readonly', False)]},
543 help="Location where the system will stock the finished products."),
544 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
545 'date_start': fields.datetime('Start Date', select=True, readonly=True, copy=False),
546 'date_finished': fields.datetime('End Date', select=True, readonly=True, copy=False),
547 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', readonly=True, states={'draft': [('readonly', False)]},
548 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
549 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
550 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."),
551 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True, copy=False),
552 'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
553 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
554 'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
555 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
556 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
557 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
558 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
559 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
560 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
562 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
563 readonly=True, states={'draft': [('readonly', False)]}),
564 'state': fields.selection(
565 [('draft', 'New'), ('cancel', 'Cancelled'), ('confirmed', 'Awaiting Raw Materials'),
566 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
567 string='Status', readonly=True,
568 track_visibility='onchange', copy=False,
569 help="When the production order is created the status is set to 'Draft'.\n\
570 If the order is confirmed the status is set to 'Waiting Goods'.\n\
571 If any exceptions are there, the status is set to 'Picking Exception'.\n\
572 If the stock is available then the status is set to 'Ready to Produce'.\n\
573 When the production gets started then the status is set to 'In Production'.\n\
574 When the production is over, the status is set to 'Done'."),
575 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
576 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
577 'user_id': fields.many2one('res.users', 'Responsible'),
578 'company_id': fields.many2one('res.company', 'Company', required=True),
579 'ready_production': fields.function(_moves_assigned, type='boolean', string="Ready for production", store={'stock.move': (_mrp_from_move, ['state'], 10)}),
583 'priority': lambda *a: '1',
584 'state': lambda *a: 'draft',
585 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
586 'product_qty': lambda *a: 1.0,
587 'user_id': lambda self, cr, uid, c: uid,
588 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
589 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
590 'location_src_id': _src_id_default,
591 'location_dest_id': _dest_id_default
595 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
598 _order = 'priority desc, date_planned asc'
600 def _check_qty(self, cr, uid, ids, context=None):
601 for order in self.browse(cr, uid, ids, context=context):
602 if order.product_qty <= 0:
607 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
610 def create(self, cr, uid, values, context=None):
613 product_obj = self.pool.get('product.product')
614 if 'product_id' in values and not 'product_uom' in values:
615 values['product_uom'] = product_obj.browse(cr, uid, values.get('product_id'), context=context).uom_id.id
616 return super(mrp_production, self).create(cr, uid, values, context=context)
618 def unlink(self, cr, uid, ids, context=None):
619 for production in self.browse(cr, uid, ids, context=context):
620 if production.state not in ('draft', 'cancel'):
621 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
622 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
624 def location_id_change(self, cr, uid, ids, src, dest, context=None):
625 """ Changes destination location if source location is changed.
626 @param src: Source location id.
627 @param dest: Destination location id.
628 @return: Dictionary of values.
633 return {'value': {'location_dest_id': src}}
636 def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
637 """ Finds UoM of changed product.
638 @param product_id: Id of changed product.
639 @return: Dictionary of values.
644 'product_uom': False,
647 'product_uos_qty': 0,
650 bom_obj = self.pool.get('mrp.bom')
651 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
652 bom_id = bom_obj._bom_find(cr, uid, product_id=product.id, properties=[], context=context)
655 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
656 routing_id = bom_point.routing_id.id or False
657 product_uom_id = product.uom_id and product.uom_id.id or False
658 result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
659 if product.uos_id.id:
660 result['value']['product_uos_qty'] = product_qty * product.uos_coeff
661 result['value']['product_uos'] = product.uos_id.id
664 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
665 """ Finds routing for changed BoM.
666 @param product: Id of product.
667 @return: Dictionary of values.
673 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
674 routing_id = bom_point.routing_id.id or False
676 'routing_id': routing_id
678 return {'value': result}
681 def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
682 """ Compute product_lines and workcenter_lines from BoM structure
683 @return: product_lines
685 if properties is None:
688 bom_obj = self.pool.get('mrp.bom')
689 uom_obj = self.pool.get('product.uom')
690 prod_line_obj = self.pool.get('mrp.production.product.line')
691 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
692 for production in self.browse(cr, uid, ids, context=context):
693 #unlink product_lines
694 prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
695 #unlink workcenter_lines
696 workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
697 # search BoM structure and route
698 bom_point = production.bom_id
699 bom_id = production.bom_id.id
701 bom_id = bom_obj._bom_find(cr, uid, product_id=production.product_id.id, properties=properties, context=context)
703 bom_point = bom_obj.browse(cr, uid, bom_id)
704 routing_id = bom_point.routing_id.id or False
705 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
708 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
710 # get components and workcenter_lines from BoM structure
711 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
712 # product_lines, workcenter_lines
713 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)
715 # reset product_lines in production order
717 line['production_id'] = production.id
718 prod_line_obj.create(cr, uid, line)
720 #reset workcenter_lines in production order
721 for line in results2:
722 line['production_id'] = production.id
723 workcenter_line_obj.create(cr, uid, line)
726 def action_compute(self, cr, uid, ids, properties=None, context=None):
727 """ Computes bills of material of a product.
728 @param properties: List containing dictionaries of properties.
729 @return: No. of products.
731 return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
733 def action_cancel(self, cr, uid, ids, context=None):
734 """ Cancels the production order and related stock moves.
739 move_obj = self.pool.get('stock.move')
740 for production in self.browse(cr, uid, ids, context=context):
741 if production.move_created_ids:
742 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
743 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
744 self.write(cr, uid, ids, {'state': 'cancel'})
745 # Put related procurements in exception
746 proc_obj = self.pool.get("procurement.order")
747 procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
749 proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
752 def action_ready(self, cr, uid, ids, context=None):
753 """ Changes the production state to Ready and location id of stock move.
756 move_obj = self.pool.get('stock.move')
757 self.write(cr, uid, ids, {'state': 'ready'})
759 for production in self.browse(cr, uid, ids, context=context):
760 if not production.move_created_ids:
761 self._make_production_produce_line(cr, uid, production, context=context)
763 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
764 move_obj.write(cr, uid, [production.move_prod_id.id],
765 {'location_id': production.location_dest_id.id})
768 def action_production_end(self, cr, uid, ids, context=None):
769 """ Changes production state to Finish and writes finished date.
772 for production in self.browse(cr, uid, ids):
773 self._costs_generate(cr, uid, production)
774 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
775 # Check related procurements
776 proc_obj = self.pool.get("procurement.order")
777 procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
778 proc_obj.check(cr, uid, procs, context=context)
781 def test_production_done(self, cr, uid, ids):
782 """ Tests whether production is done or not.
783 @return: True or False
786 for production in self.browse(cr, uid, ids):
787 if production.move_lines:
790 if production.move_created_ids:
794 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
795 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
796 it's always equal to the quantity encoded in the production order or the production wizard, but if the
797 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
799 :param production_id: ID of the mrp.order
800 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
801 :return: The factor to apply to the quantity that we should produce for the given production order.
805 def _get_produced_qty(self, cr, uid, production, context=None):
806 ''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
809 for produced_product in production.move_created_ids2:
810 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
812 produced_qty += produced_product.product_qty
815 def _get_consumed_data(self, cr, uid, production, context=None):
816 ''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
819 # Calculate already consumed qtys
820 for consumed in production.move_lines2:
821 if consumed.scrapped:
823 if not consumed_data.get(consumed.product_id.id, False):
824 consumed_data[consumed.product_id.id] = 0
825 consumed_data[consumed.product_id.id] += consumed.product_qty
828 def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
830 Calculates the quantity still needed to produce an extra number of products
831 product_qty is in the uom of the product
833 quant_obj = self.pool.get("stock.quant")
834 uom_obj = self.pool.get("product.uom")
835 produced_qty = self._get_produced_qty(cr, uid, production, context=context)
836 consumed_data = self._get_consumed_data(cr, uid, production, context=context)
838 #In case no product_qty is given, take the remaining qty to produce for the given production
840 product_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id) - produced_qty
841 production_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id)
844 for scheduled in production.product_lines:
845 if scheduled.product_id.type == 'service':
847 qty = uom_obj._compute_qty(cr, uid, scheduled.product_uom.id, scheduled.product_qty, scheduled.product_id.uom_id.id)
848 if scheduled_qty.get(scheduled.product_id.id):
849 scheduled_qty[scheduled.product_id.id] += qty
851 scheduled_qty[scheduled.product_id.id] = qty
853 # Find product qty to be consumed and consume it
854 for product_id in scheduled_qty.keys():
856 consumed_qty = consumed_data.get(product_id, 0.0)
858 # qty available for consume and produce
859 sched_product_qty = scheduled_qty[product_id]
860 qty_avail = sched_product_qty - consumed_qty
862 # there will be nothing to consume for this raw material
865 if not dicts.get(product_id):
866 dicts[product_id] = {}
868 # total qty of consumed product we need after this consumption
869 if product_qty + produced_qty <= production_qty:
870 total_consume = ((product_qty + produced_qty) * sched_product_qty / production_qty)
872 total_consume = sched_product_qty
873 qty = total_consume - consumed_qty
875 # Search for quants related to this related move
876 for move in production.move_lines:
879 if move.product_id.id != product_id:
882 q = min(move.product_qty, qty)
883 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, q, domain=[('qty', '>', 0.0)],
884 prefered_domain_list=[[('reservation_id', '=', move.id)]], context=context)
885 for quant, quant_qty in quants:
887 lot_id = quant.lot_id.id
888 if not product_id in dicts.keys():
889 dicts[product_id] = {lot_id: quant_qty}
890 elif lot_id in dicts[product_id].keys():
891 dicts[product_id][lot_id] += quant_qty
893 dicts[product_id][lot_id] = quant_qty
896 if dicts[product_id].get(False):
897 dicts[product_id][False] += qty
899 dicts[product_id][False] = qty
902 for prod in dicts.keys():
903 for lot, qty in dicts[prod].items():
904 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
907 def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
908 """ To produce final product based on production mode (consume/consume&produce).
909 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
910 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
911 and stock move lines of final product will be also done/produced.
912 @param production_id: the ID of mrp.production object
913 @param production_qty: specify qty to produce in the uom of the production order
914 @param production_mode: specify production mode (consume/consume&produce).
915 @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
918 stock_mov_obj = self.pool.get('stock.move')
919 uom_obj = self.pool.get("product.uom")
920 production = self.browse(cr, uid, production_id, context=context)
921 production_qty_uom = uom_obj._compute_qty(cr, uid, production.product_uom.id, production_qty, production.product_id.uom_id.id)
923 main_production_move = False
924 if production_mode == 'consume_produce':
925 # To produce remaining qty of final product
926 produced_products = {}
927 for produced_product in production.move_created_ids2:
928 if produced_product.scrapped:
930 if not produced_products.get(produced_product.product_id.id, False):
931 produced_products[produced_product.product_id.id] = 0
932 produced_products[produced_product.product_id.id] += produced_product.product_qty
934 for produce_product in production.move_created_ids:
935 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
938 lot_id = wiz.lot_id.id
939 qty = min(subproduct_factor * production_qty_uom, produce_product.product_qty) #Needed when producing more than maximum quantity
940 new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], qty,
941 location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
942 stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
943 remaining_qty = subproduct_factor * production_qty_uom - qty
944 if remaining_qty: # In case you need to make more than planned
945 #consumed more in wizard than previously planned
946 extra_move_id = stock_mov_obj.copy(cr, uid, produce_product.id, default={'state': 'confirmed',
947 'product_uom_qty': remaining_qty,
948 'production_id': production_id}, context=context)
950 stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
952 if produce_product.product_id.id == production.product_id.id:
953 main_production_move = produce_product.id
955 if production_mode in ['consume', 'consume_produce']:
958 for cons in wiz.consume_lines:
959 consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
961 consume_lines = self._calculate_qty(cr, uid, production, production_qty_uom, context=context)
962 for consume in consume_lines:
963 remaining_qty = consume['product_qty']
964 for raw_material_line in production.move_lines:
965 if remaining_qty <= 0:
967 if consume['product_id'] != raw_material_line.product_id.id:
969 consumed_qty = min(remaining_qty, raw_material_line.product_qty)
970 stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id,
971 restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
972 remaining_qty -= consumed_qty
974 #consumed more in wizard than previously planned
975 product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
976 extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
978 if consume['lot_id']:
979 stock_mov_obj.write(cr, uid, [extra_move_id], {'restrict_lot_id': consume['lot_id']}, context=context)
980 stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
982 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
983 self.signal_workflow(cr, uid, [production_id], 'button_produce_done')
986 def _costs_generate(self, cr, uid, production):
987 """ Calculates total costs at the end of the production.
988 @param production: Id of production order.
989 @return: Calculated amount.
992 analytic_line_obj = self.pool.get('account.analytic.line')
993 for wc_line in production.workcenter_lines:
994 wc = wc_line.workcenter_id
995 if wc.costs_journal_id and wc.costs_general_account_id:
997 value = wc_line.hour * wc.costs_hour
998 account = wc.costs_hour_account_id.id
999 if value and account:
1001 # we user SUPERUSER_ID as we do not garantee an mrp user
1002 # has access to account analytic lines but still should be
1003 # able to produce orders
1004 analytic_line_obj.create(cr, SUPERUSER_ID, {
1005 'name': wc_line.name + ' (H)',
1007 'account_id': account,
1008 'general_account_id': wc.costs_general_account_id.id,
1009 'journal_id': wc.costs_journal_id.id,
1011 'product_id': wc.product_id.id,
1012 'unit_amount': wc_line.hour,
1013 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
1016 value = wc_line.cycle * wc.costs_cycle
1017 account = wc.costs_cycle_account_id.id
1018 if value and account:
1020 analytic_line_obj.create(cr, SUPERUSER_ID, {
1021 'name': wc_line.name + ' (C)',
1023 'account_id': account,
1024 'general_account_id': wc.costs_general_account_id.id,
1025 'journal_id': wc.costs_journal_id.id,
1027 'product_id': wc.product_id.id,
1028 'unit_amount': wc_line.cycle,
1029 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
1033 def action_in_production(self, cr, uid, ids, context=None):
1034 """ Changes state to In Production and writes starting date.
1037 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
1039 def consume_lines_get(self, cr, uid, ids, *args):
1041 for order in self.browse(cr, uid, ids, context={}):
1042 res += [x.id for x in order.move_lines]
1045 def test_ready(self, cr, uid, ids):
1047 for production in self.browse(cr, uid, ids):
1048 if production.move_lines and not production.ready_production:
1054 def _make_production_produce_line(self, cr, uid, production, context=None):
1055 stock_move = self.pool.get('stock.move')
1056 proc_obj = self.pool.get('procurement.order')
1057 source_location_id = production.product_id.property_stock_production.id
1058 destination_location_id = production.location_dest_id.id
1059 procs = proc_obj.search(cr, uid, [('production_id', '=', production.id)], context=context)
1060 procurement_id = procs and procs[0] or False
1062 'name': production.name,
1063 'date': production.date_planned,
1064 'product_id': production.product_id.id,
1065 'product_uom': production.product_uom.id,
1066 'product_uom_qty': production.product_qty,
1067 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1068 'product_uos': production.product_uos and production.product_uos.id or False,
1069 'location_id': source_location_id,
1070 'location_dest_id': destination_location_id,
1071 'move_dest_id': production.move_prod_id.id,
1072 'procurement_id': procurement_id,
1073 'company_id': production.company_id.id,
1074 'production_id': production.id,
1075 'origin': production.name,
1077 move_id = stock_move.create(cr, uid, data, context=context)
1078 #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1079 #is 1 element long, so we can take the first.
1080 return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1082 def _get_raw_material_procure_method(self, cr, uid, product, context=None):
1083 '''This method returns the procure_method to use when creating the stock move for the production raw materials'''
1084 warehouse_obj = self.pool['stock.warehouse']
1086 mto_route = warehouse_obj._get_mto_route(cr, uid, context=context)
1088 return "make_to_stock"
1089 routes = product.route_ids + product.categ_id.total_route_ids
1090 if mto_route in [x.id for x in routes]:
1091 return "make_to_order"
1092 return "make_to_stock"
1094 def _create_previous_move(self, cr, uid, move_id, product, source_location_id, dest_location_id, context=None):
1096 When the routing gives a different location than the raw material location of the production order,
1097 we should create an extra move from the raw material location to the location of the routing, which
1098 precedes the consumption line (chained). The picking type depends on the warehouse in which this happens
1099 and the type of locations.
1101 loc_obj = self.pool.get("stock.location")
1102 stock_move = self.pool.get('stock.move')
1103 type_obj = self.pool.get('stock.picking.type')
1104 # Need to search for a picking type
1105 move = stock_move.browse(cr, uid, move_id, context=context)
1106 src_loc = loc_obj.browse(cr, uid, source_location_id, context=context)
1107 dest_loc = loc_obj.browse(cr, uid, dest_location_id, context=context)
1108 code = stock_move.get_code_from_locs(cr, uid, move, src_loc, dest_loc, context=context)
1109 if code == 'outgoing':
1112 check_loc = dest_loc
1113 wh = loc_obj.get_warehouse(cr, uid, check_loc, context=context)
1114 domain = [('code', '=', code)]
1116 domain += [('warehouse_id', '=', wh)]
1117 types = type_obj.search(cr, uid, domain, context=context)
1118 move = stock_move.copy(cr, uid, move_id, default = {
1119 'location_id': source_location_id,
1120 'location_dest_id': dest_location_id,
1121 'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context),
1122 'raw_material_production_id': False,
1123 'move_dest_id': move_id,
1124 'picking_type_id': types and types[0] or False,
1128 def _make_consume_line_from_data(self, cr, uid, production, product, uom_id, qty, uos_id, uos_qty, context=None):
1129 stock_move = self.pool.get('stock.move')
1130 loc_obj = self.pool.get('stock.location')
1131 # Internal shipment is created for Stockable and Consumer Products
1132 if product.type not in ('product', 'consu'):
1134 # Take routing location as a Source Location.
1135 source_location_id = production.location_src_id.id
1136 prod_location_id = source_location_id
1138 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:
1139 source_location_id = production.bom_id.routing_id.location_id.id
1142 destination_location_id = production.product_id.property_stock_production.id
1143 move_id = stock_move.create(cr, uid, {
1144 'name': production.name,
1145 'date': production.date_planned,
1146 'product_id': product.id,
1147 'product_uom_qty': qty,
1148 'product_uom': uom_id,
1149 'product_uos_qty': uos_id and uos_qty or False,
1150 'product_uos': uos_id or False,
1151 'location_id': source_location_id,
1152 'location_dest_id': destination_location_id,
1153 'company_id': production.company_id.id,
1154 '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
1155 'raw_material_production_id': production.id,
1156 #this saves us a browse in create()
1157 'price_unit': product.standard_price,
1158 'origin': production.name,
1159 'warehouse_id': loc_obj.get_warehouse(cr, uid, production.location_src_id, context=context),
1163 prev_move = self._create_previous_move(cr, uid, move_id, product, prod_location_id, source_location_id, context=context)
1164 stock_move.action_confirm(cr, uid, [prev_move], context=context)
1167 def _make_production_consume_line(self, cr, uid, line, context=None):
1168 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)
1171 def _make_service_procurement(self, cr, uid, line, context=None):
1172 prod_obj = self.pool.get('product.product')
1173 if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
1175 'name': line.production_id.name,
1176 'origin': line.production_id.name,
1177 'company_id': line.production_id.company_id.id,
1178 'date_planned': line.production_id.date_planned,
1179 'product_id': line.product_id.id,
1180 'product_qty': line.product_qty,
1181 'product_uom': line.product_uom.id,
1182 'product_uos_qty': line.product_uos_qty,
1183 'product_uos': line.product_uos.id,
1185 proc_obj = self.pool.get("procurement.order")
1186 proc = proc_obj.create(cr, uid, vals, context=context)
1187 proc_obj.run(cr, uid, [proc], context=context)
1190 def action_confirm(self, cr, uid, ids, context=None):
1191 """ Confirms production order.
1192 @return: Newly generated Shipment Id.
1194 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)])
1195 self.action_compute(cr, uid, uncompute_ids, context=context)
1196 for production in self.browse(cr, uid, ids, context=context):
1197 self._make_production_produce_line(cr, uid, production, context=context)
1200 for line in production.product_lines:
1201 if line.product_id.type != 'service':
1202 stock_move_id = self._make_production_consume_line(cr, uid, line, context=context)
1203 stock_moves.append(stock_move_id)
1205 self._make_service_procurement(cr, uid, line, context=context)
1207 self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
1208 production.write({'state': 'confirmed'})
1211 def action_assign(self, cr, uid, ids, context=None):
1213 Checks the availability on the consume lines of the production order
1215 from openerp import workflow
1216 move_obj = self.pool.get("stock.move")
1217 for production in self.browse(cr, uid, ids, context=context):
1218 move_obj.action_assign(cr, uid, [x.id for x in production.move_lines], context=context)
1219 if self.pool.get('mrp.production').test_ready(cr, uid, [production.id]):
1220 workflow.trg_validate(uid, 'mrp.production', production.id, 'moves_ready', cr)
1223 def force_production(self, cr, uid, ids, *args):
1224 """ Assigns products.
1225 @param *args: Arguments
1228 from openerp import workflow
1229 move_obj = self.pool.get('stock.move')
1230 for order in self.browse(cr, uid, ids):
1231 move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1232 if self.pool.get('mrp.production').test_ready(cr, uid, [order.id]):
1233 workflow.trg_validate(uid, 'mrp.production', order.id, 'moves_ready', cr)
1237 class mrp_production_workcenter_line(osv.osv):
1238 _name = 'mrp.production.workcenter.line'
1239 _description = 'Work Order'
1241 _inherit = ['mail.thread']
1244 'name': fields.char('Work Order', required=True),
1245 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1246 'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1247 'hour': fields.float('Number of Hours', digits=(16, 2)),
1248 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1249 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1250 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1253 'sequence': lambda *a: 1,
1254 'hour': lambda *a: 0,
1255 'cycle': lambda *a: 0,
1258 class mrp_production_product_line(osv.osv):
1259 _name = 'mrp.production.product.line'
1260 _description = 'Production Scheduled Product'
1262 'name': fields.char('Name', required=True),
1263 'product_id': fields.many2one('product.product', 'Product', required=True),
1264 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1265 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1266 'product_uos_qty': fields.float('Product UOS Quantity'),
1267 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1268 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1271 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: