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