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