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