[MERGE]: Merge with lp:openobject-trunk-dev-addons2
[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, ),
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' is ready to produce.") % ( name,)
651             self.log(cr, uid, production_id, message)
652         return True
653
654     def action_production_end(self, cr, uid, ids):
655         """ Changes production state to Finish and writes finished date.
656         @return: True
657         """
658         for production in self.browse(cr, uid, ids):
659             self._costs_generate(cr, uid, production)
660         return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
661
662     def test_production_done(self, cr, uid, ids):
663         """ Tests whether production is done or not.
664         @return: True or False
665         """
666         res = True
667         for production in self.browse(cr, uid, ids):
668             if production.move_lines:
669                res = False
670
671             if production.move_created_ids:
672                res = False
673         return res
674
675     def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
676         """ To produce final product based on production mode (consume/consume&produce).
677         If Production mode is consume, all stock move lines of raw materials will be done/consumed.
678         If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
679         and stock move lines of final product will be also done/produced.
680         @param production_id: the ID of mrp.production object
681         @param production_qty: specify qty to produce
682         @param production_mode: specify production mode (consume/consume&produce).
683         @return: True
684         """
685
686         stock_mov_obj = self.pool.get('stock.move')
687         production = self.browse(cr, uid, production_id)
688
689         final_product_todo = []
690
691         produced_qty = 0
692         if production_mode == 'consume_produce':
693             produced_qty = production_qty
694
695         for produced_product in production.move_created_ids2:
696             if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
697                 continue
698             produced_qty += produced_product.product_qty
699
700         if production_mode in ['consume','consume_produce']:
701             consumed_products = {}
702             check = {}
703             scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
704
705             for consumed_product in production.move_lines2:
706                 consumed = consumed_product.product_qty
707                 if consumed_product.scrapped:
708                     continue
709                 if not consumed_products.get(consumed_product.product_id.id, False):
710                     consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
711                     check[consumed_product.product_id.id] = 0
712                 for f in production.product_lines:
713                     if f.product_id.id == consumed_product.product_id.id:
714                         if (len(production.move_lines2) - scrapped) > len(production.product_lines):
715                             check[consumed_product.product_id.id] += consumed_product.product_qty
716                             consumed = check[consumed_product.product_id.id]
717                         rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
718                         consumed_products[consumed_product.product_id.id] = rest_consumed
719
720             for raw_product in production.move_lines:
721                 for f in production.product_lines:
722                     if f.product_id.id == raw_product.product_id.id:
723                         consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
724                         if consumed_qty == 0:
725                             consumed_qty = production_qty * f.product_qty / production.product_qty
726                         if consumed_qty > 0:
727                             stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
728
729         if production_mode == 'consume_produce':
730             # To produce remaining qty of final product
731             vals = {'state':'confirmed'}
732             final_product_todo = [x.id for x in production.move_created_ids]
733             stock_mov_obj.write(cr, uid, final_product_todo, vals)
734             produced_products = {}
735             for produced_product in production.move_created_ids2:
736                 if produced_product.scrapped:
737                     continue
738                 if not produced_products.get(produced_product.product_id.id, False):
739                     produced_products[produced_product.product_id.id] = 0
740                 produced_products[produced_product.product_id.id] += produced_product.product_qty
741
742             for produce_product in production.move_created_ids:
743                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
744                 rest_qty = production.product_qty - produced_qty
745                 if rest_qty <= production_qty:
746                    production_qty = rest_qty
747                 if rest_qty > 0 :
748                     stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, production.location_dest_id.id, context=context)
749
750         for raw_product in production.move_lines2:
751             new_parent_ids = []
752             parent_move_ids = [x.id for x in raw_product.move_history_ids]
753             for final_product in production.move_created_ids2:
754                 if final_product.id not in parent_move_ids:
755                     new_parent_ids.append(final_product.id)
756             for new_parent_id in new_parent_ids:
757                 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
758
759         wf_service = netsvc.LocalService("workflow")
760         wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
761         return True
762
763     def _costs_generate(self, cr, uid, production):
764         """ Calculates total costs at the end of the production.
765         @param production: Id of production order.
766         @return: Calculated amount.
767         """
768         amount = 0.0
769         analytic_line_obj = self.pool.get('account.analytic.line')
770         for wc_line in production.workcenter_lines:
771             wc = wc_line.workcenter_id
772             if wc.costs_journal_id and wc.costs_general_account_id:
773                 value = wc_line.hour * wc.costs_hour
774                 account = wc.costs_hour_account_id.id
775                 if value and account:
776                     amount += value
777                     analytic_line_obj.create(cr, uid, {
778                         'name': wc_line.name + ' (H)',
779                         'amount': value,
780                         'account_id': account,
781                         'general_account_id': wc.costs_general_account_id.id,
782                         'journal_id': wc.costs_journal_id.id,
783                         'code': wc.code
784                     } )
785             if wc.costs_journal_id and wc.costs_general_account_id:
786                 value = wc_line.cycle * wc.costs_cycle
787                 account = wc.costs_cycle_account_id.id
788                 if value and account:
789                     amount += value
790                     analytic_line_obj.create(cr, uid, {
791                         'name': wc_line.name+' (C)',
792                         'amount': value,
793                         'account_id': account,
794                         'general_account_id': wc.costs_general_account_id.id,
795                         'journal_id': wc.costs_journal_id.id,
796                         'code': wc.code
797                     } )
798         return amount
799
800     def action_in_production(self, cr, uid, ids):
801         """ Changes state to In Production and writes starting date.
802         @return: True
803         """
804         self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
805         return True
806
807     def test_if_product(self, cr, uid, ids):
808         """
809         @return: True or False
810         """
811         res = True
812         for production in self.browse(cr, uid, ids):
813             if not production.product_lines:
814                 if not self.action_compute(cr, uid, [production.id]):
815                     res = False
816         return res
817
818     def _get_auto_picking(self, cr, uid, production):
819         return True
820
821     def action_confirm(self, cr, uid, ids):
822         """ Confirms production order.
823         @return: Newly generated picking Id.
824         """
825         picking_id = False
826         proc_ids = []
827         seq_obj = self.pool.get('ir.sequence')
828         pick_obj = self.pool.get('stock.picking')
829         move_obj = self.pool.get('stock.move')
830         proc_obj = self.pool.get('procurement.order')
831         wf_service = netsvc.LocalService("workflow")
832         for production in self.browse(cr, uid, ids):
833             if not production.product_lines:
834                 self.action_compute(cr, uid, [production.id])
835                 production = self.browse(cr, uid, [production.id])[0]
836             routing_loc = None
837             pick_type = 'internal'
838             address_id = False
839             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
840                 routing_loc = production.bom_id.routing_id.location_id
841                 if routing_loc.usage <> 'internal':
842                     pick_type = 'out'
843                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
844                 routing_loc = routing_loc.id
845             pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
846             picking_id = pick_obj.create(cr, uid, {
847                 'name': pick_name,
848                 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
849                 'type': pick_type,
850                 'move_type': 'one',
851                 'state': 'auto',
852                 'address_id': address_id,
853                 'auto_picking': self._get_auto_picking(cr, uid, production),
854                 'company_id': production.company_id.id,
855             })
856
857             source = production.product_id.product_tmpl_id.property_stock_production.id
858             data = {
859                 'name':'PROD:' + production.name,
860                 'date': production.date_planned,
861                 'product_id': production.product_id.id,
862                 'product_qty': production.product_qty,
863                 'product_uom': production.product_uom.id,
864                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
865                 'product_uos': production.product_uos and production.product_uos.id or False,
866                 'location_id': source,
867                 'location_dest_id': production.location_dest_id.id,
868                 'move_dest_id': production.move_prod_id.id,
869                 'state': 'waiting',
870                 'company_id': production.company_id.id,
871             }
872             res_final_id = move_obj.create(cr, uid, data)
873
874             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
875             moves = []
876             for line in production.product_lines:
877                 move_id = False
878                 newdate = production.date_planned
879                 if line.product_id.type in ('product', 'consu'):
880                     res_dest_id = move_obj.create(cr, uid, {
881                         'name':'PROD:' + production.name,
882                         'date': production.date_planned,
883                         'product_id': line.product_id.id,
884                         'product_qty': line.product_qty,
885                         'product_uom': line.product_uom.id,
886                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
887                         'product_uos': line.product_uos and line.product_uos.id or False,
888                         'location_id': routing_loc or production.location_src_id.id,
889                         'location_dest_id': source,
890                         'move_dest_id': res_final_id,
891                         'state': 'waiting',
892                         'company_id': production.company_id.id,
893                     })
894                     moves.append(res_dest_id)
895                     move_id = move_obj.create(cr, uid, {
896                         'name':'PROD:' + production.name,
897                         'picking_id':picking_id,
898                         'product_id': line.product_id.id,
899                         'product_qty': line.product_qty,
900                         'product_uom': line.product_uom.id,
901                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
902                         'product_uos': line.product_uos and line.product_uos.id or False,
903                         'date': newdate,
904                         'move_dest_id': res_dest_id,
905                         'location_id': production.location_src_id.id,
906                         'location_dest_id': routing_loc or production.location_src_id.id,
907                         'state': 'waiting',
908                         'company_id': production.company_id.id,
909                     })
910                 proc_id = proc_obj.create(cr, uid, {
911                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
912                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
913                     'date_planned': newdate,
914                     'product_id': line.product_id.id,
915                     'product_qty': line.product_qty,
916                     'product_uom': line.product_uom.id,
917                     'product_uos_qty': line.product_uos and line.product_qty or False,
918                     'product_uos': line.product_uos and line.product_uos.id or False,
919                     'location_id': production.location_src_id.id,
920                     'procure_method': line.product_id.procure_method,
921                     'move_id': move_id,
922                     'company_id': production.company_id.id,
923                 })
924                 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
925                 proc_ids.append(proc_id)
926             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
927             self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
928             message = _("Manufacturing order '%s' is scheduled for the %s.") % (
929                 production.name,
930                 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
931             )
932             self.log(cr, uid, production.id, message)
933         return picking_id
934
935     def force_production(self, cr, uid, ids, *args):
936         """ Assigns products.
937         @param *args: Arguments
938         @return: True
939         """
940         pick_obj = self.pool.get('stock.picking')
941         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
942         return True
943
944 mrp_production()
945
946 class mrp_production_workcenter_line(osv.osv):
947     _name = 'mrp.production.workcenter.line'
948     _description = 'Work Order'
949     _order = 'sequence'
950
951     _columns = {
952         'name': fields.char('Work Order', size=64, required=True),
953         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
954         'cycle': fields.float('Nbr of cycles', digits=(16,2)),
955         'hour': fields.float('Nbr of hours', digits=(16,2)),
956         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
957         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
958     }
959     _defaults = {
960         'sequence': lambda *a: 1,
961         'hour': lambda *a: 0,
962         'cycle': lambda *a: 0,
963     }
964 mrp_production_workcenter_line()
965
966 class mrp_production_product_line(osv.osv):
967     _name = 'mrp.production.product.line'
968     _description = 'Production Scheduled Product'
969     _columns = {
970         'name': fields.char('Name', size=64, required=True),
971         'product_id': fields.many2one('product.product', 'Product', required=True),
972         'product_qty': fields.float('Product Qty', required=True),
973         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
974         'product_uos_qty': fields.float('Product UOS Qty'),
975         'product_uos': fields.many2one('product.uom', 'Product UOS'),
976         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
977     }
978 mrp_production_product_line()
979
980 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: