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