4c3b205062d384204923764afad7fffbd7803c9d
[odoo/odoo.git] / addons / mrp / mrp.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23 from datetime import datetime
24
25 import openerp.addons.decimal_precision as dp
26 from openerp.osv import fields, osv, orm
27 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
28 from openerp.tools import float_compare
29 from openerp.tools.translate import _
30 from openerp import tools
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, product_qty=0, 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         res={'value':{}}
271         if product_id:
272             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
273             res['value']['name'] = prod.name
274             res['value']['product_uom'] = prod.uom_id.id
275             if prod.uos_id.id:
276                 #res['value']['product_uos_qty'] = product_qty * prod.uos_coeff
277                 res['value']['product_uos'] = prod.uos_id.id
278             else:
279                 res['value']['product_uos_qty'] = 0
280         return res
281
282     def onchange_product_qty_change(self, cr, uid, ids, product_id, qty=0, context=None):
283         if product_id:
284             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
285             if prod.uos_id:
286                 return {'value': {'product_uos': prod.uos_id.id, 'product_uos_qty': qty * prod.uos_coeff}}
287         return {}
288
289     def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
290         res = {'value':{}}
291         if not product_uom or not product_id:
292             return res
293         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
294         uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
295         if uom.category_id.id != product.uom_id.category_id.id:
296             res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
297             res['value'].update({'product_uom': product.uom_id.id})
298         return res
299
300     def _bom_find(self, cr, uid, product_id, product_uom, properties=None):
301         """ Finds BoM for particular product and product uom.
302         @param product_id: Selected product.
303         @param product_uom: Unit of measure of a product.
304         @param properties: List of related properties.
305         @return: False or BoM id.
306         """
307         if properties is None:
308             properties = []
309         cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
310         ids = map(lambda x: x[0], cr.fetchall())
311         max_prop = 0
312         result = False
313         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
314             prop = 0
315             for prop_id in bom.property_ids:
316                 if prop_id.id in properties:
317                     prop += 1
318             if (prop > max_prop) or ((max_prop == 0) and not result):
319                 result = bom.id
320                 max_prop = prop
321         return result
322
323     def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
324         """ Finds Products and Work Centers for related BoM for manufacturing order.
325         @param bom: BoM of particular product.
326         @param factor: Factor of product UoM.
327         @param properties: A List of properties Ids.
328         @param addthis: If BoM found then True else False.
329         @param level: Depth level to find BoM lines starts from 10.
330         @return: result: List of dictionaries containing product details.
331                  result2: List of dictionaries containing Work Center details.
332         """
333         routing_obj = self.pool.get('mrp.routing')
334         factor = factor / (bom.product_efficiency or 1.0)
335         factor = rounding(factor, bom.product_rounding)
336         if factor < bom.product_rounding:
337             factor = bom.product_rounding
338         result = []
339         result2 = []
340         phantom = False
341         if bom.type == 'phantom' and not bom.bom_lines:
342             newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
343
344             if newbom:
345                 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
346                 result = result + res[0]
347                 result2 = result2 + res[1]
348                 phantom = True
349             else:
350                 phantom = False
351         if not phantom:
352             if addthis and not bom.bom_lines:
353                 result.append(
354                 {
355                     'name': bom.product_id.name,
356                     'product_id': bom.product_id.id,
357                     'product_qty': bom.product_qty * factor,
358                     'product_uom': bom.product_uom.id,
359                     'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
360                     'product_uos': bom.product_uos and bom.product_uos.id or False,
361                 })
362             routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
363             if routing:
364                 for wc_use in routing.workcenter_lines:
365                     wc = wc_use.workcenter_id
366                     d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
367                     mult = (d + (m and 1.0 or 0.0))
368                     cycle = mult * wc_use.cycle_nbr
369                     result2.append({
370                         'name': tools.ustr(wc_use.name) + ' - '  + tools.ustr(bom.product_id.name),
371                         'workcenter_id': wc.id,
372                         'sequence': level+(wc_use.sequence or 0),
373                         'cycle': cycle,
374                         '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)),
375                     })
376             for bom2 in bom.bom_lines:
377                 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
378                 result = result + res[0]
379                 result2 = result2 + res[1]
380         return result, result2
381
382     def copy_data(self, cr, uid, id, default=None, context=None):
383         if default is None:
384             default = {}
385         bom_data = self.read(cr, uid, id, [], context=context)
386         default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False)
387         return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
388
389
390 def rounding(f, r):
391     import math
392     if not r:
393         return f
394     return math.ceil(f / r) * r
395
396 class mrp_production(osv.osv):
397     """
398     Production Orders / Manufacturing Orders
399     """
400     _name = 'mrp.production'
401     _description = 'Manufacturing Order'
402     _date_name  = 'date_planned'
403     _inherit = ['mail.thread', 'ir.needaction_mixin']
404
405     def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
406         """ Calculates total hours and total no. of cycles for a production order.
407         @param prop: Name of field.
408         @param unknow_none:
409         @return: Dictionary of values.
410         """
411         result = {}
412         for prod in self.browse(cr, uid, ids, context=context):
413             result[prod.id] = {
414                 'hour_total': 0.0,
415                 'cycle_total': 0.0,
416             }
417             for wc in prod.workcenter_lines:
418                 result[prod.id]['hour_total'] += wc.hour
419                 result[prod.id]['cycle_total'] += wc.cycle
420         return result
421
422     def _src_id_default(self, cr, uid, ids, context=None):
423         try:
424             location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
425             self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
426         except (orm.except_orm, ValueError):
427             location_id = False
428         return location_id
429
430     def _dest_id_default(self, cr, uid, ids, context=None):
431         try:
432             location_model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
433             self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
434         except (orm.except_orm, ValueError):
435             location_id = False
436         return location_id
437
438     def _get_progress(self, cr, uid, ids, name, arg, context=None):
439         """ Return product quantity percentage """
440         result = dict.fromkeys(ids, 100)
441         for mrp_production in self.browse(cr, uid, ids, context=context):
442             if mrp_production.product_qty:
443                 done = 0.0
444                 for move in mrp_production.move_created_ids2:
445                     if not move.scrapped and move.product_id == mrp_production.product_id:
446                         done += move.product_qty
447                 result[mrp_production.id] = done / mrp_production.product_qty * 100
448         return result
449
450     _columns = {
451         'name': fields.char('Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
452         'origin': fields.char('Source Document', size=64, readonly=True, states={'draft': [('readonly', False)]},
453             help="Reference of the document that generated this production order request."),
454         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority',
455             select=True, readonly=True, states=dict.fromkeys(['draft', 'confirmed'], [('readonly', False)])),
456
457         'product_id': fields.many2one('product.product', 'Product', required=True, readonly=True, states={'draft': [('readonly', False)]}),
458         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, readonly=True, states={'draft':[('readonly',False)]}),
459         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)]}),
460         'product_uos_qty': fields.float('Product UoS Quantity', readonly=True, states={'draft': [('readonly', False)]}),
461         'product_uos': fields.many2one('product.uom', 'Product UoS', readonly=True, states={'draft': [('readonly', False)]}),
462         'progress': fields.function(_get_progress, type='float',
463             string='Production progress'),
464
465         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
466             readonly=True, states={'draft':[('readonly',False)]},
467             help="Location where the system will look for components."),
468         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
469             readonly=True, states={'draft':[('readonly',False)]},
470             help="Location where the system will stock the finished products."),
471         'date_planned': fields.datetime('Scheduled Date', required=True, select=1, readonly=True, states={'draft':[('readonly',False)]}),
472         'date_start': fields.datetime('Start Date', select=True, readonly=True),
473         'date_finished': fields.datetime('End Date', select=True, readonly=True),
474         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)], readonly=True, states={'draft':[('readonly',False)]},
475             help="Bill of Materials allow you to define the list of required raw materials to make a finished product."),
476         'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null', readonly=True, states={'draft':[('readonly',False)]},
477             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."),
478         'picking_id': fields.many2one('stock.picking', 'Picking List', readonly=True, ondelete="restrict",
479             help="This is the Internal Picking List that brings the finished product to the production plan"),
480         'move_prod_id': fields.many2one('stock.move', 'Product Move', readonly=True),
481         'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products to Consume',
482             domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
483         'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products',
484             domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
485         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Products to Produce',
486             domain=[('state','not in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
487         'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Produced Products',
488             domain=[('state','in', ('done', 'cancel'))], readonly=True, states={'draft':[('readonly',False)]}),
489         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods',
490             readonly=True, states={'draft':[('readonly',False)]}),
491         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation',
492             readonly=True, states={'draft':[('readonly',False)]}),
493         'state': fields.selection(
494             [('draft', 'New'), ('cancel', 'Cancelled'), ('picking_except', 'Picking Exception'), ('confirmed', 'Awaiting Raw Materials'),
495                 ('ready', 'Ready to Produce'), ('in_production', 'Production Started'), ('done', 'Done')],
496             string='Status', readonly=True,
497             track_visibility='onchange',
498             help="When the production order is created the status is set to 'Draft'.\n\
499                 If the order is confirmed the status is set to 'Waiting Goods'.\n\
500                 If any exceptions are there, the status is set to 'Picking Exception'.\n\
501                 If the stock is available then the status is set to 'Ready to Produce'.\n\
502                 When the production gets started then the status is set to 'In Production'.\n\
503                 When the production is over, the status is set to 'Done'."),
504         'hour_total': fields.function(_production_calc, type='float', string='Total Hours', multi='workorder', store=True),
505         'cycle_total': fields.function(_production_calc, type='float', string='Total Cycles', multi='workorder', store=True),
506         'user_id':fields.many2one('res.users', 'Responsible'),
507         'company_id': fields.many2one('res.company','Company',required=True),
508     }
509     _defaults = {
510         'priority': lambda *a: '1',
511         'state': lambda *a: 'draft',
512         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
513         'product_qty':  lambda *a: 1.0,
514         'user_id': lambda self, cr, uid, c: uid,
515         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
516         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
517         'location_src_id': _src_id_default,
518         'location_dest_id': _dest_id_default
519     }
520     _sql_constraints = [
521         ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
522     ]
523     _order = 'priority desc, date_planned asc';
524
525     def _check_qty(self, cr, uid, ids, context=None):
526         for order in self.browse(cr, uid, ids, context=context):
527             if order.product_qty <= 0:
528                 return False
529         return True
530
531     _constraints = [
532         (_check_qty, 'Order quantity cannot be negative or zero!', ['product_qty']),
533     ]
534
535     def unlink(self, cr, uid, ids, context=None):
536         for production in self.browse(cr, uid, ids, context=context):
537             if production.state not in ('draft', 'cancel'):
538                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a manufacturing order in state \'%s\'.') % production.state)
539         return super(mrp_production, self).unlink(cr, uid, ids, context=context)
540
541     def copy(self, cr, uid, id, default=None, context=None):
542         if default is None:
543             default = {}
544         default.update({
545             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
546             'move_lines' : [],
547             'move_lines2' : [],
548             'move_created_ids' : [],
549             'move_created_ids2' : [],
550             'product_lines' : [],
551             'move_prod_id' : False,
552             'picking_id' : False
553         })
554         return super(mrp_production, self).copy(cr, uid, id, default, context)
555
556     def location_id_change(self, cr, uid, ids, src, dest, context=None):
557         """ Changes destination location if source location is changed.
558         @param src: Source location id.
559         @param dest: Destination location id.
560         @return: Dictionary of values.
561         """
562         if dest:
563             return {}
564         if src:
565             return {'value': {'location_dest_id': src}}
566         return {}
567
568     def product_id_change(self, cr, uid, ids, product_id, product_qty=0, context=None):
569         print 'product_id_change>>>>>>>>>>\n\n\n'
570         """ Finds UoM of changed product.
571         @param product_id: Id of changed product.
572         @return: Dictionary of values.
573         """
574         if not product_id:
575             return {'value': {
576                 'product_uom': False,
577                 'bom_id': False,
578                 'routing_id': False,
579                 'product_uos_qty': False, 
580                 'product_uos': False
581             }}
582         bom_obj = self.pool.get('mrp.bom')
583         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
584         bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
585         routing_id = False
586         if bom_id:
587             bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
588             routing_id = bom_point.routing_id.id or False
589
590         product_uom_id = product.uom_id and product.uom_id.id or False
591         product_uos_id = product.uos_id and product.uos_id.id or False
592         result={'value': {}}
593         if product.uos_id.id:
594             result['value']['product_uos_qty'] = product_qty * product.uos_coeff
595             result['value']['product_uos'] = product.uos_id.id
596         else:
597             result['value']['product_uos_qty'] = 0
598         
599         result['value']['product_uom'] = product_uom_id
600         result['value']['bom_id'] = bom_id
601         result['value']['routing_id'] = routing_id
602         return result
603
604     def onchange_product_qty_change(self, cr, uid, ids, product_id, qty=0, context=None):
605         if product_id:
606             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
607             if prod.uos_id:
608                 return {'value': {'product_uos': prod.uos_id.id, 'product_uos_qty': qty * prod.uos_coeff}}
609         return {}
610
611     def bom_id_change(self, cr, uid, ids, bom_id, context=None):
612         """ Finds routing for changed BoM.
613         @param product: Id of product.
614         @return: Dictionary of values.
615         """
616         if not bom_id:
617             return {'value': {
618                 'routing_id': False
619             }}
620         bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id, context=context)
621         routing_id = bom_point.routing_id.id or False
622         result = {
623             'routing_id': routing_id
624         }
625         return {'value': result}
626
627     def action_picking_except(self, cr, uid, ids):
628         """ Changes the state to Exception.
629         @return: True
630         """
631         self.write(cr, uid, ids, {'state': 'picking_except'})
632         return True
633
634     def action_compute(self, cr, uid, ids, properties=None, context=None):
635         """ Computes bills of material of a product.
636         @param properties: List containing dictionaries of properties.
637         @return: No. of products.
638         """
639         if properties is None:
640             properties = []
641         results = []
642         bom_obj = self.pool.get('mrp.bom')
643         uom_obj = self.pool.get('product.uom')
644         prod_line_obj = self.pool.get('mrp.production.product.line')
645         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
646         for production in self.browse(cr, uid, ids):
647
648             p_ids = prod_line_obj.search(cr, SUPERUSER_ID, [('production_id', '=', production.id)], context=context)
649             prod_line_obj.unlink(cr, SUPERUSER_ID, p_ids, context=context)
650             w_ids = workcenter_line_obj.search(cr, SUPERUSER_ID, [('production_id', '=', production.id)], context=context)
651             workcenter_line_obj.unlink(cr, SUPERUSER_ID, w_ids, context=context)
652
653             bom_point = production.bom_id
654             bom_id = production.bom_id.id
655             if not bom_point:
656                 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
657                 if bom_id:
658                     bom_point = bom_obj.browse(cr, uid, bom_id)
659                     routing_id = bom_point.routing_id.id or False
660                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
661
662             if not bom_id:
663                 raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
664             factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
665             res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
666             results = res[0]
667             results2 = res[1]
668             for line in results:
669                 line['production_id'] = production.id
670                 prod_line_obj.create(cr, uid, line)
671             for line in results2:
672                 line['production_id'] = production.id
673                 workcenter_line_obj.create(cr, uid, line)
674         return len(results)
675
676     def action_cancel(self, cr, uid, ids, context=None):
677         """ Cancels the production order and related stock moves.
678         @return: True
679         """
680         if context is None:
681             context = {}
682         move_obj = self.pool.get('stock.move')
683         for production in self.browse(cr, uid, ids, context=context):
684             if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
685                 raise osv.except_osv(
686                     _('Cannot cancel manufacturing order!'),
687                     _('You must first cancel related internal picking attached to this manufacturing order.'))
688             if production.move_created_ids:
689                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
690             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
691         self.write(cr, uid, ids, {'state': 'cancel'})
692         return True
693
694     def action_ready(self, cr, uid, ids, context=None):
695         """ Changes the production state to Ready and location id of stock move.
696         @return: True
697         """
698         move_obj = self.pool.get('stock.move')
699         self.write(cr, uid, ids, {'state': 'ready'})
700
701         for (production_id,name) in self.name_get(cr, uid, ids):
702             production = self.browse(cr, uid, production_id)
703             if production.move_prod_id and production.move_prod_id.location_id.id != production.location_dest_id.id:
704                 move_obj.write(cr, uid, [production.move_prod_id.id],
705                         {'location_id': production.location_dest_id.id})
706         return True
707
708     def action_production_end(self, cr, uid, ids, context=None):
709         """ Changes production state to Finish and writes finished date.
710         @return: True
711         """
712         for production in self.browse(cr, uid, ids):
713             self._costs_generate(cr, uid, production)
714         write_res = self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
715         return write_res
716
717     def test_production_done(self, cr, uid, ids):
718         """ Tests whether production is done or not.
719         @return: True or False
720         """
721         res = True
722         for production in self.browse(cr, uid, ids):
723             if production.move_lines:
724                 res = False
725
726             if production.move_created_ids:
727                 res = False
728         return res
729
730     def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
731         """ Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
732             it's always equal to the quantity encoded in the production order or the production wizard, but if the
733             module mrp_subproduct is installed, then we must use the move_id to identify the product to produce
734             and its quantity.
735         :param production_id: ID of the mrp.order
736         :param move_id: ID of the stock move that needs to be produced. Will be used in mrp_subproduct.
737         :return: The factor to apply to the quantity that we should produce for the given production order.
738         """
739         return 1
740
741     def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
742         """ To produce final product based on production mode (consume/consume&produce).
743         If Production mode is consume, all stock move lines of raw materials will be done/consumed.
744         If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
745         and stock move lines of final product will be also done/produced.
746         @param production_id: the ID of mrp.production object
747         @param production_qty: specify qty to produce
748         @param production_mode: specify production mode (consume/consume&produce).
749         @return: True
750         """
751         stock_mov_obj = self.pool.get('stock.move')
752         production = self.browse(cr, uid, production_id, context=context)
753
754         produced_qty = 0
755         for produced_product in production.move_created_ids2:
756             if (produced_product.scrapped) or (produced_product.product_id.id != production.product_id.id):
757                 continue
758             produced_qty += produced_product.product_qty
759         if production_mode in ['consume','consume_produce']:
760             consumed_data = {}
761
762             # Calculate already consumed qtys
763             for consumed in production.move_lines2:
764                 if consumed.scrapped:
765                     continue
766                 if not consumed_data.get(consumed.product_id.id, False):
767                     consumed_data[consumed.product_id.id] = 0
768                 consumed_data[consumed.product_id.id] += consumed.product_qty
769
770             # Find product qty to be consumed and consume it
771             for scheduled in production.product_lines:
772
773                 # total qty of consumed product we need after this consumption
774                 total_consume = ((production_qty + produced_qty) * scheduled.product_qty / production.product_qty)
775
776                 # qty available for consume and produce
777                 qty_avail = scheduled.product_qty - consumed_data.get(scheduled.product_id.id, 0.0)
778
779                 if qty_avail <= 0.0:
780                     # there will be nothing to consume for this raw material
781                     continue
782
783                 raw_product = [move for move in production.move_lines if move.product_id.id==scheduled.product_id.id]
784                 if raw_product:
785                     # qtys we have to consume
786                     qty = total_consume - consumed_data.get(scheduled.product_id.id, 0.0)
787                     if float_compare(qty, qty_avail, precision_rounding=scheduled.product_id.uom_id.rounding) == 1:
788                         # if qtys we have to consume is more than qtys available to consume
789                         prod_name = scheduled.product_id.name_get()[0][1]
790                         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))
791                     if qty <= 0.0:
792                         # we already have more qtys consumed than we need
793                         continue
794
795                     raw_product[0].action_consume(qty, raw_product[0].location_id.id, context=context)
796
797         if production_mode == 'consume_produce':
798             # To produce remaining qty of final product
799             #vals = {'state':'confirmed'}
800             #final_product_todo = [x.id for x in production.move_created_ids]
801             #stock_mov_obj.write(cr, uid, final_product_todo, vals)
802             #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
803             produced_products = {}
804             for produced_product in production.move_created_ids2:
805                 if produced_product.scrapped:
806                     continue
807                 if not produced_products.get(produced_product.product_id.id, False):
808                     produced_products[produced_product.product_id.id] = 0
809                 produced_products[produced_product.product_id.id] += produced_product.product_qty
810
811             for produce_product in production.move_created_ids:
812                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
813                 subproduct_factor = self._get_subproduct_factor(cr, uid, production.id, produce_product.id, context=context)
814                 rest_qty = (subproduct_factor * production.product_qty) - produced_qty
815
816                 if rest_qty < production_qty:
817                     prod_name = produce_product.product_id.name_get()[0][1]
818                     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))
819                 if rest_qty > 0 :
820                     stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty), context=context)
821
822         for raw_product in production.move_lines2:
823             new_parent_ids = []
824             parent_move_ids = [x.id for x in raw_product.move_history_ids]
825             for final_product in production.move_created_ids2:
826                 if final_product.id not in parent_move_ids:
827                     new_parent_ids.append(final_product.id)
828             for new_parent_id in new_parent_ids:
829                 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
830         self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
831         self.signal_button_produce_done(cr, uid, [production_id])
832         return True
833
834     def _costs_generate(self, cr, uid, production):
835         """ Calculates total costs at the end of the production.
836         @param production: Id of production order.
837         @return: Calculated amount.
838         """
839         amount = 0.0
840         analytic_line_obj = self.pool.get('account.analytic.line')
841         for wc_line in production.workcenter_lines:
842             wc = wc_line.workcenter_id
843             if wc.costs_journal_id and wc.costs_general_account_id:
844                 # Cost per hour
845                 value = wc_line.hour * wc.costs_hour
846                 account = wc.costs_hour_account_id.id
847                 if value and account:
848                     amount += value
849                     analytic_line_obj.create(cr, uid, {
850                         'name': wc_line.name + ' (H)',
851                         'amount': value,
852                         'account_id': account,
853                         'general_account_id': wc.costs_general_account_id.id,
854                         'journal_id': wc.costs_journal_id.id,
855                         'ref': wc.code,
856                         'product_id': wc.product_id.id,
857                         'unit_amount': wc_line.hour,
858                         'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
859                     } )
860                 # Cost per cycle
861                 value = wc_line.cycle * wc.costs_cycle
862                 account = wc.costs_cycle_account_id.id
863                 if value and account:
864                     amount += value
865                     analytic_line_obj.create(cr, uid, {
866                         'name': wc_line.name+' (C)',
867                         'amount': value,
868                         'account_id': account,
869                         'general_account_id': wc.costs_general_account_id.id,
870                         'journal_id': wc.costs_journal_id.id,
871                         'ref': wc.code,
872                         'product_id': wc.product_id.id,
873                         'unit_amount': wc_line.cycle,
874                         'product_uom_id': wc.product_id and wc.product_id.uom_id.id or False
875                     } )
876         return amount
877
878     def action_in_production(self, cr, uid, ids, context=None):
879         """ Changes state to In Production and writes starting date.
880         @return: True
881         """
882         return self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
883
884     def test_if_product(self, cr, uid, ids):
885         """
886         @return: True or False
887         """
888         res = True
889         for production in self.browse(cr, uid, ids):
890             if not production.product_lines:
891                 if not self.action_compute(cr, uid, [production.id]):
892                     res = False
893         return res
894
895     def _get_auto_picking(self, cr, uid, production):
896         return True
897
898     def _make_production_line_procurement(self, cr, uid, production_line, shipment_move_id, context=None):
899         procurement_order = self.pool.get('procurement.order')
900         production = production_line.production_id
901         location_id = production.location_src_id.id
902         date_planned = production.date_planned
903         procurement_name = (production.origin or '').split(':')[0] + ':' + production.name
904         procurement_id = procurement_order.create(cr, uid, {
905                     'name': procurement_name,
906                     'origin': procurement_name,
907                     'date_planned': date_planned,
908                     'product_id': production_line.product_id.id,
909                     'product_qty': production_line.product_qty,
910                     'product_uom': production_line.product_uom.id,
911                     'product_uos_qty': production_line.product_uos and production_line.product_qty or False,
912                     'product_uos': production_line.product_uos and production_line.product_uos.id or False,
913                     'location_id': location_id,
914                     'procure_method': production_line.product_id.procure_method,
915                     'move_id': shipment_move_id,
916                     'company_id': production.company_id.id,
917                 })
918         procurement_order.signal_button_confirm(cr, uid, [procurement_id])
919         return procurement_id
920
921     def _make_production_internal_shipment_line(self, cr, uid, production_line, shipment_id, parent_move_id, destination_location_id=False, context=None):
922         stock_move = self.pool.get('stock.move')
923         production = production_line.production_id
924         date_planned = production.date_planned
925         # Internal shipment is created for Stockable and Consumer Products
926         if production_line.product_id.type not in ('product', 'consu'):
927             return False
928         source_location_id = production.location_src_id.id
929         if not destination_location_id:
930             destination_location_id = source_location_id
931         return stock_move.create(cr, uid, {
932                         'name': production.name,
933                         'picking_id': shipment_id,
934                         'product_id': production_line.product_id.id,
935                         'product_qty': production_line.product_qty,
936                         'product_uom': production_line.product_uom.id,
937                         'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
938                         'product_uos': production_line.product_uos and production_line.product_uos.id or False,
939                         'date': date_planned,
940                         'move_dest_id': parent_move_id,
941                         'location_id': source_location_id,
942                         'location_dest_id': destination_location_id,
943                         'state': 'waiting',
944                         'company_id': production.company_id.id,
945                 })
946
947     def _make_production_internal_shipment(self, cr, uid, production, context=None):
948         ir_sequence = self.pool.get('ir.sequence')
949         stock_picking = self.pool.get('stock.picking')
950         routing_loc = None
951         pick_type = 'internal'
952         partner_id = False
953
954         # Take routing address as a Shipment Address.
955         # If usage of routing location is a internal, make outgoing shipment otherwise internal shipment
956         if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
957             routing_loc = production.bom_id.routing_id.location_id
958             if routing_loc.usage != 'internal':
959                 pick_type = 'out'
960             partner_id = routing_loc.partner_id and routing_loc.partner_id.id or False
961
962         # Take next Sequence number of shipment base on type
963         pick_name = ir_sequence.get(cr, uid, 'stock.picking.' + pick_type)
964
965         picking_id = stock_picking.create(cr, uid, {
966             'name': pick_name,
967             'origin': (production.origin or '').split(':')[0] + ':' + production.name,
968             'type': pick_type,
969             'move_type': 'one',
970             'state': 'auto',
971             'partner_id': partner_id,
972             'auto_picking': self._get_auto_picking(cr, uid, production),
973             'company_id': production.company_id.id,
974         })
975         production.write({'picking_id': picking_id}, context=context)
976         return picking_id
977
978     def _make_production_produce_line(self, cr, uid, production, context=None):
979         stock_move = self.pool.get('stock.move')
980         source_location_id = production.product_id.property_stock_production.id
981         destination_location_id = production.location_dest_id.id
982         data = {
983             'name': production.name,
984             'date': production.date_planned,
985             'product_id': production.product_id.id,
986             'product_qty': production.product_qty,
987             'product_uom': production.product_uom.id,
988             'product_uos_qty': production.product_uos and production.product_uos_qty or False,
989             'product_uos': production.product_uos and production.product_uos.id or False,
990             'location_id': source_location_id,
991             'location_dest_id': destination_location_id,
992             'move_dest_id': production.move_prod_id.id,
993             'state': 'waiting',
994             'company_id': production.company_id.id,
995         }
996         move_id = stock_move.create(cr, uid, data, context=context)
997         production.write({'move_created_ids': [(6, 0, [move_id])]}, context=context)
998         return move_id
999
1000     def _make_production_consume_line(self, cr, uid, production_line, parent_move_id, source_location_id=False, context=None):
1001         stock_move = self.pool.get('stock.move')
1002         production = production_line.production_id
1003         # Internal shipment is created for Stockable and Consumer Products
1004         if production_line.product_id.type not in ('product', 'consu'):
1005             return False
1006         destination_location_id = production.product_id.property_stock_production.id
1007         if not source_location_id:
1008             source_location_id = production.location_src_id.id
1009         move_id = stock_move.create(cr, uid, {
1010             'name': production.name,
1011             'date': production.date_planned,
1012             'product_id': production_line.product_id.id,
1013             'product_qty': production_line.product_qty,
1014             'product_uom': production_line.product_uom.id,
1015             'product_uos_qty': production_line.product_uos and production_line.product_uos_qty or False,
1016             'product_uos': production_line.product_uos and production_line.product_uos.id or False,
1017             'location_id': source_location_id,
1018             'location_dest_id': destination_location_id,
1019             'move_dest_id': parent_move_id,
1020             'state': 'waiting',
1021             'company_id': production.company_id.id,
1022         })
1023         production.write({'move_lines': [(4, move_id)]}, context=context)
1024         return move_id
1025
1026     def action_confirm(self, cr, uid, ids, context=None):
1027         """ Confirms production order.
1028         @return: Newly generated Shipment Id.
1029         """
1030         shipment_id = False
1031         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)])
1032         self.action_compute(cr, uid, uncompute_ids, context=context)
1033         for production in self.browse(cr, uid, ids, context=context):
1034             shipment_id = self._make_production_internal_shipment(cr, uid, production, context=context)
1035             produce_move_id = self._make_production_produce_line(cr, uid, production, context=context)
1036
1037             # Take routing location as a Source Location.
1038             source_location_id = production.location_src_id.id
1039             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
1040                 source_location_id = production.bom_id.routing_id.location_id.id
1041
1042             for line in production.product_lines:
1043                 consume_move_id = self._make_production_consume_line(cr, uid, line, produce_move_id, source_location_id=source_location_id, context=context)
1044                 shipment_move_id = self._make_production_internal_shipment_line(cr, uid, line, shipment_id, consume_move_id,\
1045                                  destination_location_id=source_location_id, context=context)
1046                 self._make_production_line_procurement(cr, uid, line, shipment_move_id, context=context)
1047
1048             self.pool.get('stock.picking').signal_button_confirm(cr, uid, [shipment_id])
1049             production.write({'state':'confirmed'}, context=context)
1050         return shipment_id
1051
1052     def force_production(self, cr, uid, ids, *args):
1053         """ Assigns products.
1054         @param *args: Arguments
1055         @return: True
1056         """
1057         pick_obj = self.pool.get('stock.picking')
1058         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
1059         return True
1060
1061
1062 class mrp_production_workcenter_line(osv.osv):
1063     _name = 'mrp.production.workcenter.line'
1064     _description = 'Work Order'
1065     _order = 'sequence'
1066     _inherit = ['mail.thread']
1067
1068     _columns = {
1069         'name': fields.char('Work Order', size=64, required=True),
1070         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
1071         'cycle': fields.float('Number of Cycles', digits=(16,2)),
1072         'hour': fields.float('Number of Hours', digits=(16,2)),
1073         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
1074         'production_id': fields.many2one('mrp.production', 'Manufacturing Order',
1075             track_visibility='onchange', select=True, ondelete='cascade', required=True),
1076     }
1077     _defaults = {
1078         'sequence': lambda *a: 1,
1079         'hour': lambda *a: 0,
1080         'cycle': lambda *a: 0,
1081     }
1082
1083 class mrp_production_product_line(osv.osv):
1084     _name = 'mrp.production.product.line'
1085     _description = 'Production Scheduled Product'
1086     _columns = {
1087         'name': fields.char('Name', size=64, required=True),
1088         'product_id': fields.many2one('product.product', 'Product', required=True),
1089         'product_qty': fields.float('Product Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
1090         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
1091         'product_uos_qty': fields.float('Product UOS Quantity'),
1092         'product_uos': fields.many2one('product.uom', 'Product UOS'),
1093         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
1094     }
1095
1096 class product_product(osv.osv):
1097     _inherit = "product.product"
1098     _columns = {
1099         'bom_ids': fields.one2many('mrp.bom', 'product_id', 'Bill of Materials'),
1100     }
1101
1102 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: