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