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