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