[FIX] stock: packages menuitem hidden by the right group. sale_stock: fixed bug ...
[odoo/odoo.git] / addons / mrp / mrp.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import time
23 from datetime import datetime
24 import openerp.addons.decimal_precision as dp
25 from openerp.osv import fields, osv, orm
26 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
27 from openerp.tools import float_compare
28 from openerp.tools.translate import _
29 from openerp import tools, SUPERUSER_ID
30 from openerp import SUPERUSER_ID
31 from openerp.addons.product import _common
32
33
34 class mrp_property_group(osv.osv):
35     """
36     Group of mrp properties.
37     """
38     _name = 'mrp.property.group'
39     _description = 'Property Group'
40     _columns = {
41         'name': fields.char('Property Group', size=64, required=True),
42         'description': fields.text('Description'),
43     }
44
45 class mrp_property(osv.osv):
46     """
47     Properties of mrp.
48     """
49     _name = 'mrp.property'
50     _description = 'Property'
51     _columns = {
52         'name': fields.char('Name', size=64, required=True),
53         'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
54         'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
55         'description': fields.text('Description'),
56     }
57     _defaults = {
58         'composition': lambda *a: 'min',
59     }
60 #----------------------------------------------------------
61 # Work Centers
62 #----------------------------------------------------------
63 # capacity_hour : capacity per hour. default: 1.0.
64 #          Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
65 # unit_per_cycle : how many units are produced for one cycle
66
67 class mrp_workcenter(osv.osv):
68     _name = 'mrp.workcenter'
69     _description = 'Work Center'
70     _inherits = {'resource.resource':"resource_id"}
71     _columns = {
72         'note': fields.text('Description', help="Description of the Work Center. Explain here what's a cycle according to this Work Center."),
73         '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."),
74         'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
75         'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
76         'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
77         'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work Center per hour."),
78         'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','!=','view')],
79             help="Fill this only if you want automatic analytic accounting entries on production orders."),
80         'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work Center per cycle."),
81         'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','!=','view')],
82             help="Fill this only if you want automatic analytic accounting entries on production orders."),
83         'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
84         'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','!=','view')]),
85         'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
86         'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to easily track your production costs in the analytic accounting."),
87     }
88     _defaults = {
89         'capacity_per_cycle': 1.0,
90         'resource_type': 'material',
91      }
92
93     def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
94         value = {}
95
96         if product_id:
97             cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
98             value = {'costs_hour': cost.standard_price}
99         return {'value': value}
100
101 class mrp_routing(osv.osv):
102     """
103     For specifying the routings of Work Centers.
104     """
105     _name = 'mrp.routing'
106     _description = 'Routing'
107     _columns = {
108         'name': fields.char('Name', size=64, required=True),
109         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
110         'code': fields.char('Code', size=8),
111
112         'note': fields.text('Description'),
113         'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
114
115         'location_id': fields.many2one('stock.location', 'Production Location',
116             help="Keep empty if you produce at the location where the finished products are needed." \
117                 "Set a location if you produce at a fixed location. This can be a partner location " \
118                 "if you subcontract the manufacturing operations."
119         ),
120         'company_id': fields.many2one('res.company', 'Company'),
121     }
122     _defaults = {
123         'active': lambda *a: 1,
124         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
125     }
126
127 class mrp_routing_workcenter(osv.osv):
128     """
129     Defines working cycles and hours of a Work Center using routings.
130     """
131     _name = 'mrp.routing.workcenter'
132     _description = 'Work Center Usage'
133     _order = 'sequence'
134     _columns = {
135         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
136         'name': fields.char('Name', size=64, required=True),
137         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing Work Centers."),
138         'cycle_nbr': fields.float('Number of Cycles', required=True,
139             help="Number of iterations this work center has to do in the specified operation of the routing."),
140         '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."),
141         'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
142              help="Routing indicates all the Work Centers used, for how long and/or cycles." \
143                 "If Routing is indicated then,the third tab of a production order (Work Centers) will be automatically pre-completed."),
144         'note': fields.text('Description'),
145         'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
146     }
147     _defaults = {
148         'cycle_nbr': lambda *a: 1.0,
149         'hour_nbr': lambda *a: 0.0,
150     }
151
152 class mrp_bom(osv.osv):
153     """
154     Defines bills of material for a product.
155     """
156     _name = 'mrp.bom'
157     _description = 'Bill of Material'
158     _inherit = ['mail.thread']
159
160     def _child_compute(self, cr, uid, ids, name, arg, context=None):
161         """ Gets child bom.
162         @param self: The object pointer
163         @param cr: The current row, from the database cursor,
164         @param uid: The current user ID for security checks
165         @param ids: List of selected IDs
166         @param name: Name of the field
167         @param arg: User defined argument
168         @param context: A standard dictionary for contextual values
169         @return:  Dictionary of values
170         """
171         result = {}
172         if context is None:
173             context = {}
174         bom_obj = self.pool.get('mrp.bom')
175         bom_id = context and context.get('active_id', False) or False
176         cr.execute('select id from mrp_bom')
177         if all(bom_id != r[0] for r in cr.fetchall()):
178             ids.sort()
179             bom_id = ids[0]
180         bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
181         for bom in self.browse(cr, uid, ids, context=context):
182             if (bom_parent) or (bom.id == bom_id):
183                 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
184             else:
185                 result[bom.id] = []
186             if bom.bom_lines:
187                 continue
188             ok = ((name=='child_complete_ids'))
189             if (bom.type=='phantom' or ok):
190                 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
191                 if sids:
192                     bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
193                     result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
194
195         return result
196
197     def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
198         """ Sets particular method for the selected bom type.
199         @param field_name: Name of the field
200         @param arg: User defined argument
201         @return:  Dictionary of values
202         """
203         res = dict.fromkeys(ids, False)
204         for line in self.browse(cr, uid, ids, context=context):
205             if line.type == 'phantom' and not line.bom_id:
206                 res[line.id] = 'set'
207                 continue
208             if line.bom_lines or line.type == 'phantom':
209                 continue
210             if line.product_id.procure_method == 'make_to_stock':
211                 res[line.id] = 'stock'
212             else:
213                 res[line.id] = 'order'
214         return res
215
216     _columns = {
217         'name': fields.char('Name', size=64),
218         'code': fields.char('Reference', size=16),
219         '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."),
220         'type': fields.selection([('normal', 'Normal BoM'), ('phantom', 'Sets / Phantom')], 'BoM Type', required=True,
221                                  help= "If a by-product is used in several products, it can be useful to create its own BoM. "\
222                                  "Though if you don't want separated production orders for this by-product, select Set/Phantom as BoM type. "\
223                                  "If a Phantom BoM is used for a root product, it will be sold and shipped as a set of components, instead of being produced."),
224         'method': fields.function(_compute_type, string='Method', type='selection', selection=[('', ''), ('stock', 'On Stock'), ('order', 'On Order'), ('set', 'Set / Pack')]),
225         'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
226         'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
227         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
228         'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
229         'product_id': fields.many2one('product.product', 'Product', required=True),
230         'product_uos_qty': fields.float('Product UOS Qty'),
231         '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."),
232         'product_qty': fields.float('Product Quantity', required=True, digits_compute=dp.get_precision('Product Unit of Measure')),
233         '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"),
234         'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
235         'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
236         'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
237         'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
238         '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."),
239         'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id', 'property_id', 'Properties'),
240         'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', string="BoM Hierarchy", type='many2many'),
241         'company_id': fields.many2one('res.company', 'Company', required=True),
242     }
243     _defaults = {
244         'active': lambda *a: 1,
245         'product_efficiency': lambda *a: 1.0,
246         'product_qty': lambda *a: 1.0,
247         'product_rounding': lambda *a: 0.0,
248         'type': lambda *a: 'normal',
249         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
250     }
251     _order = "sequence"
252     _parent_name = "bom_id"
253     _sql_constraints = [
254         ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \
255             'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
256     ]
257
258     def _check_recursion(self, cr, uid, ids, context=None):
259         level = 100
260         while len(ids):
261             cr.execute('select distinct bom_id from mrp_bom where id IN %s', (tuple(ids),))
262             ids = filter(None, map(lambda x: x[0], cr.fetchall()))
263             if not level:
264                 return False
265             level -= 1
266         return True
267
268     def _check_product(self, cr, uid, ids, context=None):
269         all_prod = []
270         boms = self.browse(cr, uid, ids, context=context)
271         def check_bom(boms):
272             res = True
273             for bom in boms:
274                 if bom.product_id.id in all_prod:
275                     res = res and False
276                 all_prod.append(bom.product_id.id)
277                 lines = bom.bom_lines
278                 if lines:
279                     res = res and check_bom([bom_id for bom_id in lines if bom_id not in boms])
280             return res
281         return check_bom(boms)
282
283     _constraints = [
284         (_check_recursion, 'Error ! You cannot create recursive BoM.', ['parent_id']),
285         (_check_product, 'BoM line product should not be same as BoM product.', ['product_id']),
286     ]
287
288     def onchange_product_id(self, cr, uid, ids, product_id, name, product_qty=0, context=None):
289         """ Changes UoM and name if product_id changes.
290         @param name: Name of the field
291         @param product_id: Changed product_id
292         @return:  Dictionary of changed values
293         """
294         res = {}
295         if product_id:
296             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
297             res['value'] = {'name': prod.name, 'product_uom': prod.uom_id.id, 'product_uos_qty': 0, 'product_uos': False}
298             if prod.uos_id.id:
299                 res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
300                 res['value']['product_uos'] = prod.uos_id.id
301         return res
302
303     def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
304         res = {'value': {}}
305         if not product_uom or not product_id:
306             return res
307         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
308         uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
309         if uom.category_id.id != product.uom_id.category_id.id:
310             res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
311             res['value'].update({'product_uom': product.uom_id.id})
312         return res
313
314     def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
315         """ Finds BoM for particular product and product uom.
316         @param product_id: Selected product.
317         @param product_uom: Unit of measure of a product.
318         @param properties: List of related properties.
319         @return: False or BoM id.
320         """
321         if properties is None:
322             properties = []
323         domain = [('product_id', '=', product_id), ('bom_id', '=', False),
324             '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
325             '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
326         ids = self.search(cr, uid, domain)
327         max_prop = 0
328         result = False
329         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
330             prop = 0
331             for prop_id in bom.property_ids:
332                 if prop_id.id in properties:
333                     prop += 1
334             if (prop > max_prop) or ((max_prop == 0) and not result):
335                 result = bom.id
336                 max_prop = prop
337         return result
338
339     def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
340         """ Finds Products and Work Centers for related BoM for manufacturing order.
341         @param bom: BoM of particular product.
342         @param factor: Factor of product UoM.
343         @param properties: A List of properties Ids.
344         @param addthis: If BoM found then True else False.
345         @param level: Depth level to find BoM lines starts from 10.
346         @return: result: List of dictionaries containing product details.
347                  result2: List of dictionaries containing Work Center details.
348         """
349         routing_obj = self.pool.get('mrp.routing')
350         factor = factor / (bom.product_efficiency or 1.0)
351         factor = _common.ceiling(factor, bom.product_rounding)
352         if factor < bom.product_rounding:
353             factor = bom.product_rounding
354         result = []
355         result2 = []
356         phantom = False
357         if bom.type == 'phantom' and not bom.bom_lines:
358             newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
359
360             if newbom:
361                 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor * bom.product_qty, properties, addthis=True, level=level + 10)
362                 result = result + res[0]
363                 result2 = result2 + res[1]
364                 phantom = True
365             else:
366                 phantom = False
367         if not phantom:
368             if addthis and not bom.bom_lines:
369                 result.append({
370                     'name': bom.product_id.name,
371                     'product_id': bom.product_id.id,
372                     'product_qty': bom.product_qty * factor,
373                     'product_uom': bom.product_uom.id,
374                     'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
375                     'product_uos': bom.product_uos and bom.product_uos.id or False,
376                 })
377             routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
378             if routing:
379                 for wc_use in routing.workcenter_lines:
380                     wc = wc_use.workcenter_id
381                     d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
382                     mult = (d + (m and 1.0 or 0.0))
383                     cycle = mult * wc_use.cycle_nbr
384                     result2.append({
385                         'name': tools.ustr(wc_use.name) + ' - ' + tools.ustr(bom.product_id.name),
386                         'workcenter_id': wc.id,
387                         'sequence': level + (wc_use.sequence or 0),
388                         'cycle': cycle,
389                         '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)),
390                     })
391             for bom2 in bom.bom_lines:
392                 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level + 10)
393                 result = result + res[0]
394                 result2 = result2 + res[1]
395         return result, result2
396
397     def copy_data(self, cr, uid, id, default=None, context=None):
398         if default is None:
399             default = {}
400         bom_data = self.read(cr, uid, id, [], context=context)
401         default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
402         return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
403
404
405 class mrp_production(osv.osv):
406     """
407     Production Orders / Manufacturing Orders
408     """
409     _name = 'mrp.production'
410     _description = 'Manufacturing Order'
411     _date_name = 'date_planned'
412     _inherit = ['mail.thread', 'ir.needaction_mixin']
413
414     def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
415         """ Calculates total hours and total no. of cycles for a production order.
416         @param prop: Name of field.
417         @param unknow_none:
418         @return: Dictionary of values.
419         """
420         result = {}
421         for prod in self.browse(cr, uid, ids, context=context):
422             result[prod.id] = {
423                 'hour_total': 0.0,
424                 'cycle_total': 0.0,
425             }
426             for wc in prod.workcenter_lines:
427                 result[prod.id]['hour_total'] += wc.hour
428                 result[prod.id]['cycle_total'] += wc.cycle
429         return result
430
431     def _src_id_default(self, cr, uid, ids, context=None):
432         try:
433             location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
434             self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
435         except (orm.except_orm, ValueError):
436             location_id = False
437         return location_id
438
439     def _dest_id_default(self, cr, uid, ids, context=None):
440         try:
441             location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
442             self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
443         except (orm.except_orm, ValueError):
444             location_id = False
445         return location_id
446
447     def _get_progress(self, cr, uid, ids, name, arg, context=None):
448         """ Return product quantity percentage """
449         result = dict.fromkeys(ids, 100)
450         for mrp_production in self.browse(cr, uid, ids, context=context):
451             if mrp_production.product_qty:
452                 done = 0.0
453                 for move in mrp_production.move_created_ids2:
454                     if not move.scrapped and move.product_id == mrp_production.product_id:
455                         done += move.product_qty
456                 result[mrp_production.id] = done / mrp_production.product_qty * 100
457         return result
458
459     def _moves_assigned(self, cr, uid, ids, name, arg, context=None):
460         """ Test whether all the consume lines are assigned """
461         res = {}
462         for production in self.browse(cr, uid, ids, context=context):
463             res[production.id] = True
464             states = [x.state != 'assigned' for x in production.move_lines if x]
465             if any(states) or len(states) == 0:
466                 res[production.id] = False
467         return res
468
469     def _mrp_from_move(self, cr, uid, ids, context=None):
470         """ Return mrp"""
471         res = []
472         for move in self.browse(cr, uid, ids, context=context):
473             res += self.pool.get("mrp.production").search(cr, uid, [('move_lines', 'in', move.id)], context=context)
474         return res
475
476     _columns = {
477         'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
478         'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
479             help="Reference of the document that generated this production order request."),
480         'priority': fields.selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
481             select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
482
483         'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
484         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
485         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
486         'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
487         'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
488         'progress': fields.function(_get_progress, type='float',
489             string='Production progress'),
490
491         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
492             readonly=True, states={'draft': [('readonly', False)]},
493             help="Location where the system will look for components."),
494         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
495             readonly=True, states={'draft': [('readonly', False)]},
496             help="Location where the system will stock the finished products."),
497         'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft': [('readonly', False)]}),
498         'date_start': fields.datetime('Start Date', select=True, readonly=True),
499         'date_finished': fields.datetime('End Date', select=True, readonly=True),
500         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id', '=', False)], readonly=True, states={'draft': [('readonly', False)]},
501             help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
502         'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft': [('readonly', False)]},
503             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."),
504         'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
505         'move_lines': fields.one2many('stock.move', 'raw_material_production_id', 'Products to Consume',
506             domain=[('state', 'not in', ('done', 'cancel'))], readonly=True, states={'draft': [('readonly', False)]}),
507         'move_lines2': fields.one2many('stock.move', 'raw_material_production_id', 'Consumed Products',
508             domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
509         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
510             domain=[('state', 'not in', ('done', 'cancel'))], readonly=True),
511         'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
512             domain=[('state', 'in', ('done', 'cancel'))], readonly=True),
513         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
514             readonly=True),
515         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
516             readonly=True, states={'draft': [('readonly', False)]}),
517         'state': fields.selection(
518             [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
519                 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
520             string='Status', readonly=True,
521             track_visibility='onchange',
522             help="When the production order is created the status is set to 'Draft'.\n\
523                 If the order is confirmed the status is set to 'Waiting Goods'.\n\
524                 If any exceptions are there, the status is set to 'Picking Exception'.\n\
525                 If the stock is available then the status is set to 'Ready to Produce'.\n\
526                 When the production gets started then the status is set to 'In Production'.\n\
527                 When the production is over, the status is set to 'Done'."),
528         'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
529         'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
530         'user_id': fields.many2one('res.users', 'Responsible'),
531         'company_id': fields.many2one('res.company', 'Company', required=True),
532         'ready_production': fields.function(_moves_assigned, type='boolean', store={'stock.move': (_mrp_from_move, ['state'], 10)}),
533     }
534
535     _defaults = {
536         'priority': lambda *a: '1',
537         'state': lambda *a: 'draft',
538         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
539         'product_qty': lambda *a: 1.0,
540         'user_id': lambda self, cr, uid, c: uid,
541         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
542         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
543         'location_src_id': _src_id_default,
544         'location_dest_id': _dest_id_default
545     }
546
547     _sql_constraints = [
548         ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
549     ]
550
551     _order = 'priority desc, date_planned asc'
552
553     def _check_qty(self, cr, uid, ids, context=None):
554         for order in self.browse(cr, uid, ids, context=context):
555             if order.product_qty <= 0:
556                 return False
557         return True
558
559     _constraints = [
560         (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
561     ]
562
563     def unlink(self, cr, uid, ids, context=None):
564         for production in self.browse(cr, uid, ids, context=context):
565             if production.state not in ('draft', 'cancel'):
566                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
567         return super(mrp_production, self).unlink(cr, uid, ids, context=context)
568
569     def copy(self, cr, uid, id, default=None, context=None):
570         if default is None:
571             default = {}
572         default.update({
573             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
574             'move_lines': [],
575             'move_lines2': [],
576             'move_created_ids': [],
577             'move_created_ids2': [],
578             'product_lines': [],
579             'move_prod_id': False,
580         })
581         return super(mrp_production, self).copy(cr, uid, id, default, context)
582
583     def location_id_change(self, cr, uid, ids, src, dest, context=None):
584         """ Changes destination location if source location is changed.
585         @param src: Source location id.
586         @param dest: Destination location id.
587         @return: Dictionary of values.
588         """
589         if dest:
590             return {}
591         if src:
592             return {'value': {'location_dest_id': src}}
593         return {}
594
595     def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
596         """ Finds UoM of changed product.
597         @param product_id: Id of changed product.
598         @return: Dictionary of values.
599         """
600         result = {}
601         if not product_id:
602             return {'value': {
603                 'product_uom': False,
604                 'bom_id': False,
605                 'routing_id': False,
606                 'product_uos_qty': 0, 
607                 'product_uos': False
608             }}
609         bom_obj = self.pool.get('mrp.bom')
610         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
611         bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
612         routing_id = False
613         if bom_id:
614             bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
615             routing_id = bom_point.routing_id.id or False
616         product_uom_id = product.uom_id and product.uom_id.id or False
617         product_uos_id = product.uos_id and product.uos_id.id or False
618         result['value'] = {'product_uos_qty': 0, 'product_uos': False, 'product_uom': product_uom_id, 'bom_id': bom_id, 'routing_id': routing_id}
619         if product.uos_id.id:
620             result['value']['product_uos_qty'] = product_qty * product.uos_coeff
621             result['value']['product_uos'] = product.uos_id.id
622         return result
623
624     def bom_id_change(self, cr, uid, ids, bom_id, context=None):
625         """ Finds routing for changed BoM.
626         @param product: Id of product.
627         @return: Dictionary of values.
628         """
629         if not bom_id:
630             return {'value': {
631                 'routing_id': False
632             }}
633         bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
634         routing_id = bom_point.routing_id.id or False
635         result = {
636             'routing_id': routing_id
637         }
638         return {'value': result}
639
640     def action_picking_except(self, cr, uid, ids):
641         """ Changes the state to Exception.
642         @return: True
643         """
644         self.write(cr, uid, ids, {'state': 'picking_except'})
645         return True
646     
647     def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
648         """ Compute product_lines and workcenter_lines from BoM structure
649         @return: product_lines
650         """
651         if properties is None:
652             properties = []
653         results = []
654         bom_obj = self.pool.get('mrp.bom')
655         uom_obj = self.pool.get('product.uom')
656         prod_line_obj = self.pool.get('mrp.production.product.line')
657         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
658         for production in self.browse(cr, uid, ids, context=context):
659             #unlink product_lines
660             prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
661             #unlink workcenter_lines
662             workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
663             # search BoM structure and route
664             bom_point = production.bom_id
665             bom_id = production.bom_id.id
666             if not bom_point:
667                 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
668                 if bom_id:
669                     bom_point = bom_obj.browse(cr, uid, bom_id)
670                     routing_id = bom_point.routing_id.id or False
671                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
672     
673             if not bom_id:
674                 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
675
676             # get components and workcenter_lines from BoM structure
677             factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
678             res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
679             results = res[0]  # product_lines
680             results2 = res[1]  # workcenter_lines
681             # reset product_lines in production order
682             for line in results:
683                 line['production_id'] = production.id
684                 prod_line_obj.create(cr, uid, line)
685
686             #reset workcenter_lines in production order
687             for line in results2:
688                 line['production_id'] = production.id
689                 workcenter_line_obj.create(cr, uid, line)
690         return results
691
692     def action_compute(self, cr, uid, ids, properties=None, context=None):
693         """ Computes bills of material of a product.
694         @param properties: List containing dictionaries of properties.
695         @return: No. of products.
696         """
697         return len(self._action_compute_lines(cr, uid, ids, properties=properties, context=context))
698
699     def action_cancel(self, cr, uid, ids, context=None):
700         """ Cancels the production order and related stock moves.
701         @return: True
702         """
703         if context is None:
704             context = {}
705         move_obj = self.pool.get('stock.move')
706         for production in self.browse(cr, uid, ids, context=context):
707             if production.move_created_ids:
708                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
709             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
710         self.write(cr, uid, ids, {'state': 'cancel'})
711         return True
712
713     def action_ready(self, cr, uid, ids, context=None):
714         """ Changes the production state to Ready and location id of stock move.
715         @return: True
716         """
717         move_obj = self.pool.get('stock.move')
718         self.write(cr, uid, ids, {'state': 'ready'})
719
720         for production in self.browse(cr, uid, ids, context=context):
721             if not production.move_created_ids:
722                 self._make_production_produce_line(cr, uid, production, context=context)
723                 for scheduled in production.product_lines:
724                     self._make_production_line_procurement(cr, uid, scheduled, False, context=context)
725
726             if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
727                 move_obj.write(cr, uid, [production.move_prod_id.id],
728                         {'location_id': production.location_dest_id.id})
729         return True
730
731     def action_production_end(self, cr, uid, ids, context=None):
732         """ Changes production state to Finish and writes finished date.
733         @return: True
734         """
735         for production in self.browse(cr, uid, ids):
736             self._costs_generate(cr, uid, production)
737         write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
738         return write_res
739
740     def test_production_done(self, cr, uid, ids):
741         """ Tests whether production is done or not.
742         @return: True or False
743         """
744         res = True
745         for production in self.browse(cr, uid, ids):
746             if production.move_lines:
747                 res = False
748
749             if production.move_created_ids:
750                 res = False
751         return res
752
753     def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
754         """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
755             it's always equal to the quantity encoded in the production order or the production wizard, but if the
756             module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
757             and its quantity.
758         :param production_id: ID of the mrp.order
759         :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
760         :return: The factor to apply to the quantity that we should produce for the given production order.
761         """
762         return 1
763
764     def _get_produced_qty(self, cr, uid, production, context=None):
765         ''' returns the produced quantity of product 'production.product_id' for the given production, in the product UoM
766         '''
767         produced_qty = 0
768         for produced_product in production.move_created_ids2:
769             if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
770                 continue
771             produced_qty += produced_product.product_qty
772         return produced_qty
773
774     def _get_consumed_data(self, cr, uid, production, context=None):
775         ''' returns a dictionary containing for each raw material of the given production, its quantity already consumed (in the raw material UoM)
776         '''
777         consumed_data = {}
778         # Calculate already consumed qtys
779         for consumed in production.move_lines2:
780             if consumed.scrapped:
781                 continue
782             if not consumed_data.get(consumed.product_id.id, False):
783                 consumed_data[consumed.product_id.id] = 0
784             consumed_data[consumed.product_id.id] += consumed.product_qty
785         return consumed_data
786
787     def _calculate_qty(self, cr, uid, production, product_qty=0.0, context=None):
788         """
789             Calculates the quantity still needed to produce an extra number of products
790         """
791         quant_obj = self.pool.get("stock.quant")
792         produced_qty = self._get_produced_qty(cr, uid, production, context=context)
793         consumed_data = self._get_consumed_data(cr, uid, production, context=context)
794
795         #In case no product_qty is given, take the remaining qty to produce for the given production
796         if not product_qty:
797             product_qty = production.product_qty - produced_qty
798
799         dicts = {}
800         # Find product qty to be consumed and consume it
801         for scheduled in production.product_lines:
802             consumed_qty = consumed_data.get(scheduled.product_id.id, 0.0)
803             # qty available for consume and produce
804             qty_avail = scheduled.product_qty - consumed_qty
805             if qty_avail <= 0.0:
806                 # there will be nothing to consume for this raw material
807                 continue
808
809             if not dicts.get(scheduled.product_id.id):
810                 dicts[scheduled.product_id.id] = {}
811
812             # total qty of consumed product we need after this consumption
813             total_consume = ((product_qty + produced_qty) * scheduled.product_qty / production.product_qty)
814             qty = total_consume - consumed_qty
815
816             # Search for quants related to this related move
817             for move in production.move_lines:
818                 if qty <= 0.0:
819                     break
820                 if move.product_id.id != scheduled.product_id.id:
821                     continue
822                 product_id = scheduled.product_id.id
823
824                 q = min(move.product_qty, qty)
825                 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, scheduled.product_id, q, domain=[('qty', '>', 0.0)],
826                                                      prefered_domain=[('reservation_id', '=', move.id)], fallback_domain=[('reservation_id', '=', False)], context=context)
827                 for quant, quant_qty in quants:
828                     if quant:
829                         lot_id = quant.lot_id.id
830                         if not product_id in dicts.keys():
831                             dicts[product_id] = {lot_id: quant_qty}
832                         elif lot_id in dicts[product_id].keys():
833                             dicts[product_id][lot_id] += quant_qty
834                         else:
835                             dicts[product_id][lot_id] = quant_qty
836                         qty -= quant_qty
837             if qty > 0:
838                 if dicts[product_id].get(False):
839                     dicts[product_id][False] += qty
840                 else:
841                     dicts[product_id][False] = qty
842
843         consume_lines = []
844         for prod in dicts.keys():
845             for lot, qty in dicts[prod].items():
846                 consume_lines.append({'product_id': prod, 'product_qty': qty, 'lot_id': lot})
847         return consume_lines
848
849     def action_produce(self, cr, uid, production_id, production_qty, production_mode, wiz=False, context=None):
850         """ To produce final product based on production mode (consume/consume&produce).
851         If Production mode is consume, all stock move lines of raw materials will be done/consumed.
852         If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
853         and stock move lines of final product will be also done/produced.
854         @param production_id: the ID of mrp.production object
855         @param production_qty: specify qty to produce
856         @param production_mode: specify production mode (consume/consume&produce).
857         @param wiz: the mrp produce product wizard, which will tell the amount of consumed products needed
858         @return: True
859         """
860         stock_mov_obj = self.pool.get('stock.move')
861         production = self.browse(cr, uid, production_id, context=context)
862         if not production.move_lines and production.state == 'ready':
863             # trigger workflow if not products to consume (eg: services)
864             self.signal_button_produce(cr, uid, [production_id])
865
866         produced_qty = self._get_produced_qty(cr, uid, production, context=context)
867
868         main_production_move = False
869         if production_mode == 'consume_produce':
870             # To produce remaining qty of final product
871             #vals = {'state':'confirmed'}
872             #final_product_todo = [x.id for x in production.move_created_ids]
873             #stock_mov_obj.write(cr, uid, final_product_todo, vals)
874             #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
875             produced_products = {}
876             for produced_product in production.move_created_ids2:
877                 if produced_product.scrapped:
878                     continue
879                 if not produced_products.get(produced_product.product_id.id, False):
880                     produced_products[produced_product.product_id.id] = 0
881                 produced_products[produced_product.product_id.id] += produced_product.product_qty
882
883             for produce_product in production.move_created_ids:
884                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
885                 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
886                 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
887                 if float_compare(rest_qty, (subproduct_factor * production_qty), precision_rounding=produce_product.product_id.uom_id.rounding) < 0:
888                     prod_name = produce_product.product_id.name_get()[0][1]
889                     raise osv.except_osv(_('Warning!'), _('You are going to produce total %s quantities of "%s".\nBut you can only produce up to total %s quantities.') % ((subproduct_factor * production_qty), prod_name, rest_qty))
890                 if float_compare(rest_qty, 0, precision_rounding=produce_product.product_id.uom_id.rounding) > 0:
891                     lot_id = False
892                     if wiz:
893                         lot_id = wiz.lot_id.id
894                     new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
895                     if produce_product.product_id.id == production.product_id.id and new_moves:
896                         main_production_move = new_moves[0]
897
898         if production_mode in ['consume', 'consume_produce']:
899             if wiz:
900                 consume_lines = []
901                 for cons in wiz.consume_lines:
902                     consume_lines.append({'product_id': cons.product_id.id, 'lot_id': cons.lot_id.id, 'product_qty': cons.product_qty})
903             else:
904                 consume_lines = self._calculate_qty(cr, uid, production, production_qty, context=context)
905             for consume in consume_lines:
906                 remaining_qty = consume['product_qty']
907                 for raw_material_line in production.move_lines:
908                     if remaining_qty <= 0:
909                         break
910                     if consume['product_id'] != raw_material_line.product_id.id:
911                         continue
912                     consumed_qty = min(remaining_qty, raw_material_line.product_qty)
913                     stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id, restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
914                     remaining_qty -= consumed_qty
915
916         self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
917         self.signal_button_produce_done(cr, uid, [production_id])
918         return True
919
920     def _costs_generate(self, cr, uid, production):
921         """ Calculates total costs at the end of the production.
922         @param production: Id of production order.
923         @return: Calculated amount.
924         """
925         amount = 0.0
926         analytic_line_obj = self.pool.get('account.analytic.line')
927         for wc_line in production.workcenter_lines:
928             wc = wc_line.workcenter_id
929             if wc.costs_journal_id and wc.costs_general_account_id:
930                 # Cost per hour
931                 value = wc_line.hour * wc.costs_hour
932                 account = wc.costs_hour_account_id.id
933                 if value and account:
934                     amount += value
935                     # we user SUPERUSER_ID as we do not garantee an mrp user
936                     # has access to account analytic lines but still should be
937                     # able to produce orders
938                     analytic_line_obj.create(cr, SUPERUSER_ID, {
939                         'name': wc_line.name + ' (H)',
940                         'amount': value,
941                         'account_id': account,
942                         'general_account_id': wc.costs_general_account_id.id,
943                         'journal_id': wc.costs_journal_id.id,
944                         'ref': wc.code,
945                         'product_id': wc.product_id.id,
946                         'unit_amount': wc_line.hour,
947                         'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
948                     })
949                 # Cost per cycle
950                 value = wc_line.cycle * wc.costs_cycle
951                 account = wc.costs_cycle_account_id.id
952                 if value and account:
953                     amount += value
954                     analytic_line_obj.create(cr, SUPERUSER_ID, {
955                         'name': wc_line.name + ' (C)',
956                         'amount': value,
957                         'account_id': account,
958                         'general_account_id': wc.costs_general_account_id.id,
959                         'journal_id': wc.costs_journal_id.id,
960                         'ref': wc.code,
961                         'product_id': wc.product_id.id,
962                         'unit_amount': wc_line.cycle,
963                         'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
964                     })
965         return amount
966
967     def action_in_production(self, cr, uid, ids, context=None):
968         """ Changes state to In Production and writes starting date.
969         @return: True
970         """
971         return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
972
973     def consume_lines_get(self, cr, uid, ids, *args):
974         res = []
975         for order in self.browse(cr, uid, ids, context={}):
976             res += [x.id for x in order.move_lines]
977         return res
978
979     def test_ready(self, cr, uid, ids):
980         res = False
981         for production in self.browse(cr, uid, ids):
982             if production.ready_production:
983                 res = True
984         return res
985
986     def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
987         procurement_order = self.pool.get('procurement.order')
988         production = production_line.production_id
989         location_id = production.location_src_id.id
990         date_planned = production.date_planned
991         procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
992         procurement_id = procurement_order.create(cr, uid, {
993                     'name': procurement_name,
994                     'origin': procurement_name,
995                     'date_planned': date_planned,
996                     'product_id': production_line.product_id.id,
997                     'product_qty': production_line.product_qty,
998                     'product_uom': production_line.product_uom.id,
999                     'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
1000                     'product_uos': production_line.product_uos and production_line.product_uos.id or False,
1001                     'location_id': location_id,
1002                     'procure_method': production_line.product_id.procure_method,
1003                     'move_id': shipment_move_id,
1004                     'company_id': production.company_id.id,
1005                 })
1006         procurement_order.signal_button_confirm(cr, uid, [procurement_id])
1007         return procurement_id
1008
1009     def _make_production_produce_line(self, cr, uid, production, context=None):
1010         stock_move = self.pool.get('stock.move')
1011         source_location_id = production.product_id.property_stock_production.id
1012         destination_location_id = production.location_dest_id.id
1013         data = {
1014             'name': production.name,
1015             'date': production.date_planned,
1016             'product_id': production.product_id.id,
1017             'product_qty': production.product_qty,
1018             'product_uom': production.product_uom.id,
1019             'product_uom_qty': production.product_qty,
1020             'product_uos_qty': production.product_uos and production.product_uos_qty or False,
1021             'product_uos': production.product_uos and production.product_uos.id or False,
1022             'location_id': source_location_id,
1023             'location_dest_id': destination_location_id,
1024             'move_dest_id': production.move_prod_id.id,
1025             'company_id': production.company_id.id,
1026             'production_id': production.id,
1027         }
1028         move_id = stock_move.create(cr, uid, data, context=context)
1029         #a phantom bom cannot be used in mrp order so it's ok to assume the list returned by action_confirm
1030         #is 1 element long, so we can take the first.
1031         return stock_move.action_confirm(cr, uid, [move_id], context=context)[0]
1032
1033     def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
1034         stock_move = self.pool.get('stock.move')
1035         production = production_line.production_id
1036         # Internal shipment is created for Stockable and Consumer Products
1037         if production_line.product_id.type not in ('product', 'consu'):
1038             return False
1039         destination_location_id = production.product_id.property_stock_production.id
1040         if not source_location_id:
1041             source_location_id = production.location_src_id.id
1042         move_id = stock_move.create(cr, uid, {
1043             'name': production.name,
1044             'date': production.date_planned,
1045             'product_id': production_line.product_id.id,
1046             'product_uom_qty': production_line.product_qty,
1047             'product_uom': production_line.product_uom.id,
1048             'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
1049             'product_uos': production_line.product_uos and production_line.product_uos.id or False,
1050             'location_id': source_location_id,
1051             'location_dest_id': destination_location_id,
1052             'company_id': production.company_id.id,
1053             'procure_method': 'make_to_order',
1054             'raw_material_production_id': production.id,
1055         })
1056         stock_move.action_confirm(cr, uid, [move_id], context=context)
1057         return True
1058
1059     def action_confirm(self, cr, uid, ids, context=None):
1060         """ Confirms production order.
1061         @return: Newly generated Shipment Id.
1062         """
1063         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)])
1064         self.action_compute(cr, uid, uncompute_ids, context=context)
1065         for production in self.browse(cr, uid, ids, context=context):
1066             produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
1067
1068             # Take routing location as a Source Location.
1069             source_location_id = production.location_src_id.id
1070             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1071                 source_location_id = production.bom_id.routing_id.location_id.id
1072
1073             for line in production.product_lines:
1074                 self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1075             production.write({'state': 'confirmed'}, context=context)
1076         return 0
1077
1078     def force_production(self, cr, uid, ids, *args):
1079         """ Assigns products.
1080         @param *args: Arguments
1081         @return: True
1082         """
1083         move_obj = self.pool.get('stock.move')
1084         for order in self.browse(cr, uid, ids):
1085             move_obj.force_assign(cr, uid, [x.id for x in order.move_lines])
1086         return True
1087
1088
1089 class mrp_production_workcenter_line(osv.osv):
1090     _name = 'mrp.production.workcenter.line'
1091     _description = 'Work Order'
1092     _order = 'sequence'
1093     _inherit = ['mail.thread']
1094
1095     _columns = {
1096         'name': fields.char('Work Order', size=64, required=True),
1097         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1098         'cycle': fields.float('Number of Cycles', digits=(16, 2)),
1099         'hour': fields.float('Number of Hours', digits=(16, 2)),
1100         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1101         'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1102             track_visibility='onchange', select=True, ondelete='cascade', required=True),
1103     }
1104     _defaults = {
1105         'sequence': lambda *a: 1,
1106         'hour': lambda *a: 0,
1107         'cycle': lambda *a: 0,
1108     }
1109
1110 class mrp_production_product_line(osv.osv):
1111     _name = 'mrp.production.product.line'
1112     _description = 'Production Scheduled Product'
1113     _columns = {
1114         'name': fields.char('Name', size=64, required=True),
1115         'product_id': fields.many2one('product.product', 'Product', required=True),
1116         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1117         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1118         'product_uos_qty': fields.float('Product UOS Quantity'),
1119         'product_uos': fields.many2one('product.uom', 'Product UOS'),
1120         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1121     }
1122
1123 class product_product(osv.osv):
1124     _inherit = "product.product"
1125     _columns = {
1126         'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1127     }
1128
1129 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: