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', 'Normal'), ('phantom', 'Set')], '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 onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
416 if not product_uom or not product_id:
418 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
419 uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
420 if uom.category_id.id != product.uom_id.category_id.id:
421 res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
422 res['value'].update({'product_uom': product.uom_id.id})
425 def onchange_product_id(self, cr, uid, ids, product_id, product_qty=0, context=None):
426 """ Changes UoM if product_id changes.
427 @param product_id: Changed product_id
428 @return: Dictionary of changed values
432 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
434 'product_uom': prod.uom_id.id,
435 'product_uos_qty': 0,
439 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
440 res['value']['product_uos'] = prod.uos_id.id
443 class mrp_production(osv.osv):
445 Production Orders / Manufacturing Orders
447 _name = 'mrp.production'
448 _description = 'Manufacturing Order'
449 _date_name = 'date_planned'
450 _inherit = ['mail.thread', 'ir.needaction_mixin']
452 def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
453 """ Calculates total hours and total no. of cycles for a production order.
454 @param prop: Name of field.
456 @return: Dictionary of values.
459 for prod in self.browse(cr, uid, ids, context=context):
464 for wc in prod.workcenter_lines:
465 result[prod.id]['hour_total'] += wc.hour
466 result[prod.id]['cycle_total'] += wc.cycle
469 def _src_id_default(self, cr, uid, ids, context=None):
471 location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
472 self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
473 except (orm.except_orm, ValueError):
477 def _dest_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 _get_progress(self, cr, uid, ids, name, arg, context=None):
486 """ Return product quantity percentage """
487 result = dict.fromkeys(ids, 100)
488 for mrp_production in self.browse(cr, uid, ids, context=context):
489 if mrp_production.product_qty:
491 for move in mrp_production.move_created_ids2:
492 if not move.scrapped and move.product_id == mrp_production.product_id:
493 done += move.product_qty
494 result[mrp_production.id] = done / mrp_production.product_qty * 100
497 def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
498 """ Test whether all the consume lines are assigned """
500 for production in self.browse(cr, uid, ids, context=context):
501 res[production.id] = True
502 states = [x.state != 'assigned' for x in production.move_lines if x]
503 if any(states) or len(states) == 0: #When no moves, ready_production will be False, but test_ready will pass
504 res[production.id] = False
507 def _mrp_from_move(self, cr, uid, ids, context=None):
510 for move in self.browse(cr, uid, ids, context=context):
511 res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
515 'name': fields.char('Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
516 'origin': fields.char('Source Document', readonly=True, states={'draft': [('readonly', False)]},
517 help="Reference of the document that generated this production order request.", copy=False),
518 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
519 select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
521 'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]},
522 domain=[('type','!=','service')]),
523 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
524 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
525 'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
526 'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
527 'progress': fields.function(_get_progress, type='float',
528 string='Production progress'),
530 'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
531 readonly=True, states={'draft': [('readonly', False)]},
532 help="Location where the system will look for components."),
533 'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
534 readonly=True, states={'draft': [('readonly', False)]},
535 help="Location where the system will stock the finished products."),
536 'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
537 'date_start': fields.datetime('Start Date', select=True, readonly=True, copy=False),
538 'date_finished': fields.datetime('End Date', select=True, readonly=True, copy=False),
539 'bom_id': fields.many2one('mrp.bom', 'Bill of Material', readonly=True, states={'draft': [('readonly', False)]},
540 help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
541 'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
542 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."),
543 'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True, copy=False),
544 'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
545 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
546 'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
547 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
548 'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
549 domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
550 'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
551 domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
552 'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
554 'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
555 readonly=True, states={'draft': [('readonly', False)]}),
556 'state': fields.selection(
557 [('draft', 'New'), ('cancel', 'Cancelled'), ('confirmed', 'Awaiting Raw Materials'),
558 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
559 string='Status', readonly=True,
560 track_visibility='onchange', copy=False,
561 help="When the production order is created the status is set to 'Draft'.\n\
562 If the order is confirmed the status is set to 'Waiting Goods'.\n\
563 If any exceptions are there, the status is set to 'Picking Exception'.\n\
564 If the stock is available then the status is set to 'Ready to Produce'.\n\
565 When the production gets started then the status is set to 'In Production'.\n\
566 When the production is over, the status is set to 'Done'."),
567 'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
568 'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
569 'user_id': fields.many2one('res.users', 'Responsible'),
570 'company_id': fields.many2one('res.company', 'Company', required=True),
571 'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
575 'priority': lambda *a: '1',
576 'state': lambda *a: 'draft',
577 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
578 'product_qty': lambda *a: 1.0,
579 'user_id': lambda self, cr, uid, c: uid,
580 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
581 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
582 'location_src_id': _src_id_default,
583 'location_dest_id': _dest_id_default
587 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
590 _order = 'priority desc, date_planned asc'
592 def _check_qty(self, cr, uid, ids, context=None):
593 for order in self.browse(cr, uid, ids, context=context):
594 if order.product_qty <= 0:
599 (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
602 def unlink(self, cr, uid, ids, context=None):
603 for production in self.browse(cr, uid, ids, context=context):
604 if production.state not in ('draft', 'cancel'):
605 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
606 return super(mrp_production, self).unlink(cr, uid, ids, context=context)
608 def location_id_change(self, cr, uid, ids, src, dest, context=None):
609 """ Changes destination location if source location is changed.
610 @param src: Source location id.
611 @param dest: Destination location id.
612 @return: Dictionary of values.
617 return {'value': {'location_dest_id': src}}
620 def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
621 """ Finds UoM of changed product.
622 @param product_id: Id of changed product.
623 @return: Dictionary of values.
628 'product_uom': False,
631 'product_uos_qty': 0,
634 bom_obj = self.pool.get('mrp.bom')
635 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
636 bom_id = bom_obj._bom_find(cr, uid, product_id=product.id, properties=[], context=context)
639 bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
640 routing_id = bom_point.routing_id.id or False
641 product_uom_id = product.uom_id and product.uom_id.id or False
642 result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
643 if product.uos_id.id:
644 result['value']['product_uos_qty'] = product_qty * product.uos_coeff
645 result['value']['product_uos'] = product.uos_id.id
648 def bom_id_change(self, cr, uid, ids, bom_id, context=None):
649 """ Finds routing for changed BoM.
650 @param product: Id of product.
651 @return: Dictionary of values.
657 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
658 routing_id = bom_point.routing_id.id or False
660 'routing_id': routing_id
662 return {'value': result}
665 def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
666 """ Compute product_lines and workcenter_lines from BoM structure
667 @return: product_lines
669 if properties is None:
672 bom_obj = self.pool.get('mrp.bom')
673 uom_obj = self.pool.get('product.uom')
674 prod_line_obj = self.pool.get('mrp.production.product.line')
675 workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
676 for production in self.browse(cr, uid, ids, context=context):
677 #unlink product_lines
678 prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
679 #unlink workcenter_lines
680 workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
681 # search BoM structure and route
682 bom_point = production.bom_id
683 bom_id = production.bom_id.id
685 bom_id = bom_obj._bom_find(cr, uid, product_id=production.product_id.id, properties=properties, context=context)
687 bom_point = bom_obj.browse(cr, uid, bom_id)
688 routing_id = bom_point.routing_id.id or False
689 self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
692 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
694 # get components and workcenter_lines from BoM structure
695 factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
696 # product_lines, workcenter_lines
697 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)
699 # reset product_lines in production order
701 line['production_id'] = production.id
702 prod_line_obj.create(cr, uid, line)
704 #reset workcenter_lines in production order
705 for line in results2:
706 line['production_id'] = production.id
707 workcenter_line_obj.create(cr, uid, line)
710 def action_compute(self, cr, uid, ids, properties=None, context=None):
711 """ Computes bills of material of a product.
712 @param properties: List containing dictionaries of properties.
713 @return: No. of products.
715 return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
717 def action_cancel(self, cr, uid, ids, context=None):
718 """ Cancels the production order and related stock moves.
723 move_obj = self.pool.get('stock.move')
724 for production in self.browse(cr, uid, ids, context=context):
725 if production.move_created_ids:
726 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
727 move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
728 self.write(cr, uid, ids, {'state': 'cancel'})
729 # Put related procurements in exception
730 proc_obj = self.pool.get("procurement.order")
731 procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
733 proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context)
736 def action_ready(self, cr, uid, ids, context=None):
737 """ Changes the production state to Ready and location id of stock move.
740 move_obj = self.pool.get('stock.move')
741 self.write(cr, uid, ids, {'state': 'ready'})
743 for production in self.browse(cr, uid, ids, context=context):
744 if not production.move_created_ids:
745 self._make_production_produce_line(cr, uid, production, context=context)
747 if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
748 move_obj.write(cr, uid, [production.move_prod_id.id],
749 {'location_id': production.location_dest_id.id})
752 def action_production_end(self, cr, uid, ids, context=None):
753 """ Changes production state to Finish and writes finished date.
756 for production in self.browse(cr, uid, ids):
757 self._costs_generate(cr, uid, production)
758 write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
759 # Check related procurements
760 proc_obj = self.pool.get("procurement.order")
761 procs = proc_obj.search(cr, uid, [('production_id', 'in', ids)], context=context)
762 proc_obj.check(cr, uid, procs, context=context)
765 def test_production_done(self, cr, uid, ids):
766 """ Tests whether production is done or not.
767 @return: True or False
770 for production in self.browse(cr, uid, ids):
771 if production.move_lines:
774 if production.move_created_ids:
778 def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
779 """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
780 it's always equal to the quantity encoded in the production order or the production wizard, but if the
781 module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
783 :param production_id: ID of the mrp.order
784 :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
785 :return: The factor to apply to the quantity that we should produce for the given production order.
789 def _get_produced_qty(self, cr, uid, production, context=None):
790 ''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
793 for produced_product in production.move_created_ids2:
794 if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
796 produced_qty += produced_product.product_qty
799 def _get_consumed_data(self, cr, uid, production, context=None):
800 ''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
803 # Calculate already consumed qtys
804 for consumed in production.move_lines2:
805 if consumed.scrapped:
807 if not consumed_data.get(consumed.product_id.id, False):
808 consumed_data[consumed.product_id.id] = 0
809 consumed_data[consumed.product_id.id] += consumed.product_qty
812 def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
814 Calculates the quantity still needed to produce an extra number of products
815 product_qty is in the uom of the product
817 quant_obj = self.pool.get("stock.quant")
818 uom_obj = self.pool.get("product.uom")
819 produced_qty = self._get_produced_qty(cr, uid, production, context=context)
820 consumed_data = self._get_consumed_data(cr, uid, production, context=context)
822 #In case no product_qty is given, take the remaining qty to produce for the given production
824 product_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id) - produced_qty
825 production_qty = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.product_id.uom_id.id)
828 for scheduled in production.product_lines:
829 if scheduled.product_id.type == 'service':
831 qty = uom_obj._compute_qty(cr, uid, scheduled.product_uom.id, scheduled.product_qty, scheduled.product_id.uom_id.id)
832 if scheduled_qty.get(scheduled.product_id.id):
833 scheduled_qty[scheduled.product_id.id] += qty
835 scheduled_qty[scheduled.product_id.id] = qty
837 # Find product qty to be consumed and consume it
838 for product_id in scheduled_qty.keys():
840 consumed_qty = consumed_data.get(product_id, 0.0)
842 # qty available for consume and produce
843 sched_product_qty = scheduled_qty[product_id]
844 qty_avail = sched_product_qty - consumed_qty
846 # there will be nothing to consume for this raw material
849 if not dicts.get(product_id):
850 dicts[product_id] = {}
852 # total qty of consumed product we need after this consumption
853 if product_qty + produced_qty <= production_qty:
854 total_consume = ((product_qty + produced_qty) * sched_product_qty / production_qty)
856 total_consume = sched_product_qty
857 qty = total_consume - consumed_qty
859 # Search for quants related to this related move
860 for move in production.move_lines:
863 if move.product_id.id != product_id:
866 q = min(move.product_qty, qty)
867 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, q, domain=[('qty', '>', 0.0)],
868 prefered_domain_list=[[('reservation_id', '=', move.id)]], context=context)
869 for quant, quant_qty in quants:
871 lot_id = quant.lot_id.id
872 if not product_id in dicts.keys():
873 dicts[product_id] = {lot_id: quant_qty}
874 elif lot_id in dicts[product_id].keys():
875 dicts[product_id][lot_id] += quant_qty
877 dicts[product_id][lot_id] = quant_qty
880 if dicts[product_id].get(False):
881 dicts[product_id][False] += qty
883 dicts[product_id][False] = qty
886 for prod in dicts.keys():
887 for lot, qty in dicts[prod].items():
888 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
891 def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
892 """ To produce final product based on production mode (consume/consume&produce).
893 If Production mode is consume, all stock move lines of raw materials will be done/consumed.
894 If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
895 and stock move lines of final product will be also done/produced.
896 @param production_id: the ID of mrp.production object
897 @param production_qty: specify qty to produce in the uom of the production order
898 @param production_mode: specify production mode (consume/consume&produce).
899 @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
902 stock_mov_obj = self.pool.get('stock.move')
903 uom_obj = self.pool.get("product.uom")
904 production = self.browse(cr, uid, production_id, context=context)
905 production_qty_uom = uom_obj._compute_qty(cr, uid, production.product_uom.id, production_qty, production.product_id.uom_id.id)
907 main_production_move = False
908 if production_mode == 'consume_produce':
909 # To produce remaining qty of final product
910 produced_products = {}
911 for produced_product in production.move_created_ids2:
912 if produced_product.scrapped:
914 if not produced_products.get(produced_product.product_id.id, False):
915 produced_products[produced_product.product_id.id] = 0
916 produced_products[produced_product.product_id.id] += produced_product.product_qty
918 for produce_product in production.move_created_ids:
919 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
922 lot_id = wiz.lot_id.id
923 qty = min(subproduct_factor * production_qty_uom, produce_product.product_qty) #Needed when producing more than maximum quantity
924 new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], qty,
925 location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
926 stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
927 remaining_qty = subproduct_factor * production_qty_uom - qty
928 if remaining_qty: # In case you need to make more than planned
929 #consumed more in wizard than previously planned
930 extra_move_id = stock_mov_obj.copy(cr, uid, produce_product.id, default={'state': 'confirmed',
931 'product_uom_qty': remaining_qty,
932 'production_id': production_id}, context=context)
934 stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
936 if produce_product.product_id.id == production.product_id.id:
937 main_production_move = produce_product.id
939 if production_mode in ['consume', 'consume_produce']:
942 for cons in wiz.consume_lines:
943 consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
945 consume_lines = self._calculate_qty(cr, uid, production, production_qty_uom, context=context)
946 for consume in consume_lines:
947 remaining_qty = consume['product_qty']
948 for raw_material_line in production.move_lines:
949 if remaining_qty <= 0:
951 if consume['product_id'] != raw_material_line.product_id.id:
953 consumed_qty = min(remaining_qty, raw_material_line.product_qty)
954 stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id,
955 restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
956 remaining_qty -= consumed_qty
958 #consumed more in wizard than previously planned
959 product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
960 extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
962 if consume['lot_id']:
963 stock_mov_obj.write(cr, uid, [extra_move_id], {'restrict_lot_id': consume['lot_id']}, context=context)
964 stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
966 self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
967 self.signal_workflow(cr, uid, [production_id], 'button_produce_done')
970 def _costs_generate(self, cr, uid, production):
971 """ Calculates total costs at the end of the production.
972 @param production: Id of production order.
973 @return: Calculated amount.
976 analytic_line_obj = self.pool.get('account.analytic.line')
977 for wc_line in production.workcenter_lines:
978 wc = wc_line.workcenter_id
979 if wc.costs_journal_id and wc.costs_general_account_id:
981 value = wc_line.hour * wc.costs_hour
982 account = wc.costs_hour_account_id.id
983 if value and account:
985 # we user SUPERUSER_ID as we do not garantee an mrp user
986 # has access to account analytic lines but still should be
987 # able to produce orders
988 analytic_line_obj.create(cr, SUPERUSER_ID, {
989 'name': wc_line.name + ' (H)',
991 'account_id': account,
992 'general_account_id': wc.costs_general_account_id.id,
993 'journal_id': wc.costs_journal_id.id,
995 'product_id': wc.product_id.id,
996 'unit_amount': wc_line.hour,
997 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
1000 value = wc_line.cycle * wc.costs_cycle
1001 account = wc.costs_cycle_account_id.id
1002 if value and account:
1004 analytic_line_obj.create(cr, SUPERUSER_ID, {
1005 'name': wc_line.name + ' (C)',
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.cycle,
1013 'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
1017 def action_in_production(self, cr, uid, ids, context=None):
1018 """ Changes state to In Production and writes starting date.
1021 return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
1023 def consume_lines_get(self, cr, uid, ids, *args):
1025 for order in self.browse(cr, uid, ids, context={}):
1026 res += [x.id for x in order.move_lines]
1029 def test_ready(self, cr, uid, ids):
1031 for production in self.browse(cr, uid, ids):
1032 if production.move_lines and not production.ready_production:
1038 def _make_production_produce_line(self, cr, uid, production, context=None):
1039 stock_move = self.pool.get('stock.move')
1040 proc_obj = self.pool.get('procurement.order')
1041 source_location_id = production.product_id.property_stock_production.id
1042 destination_location_id = production.location_dest_id.id
1043 procs = proc_obj.search(cr, uid, [('production_id', '=', production.id)], context=context)
1044 procurement_id = procs and procs[0] or False
1046 'name': production.name,
1047 'date': production.date_planned,
1048 'product_id': production.product_id.id,
1049 'product_uom': production.product_uom.id,
1050 'product_uom_qty': production.product_qty,
1051 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1052 'product_uos': production.product_uos and production.product_uos.id or False,
1053 'location_id': source_location_id,
1054 'location_dest_id': destination_location_id,
1055 'move_dest_id': production.move_prod_id.id,
1056 'procurement_id': procurement_id,
1057 'company_id': production.company_id.id,
1058 'production_id': production.id,
1059 'origin': production.name,
1061 move_id = stock_move.create(cr, uid, data, context=context)
1062 #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1063 #is 1 element long, so we can take the first.
1064 return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1066 def _get_raw_material_procure_method(self, cr, uid, product, context=None):
1067 '''This method returns the procure_method to use when creating the stock move for the production raw materials'''
1068 warehouse_obj = self.pool['stock.warehouse']
1070 mto_route = warehouse_obj._get_mto_route(cr, uid, context=context)
1072 return "make_to_stock"
1073 routes = product.route_ids + product.categ_id.total_route_ids
1074 if mto_route in [x.id for x in routes]:
1075 return "make_to_order"
1076 return "make_to_stock"
1078 def _create_previous_move(self, cr, uid, move_id, product, source_location_id, dest_location_id, context=None):
1080 When the routing gives a different location than the raw material location of the production order,
1081 we should create an extra move from the raw material location to the location of the routing, which
1082 precedes the consumption line (chained). The picking type depends on the warehouse in which this happens
1083 and the type of locations.
1085 loc_obj = self.pool.get("stock.location")
1086 stock_move = self.pool.get('stock.move')
1087 type_obj = self.pool.get('stock.picking.type')
1088 # Need to search for a picking type
1089 move = stock_move.browse(cr, uid, move_id, context=context)
1090 src_loc = loc_obj.browse(cr, uid, source_location_id, context=context)
1091 dest_loc = loc_obj.browse(cr, uid, dest_location_id, context=context)
1092 code = stock_move.get_code_from_locs(cr, uid, move, src_loc, dest_loc, context=context)
1093 if code == 'outgoing':
1096 check_loc = dest_loc
1097 wh = loc_obj.get_warehouse(cr, uid, check_loc, context=context)
1098 domain = [('code', '=', code)]
1100 domain += [('warehouse_id', '=', wh)]
1101 types = type_obj.search(cr, uid, domain, context=context)
1102 move = stock_move.copy(cr, uid, move_id, default = {
1103 'location_id': source_location_id,
1104 'location_dest_id': dest_location_id,
1105 'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context),
1106 'raw_material_production_id': False,
1107 'move_dest_id': move_id,
1108 'picking_type_id': types and types[0] or False,
1112 def _make_consume_line_from_data(self, cr, uid, production, product, uom_id, qty, uos_id, uos_qty, context=None):
1113 stock_move = self.pool.get('stock.move')
1114 loc_obj = self.pool.get('stock.location')
1115 # Internal shipment is created for Stockable and Consumer Products
1116 if product.type not in ('product', 'consu'):
1118 # Take routing location as a Source Location.
1119 source_location_id = production.location_src_id.id
1120 prod_location_id = source_location_id
1122 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:
1123 source_location_id = production.bom_id.routing_id.location_id.id
1126 destination_location_id = production.product_id.property_stock_production.id
1127 move_id = stock_move.create(cr, uid, {
1128 'name': production.name,
1129 'date': production.date_planned,
1130 'product_id': product.id,
1131 'product_uom_qty': qty,
1132 'product_uom': uom_id,
1133 'product_uos_qty': uos_id and uos_qty or False,
1134 'product_uos': uos_id or False,
1135 'location_id': source_location_id,
1136 'location_dest_id': destination_location_id,
1137 'company_id': production.company_id.id,
1138 '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
1139 'raw_material_production_id': production.id,
1140 #this saves us a browse in create()
1141 'price_unit': product.standard_price,
1142 'origin': production.name,
1143 'warehouse_id': loc_obj.get_warehouse(cr, uid, production.location_src_id, context=context),
1147 prev_move = self._create_previous_move(cr, uid, move_id, product, prod_location_id, source_location_id, context=context)
1148 stock_move.action_confirm(cr, uid, [prev_move], context=context)
1151 def _make_production_consume_line(self, cr, uid, line, context=None):
1152 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)
1155 def _make_service_procurement(self, cr, uid, line, context=None):
1156 prod_obj = self.pool.get('product.product')
1157 if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
1159 'name': line.production_id.name,
1160 'origin': line.production_id.name,
1161 'company_id': line.production_id.company_id.id,
1162 'date_planned': line.production_id.date_planned,
1163 'product_id': line.product_id.id,
1164 'product_qty': line.product_qty,
1165 'product_uom': line.product_uom.id,
1166 'product_uos_qty': line.product_uos_qty,
1167 'product_uos': line.product_uos.id,
1169 proc_obj = self.pool.get("procurement.order")
1170 proc = proc_obj.create(cr, uid, vals, context=context)
1171 proc_obj.run(cr, uid, [proc], context=context)
1174 def action_confirm(self, cr, uid, ids, context=None):
1175 """ Confirms production order.
1176 @return: Newly generated Shipment Id.
1178 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)])
1179 self.action_compute(cr, uid, uncompute_ids, context=context)
1180 for production in self.browse(cr, uid, ids, context=context):
1181 self._make_production_produce_line(cr, uid, production, context=context)
1184 for line in production.product_lines:
1185 if line.product_id.type != 'service':
1186 stock_move_id = self._make_production_consume_line(cr, uid, line, context=context)
1187 stock_moves.append(stock_move_id)
1189 self._make_service_procurement(cr, uid, line, context=context)
1191 self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
1192 production.write({'state': 'confirmed'})
1195 def action_assign(self, cr, uid, ids, context=None):
1197 Checks the availability on the consume lines of the production order
1199 from openerp import workflow
1200 move_obj = self.pool.get("stock.move")
1201 for production in self.browse(cr, uid, ids, context=context):
1202 move_obj.action_assign(cr, uid, [x.id for x in production.move_lines], context=context)
1203 if self.pool.get('mrp.production').test_ready(cr, uid, [production.id]):
1204 workflow.trg_validate(uid, 'mrp.production', production.id, 'moves_ready', cr)
1207 def force_production(self, cr, uid, ids, *args):
1208 """ Assigns products.
1209 @param *args: Arguments
1212 from openerp import workflow
1213 move_obj = self.pool.get('stock.move')
1214 for order in self.browse(cr, uid, ids):
1215 move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1216 if self.pool.get('mrp.production').test_ready(cr, uid, [order.id]):
1217 workflow.trg_validate(uid, 'mrp.production', order.id, 'moves_ready', cr)
1221 class mrp_production_workcenter_line(osv.osv):
1222 _name = 'mrp.production.workcenter.line'
1223 _description = 'Work Order'
1225 _inherit = ['mail.thread']
1228 'name': fields.char('Work Order', required=True),
1229 'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1230 'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1231 'hour': fields.float('Number of Hours', digits=(16, 2)),
1232 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1233 'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1234 track_visibility='onchange', select=True, ondelete='cascade', required=True),
1237 'sequence': lambda *a: 1,
1238 'hour': lambda *a: 0,
1239 'cycle': lambda *a: 0,
1242 class mrp_production_product_line(osv.osv):
1243 _name = 'mrp.production.product.line'
1244 _description = 'Production Scheduled Product'
1246 'name': fields.char('Name', required=True),
1247 'product_id': fields.many2one('product.product', 'Product', required=True),
1248 'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1249 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1250 'product_uos_qty': fields.float('Product UOS Quantity'),
1251 'product_uos': fields.many2one('product.uom', 'Product UOS'),
1252 'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1255 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: