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