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