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