[FIX] hr_timesheet_sheet, mrp: don't pass a dict or list to fromkeys second argument...
[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         cr.execute("SELECT production_id, move_id from mrp_production_move_ids where production_id in %s and move_id in %s",
365             [tuple(ids), tuple(valid_move_ids)])
366         related_move_map = cr.fetchall()
367         related_move_dict = dict((k, list(set([v[1] for v in itr]))) for k, itr in groupby(related_move_map, itemgetter(0)))
368         res.update(related_move_dict)
369
370         return res
371
372 class one2many_domain(fields.one2many):
373     def set(self, cr, obj, id, field, values, user=None, context=None):
374         return super(one2many_domain, self).set(cr, obj, id, field, values,
375                                             user=user, context=context)
376
377     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
378         if not context:
379             context = {}
380
381         move_obj = obj.pool.get('stock.move')
382         res = {}
383         for i in ids:
384             res[i] = []
385         move_ids = move_obj.search(cr, user, self._domain+[('production_id', 'in', tuple(ids))], context=context)
386         related_move_dict = dict([(o.production_id.id, [o.id]) for o in move_obj.browse(cr, user, move_ids, context=context)])
387         res.update(related_move_dict)
388
389         return res
390
391 class mrp_production(osv.osv):
392     """
393     Production Orders / Manufacturing Orders
394     """
395     _name = 'mrp.production'
396     _description = 'Manufacturing Order'
397     _date_name  = 'date_planned'
398
399     def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
400         """ Calculates total hours and total no. of cycles for a production order.
401         @param prop: Name of field.
402         @param unknow_none:
403         @return: Dictionary of values.
404         """
405         result = {}
406         for prod in self.browse(cr, uid, ids, context=context):
407             result[prod.id] = {
408                 'hour_total': 0.0,
409                 'cycle_total': 0.0,
410             }
411             for wc in prod.workcenter_lines:
412                 result[prod.id]['hour_total'] += wc.hour
413                 result[prod.id]['cycle_total'] += wc.cycle
414         return result
415
416     def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
417         """ Finds production end date.
418         @param prop: Name of field.
419         @param unknow_none:
420         @return: Dictionary of values.
421         """
422         result = {}
423         for prod in self.browse(cr, uid, ids, context=context):
424             result[prod.id] = prod.date_planned
425         return result
426
427     def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
428         """ Finds production planned date.
429         @param prop: Name of field.
430         @param unknow_none:
431         @return: Dictionary of values.
432         """
433         result = {}
434         for prod in self.browse(cr, uid, ids, context=context):
435             result[prod.id] = prod.date_planned[:10]
436         return result
437
438     _columns = {
439         'name': fields.char('Reference', size=64, required=True),
440         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
441         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
442
443         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
444         'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
445         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
446         'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
447         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
448
449         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
450             help="Location where the system will look for components."),
451         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
452             help="Location where the system will stock the finished products."),
453
454         'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
455         'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
456         'date_planned': fields.datetime('Scheduled date', required=True, select=1),
457         'date_start': fields.datetime('Start Date'),
458         'date_finnished': fields.datetime('End Date'),
459
460         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
461         '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."),
462
463         'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
464             help="This is the internal picking list that brings the finished product to the production plan"),
465         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
466         '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)]}),
467         'move_lines2': many2many_domain('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
468         'move_created_ids': one2many_domain('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
469         'move_created_ids2': one2many_domain('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
470         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
471         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
472         '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,
473                                     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\'.\
474                                     \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\'.'),
475         'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
476         'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
477         'company_id': fields.many2one('res.company','Company',required=True),
478     }
479     _defaults = {
480         'priority': lambda *a: '1',
481         'state': lambda *a: 'draft',
482         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
483         'product_qty':  lambda *a: 1.0,
484         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
485         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
486     }
487     _order = 'priority desc, date_planned asc';
488     
489     def _check_qty(self, cr, uid, ids):
490         orders = self.browse(cr, uid, ids)
491         for order in orders:
492             if order.product_qty <= 0:
493                 return False
494         return True
495     
496     _constraints = [
497         (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
498     ]
499     
500     def unlink(self, cr, uid, ids, context=None):
501         productions = self.read(cr, uid, ids, ['state'])
502         unlink_ids = []
503         for s in productions:
504             if s['state'] in ['draft','cancel']:
505                 unlink_ids.append(s['id'])
506             else:
507                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
508         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
509
510     def copy(self, cr, uid, id, default=None, context=None):
511         if default is None:
512             default = {}
513         default.update({
514             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
515             'move_lines' : [],
516             'move_lines2' : [],
517             'move_created_ids' : [],
518             'move_created_ids2' : [],
519             'product_lines' : [],
520             'picking_id': False
521         })
522         return super(mrp_production, self).copy(cr, uid, id, default, context)
523
524     def location_id_change(self, cr, uid, ids, src, dest, context={}):
525         """ Changes destination location if source location is changed.
526         @param src: Source location id.
527         @param dest: Destination location id.
528         @return: Dictionary of values.
529         """
530         if dest:
531             return {}
532         if src:
533             return {'value': {'location_dest_id': src}}
534         return {}
535
536     def product_id_change(self, cr, uid, ids, product_id, context=None):
537         """ Finds UoM of changed product.
538         @param product_id: Id of changed product.
539         @return: Dictionary of values.
540         """
541         if not product_id:
542             return {'value': {
543                 'product_uom': False,
544                 'bom_id': False,
545                 'routing_id': False
546             }}
547         bom_obj = self.pool.get('mrp.bom')
548         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
549         bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
550         routing_id = False
551         if bom_id:
552             bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
553             routing_id = bom_point.routing_id.id or False
554         result = {
555             'product_uom': product.uom_id and product.uom_id.id or False,
556             'bom_id': bom_id,
557             'routing_id': routing_id
558         }
559         return {'value': result}
560
561     def bom_id_change(self, cr, uid, ids, bom_id, context=None):
562         """ Finds routing for changed BoM.
563         @param product: Id of product.
564         @return: Dictionary of values.
565         """
566         if not bom_id:
567             return {'value': {
568                 'routing_id': False
569             }}
570         bom_pool = self.pool.get('mrp.bom')
571         bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
572         routing_id = bom_point.routing_id.id or False
573         result = {
574             'routing_id': routing_id
575         }
576         return {'value': result}
577
578     def action_picking_except(self, cr, uid, ids):
579         """ Changes the state to Exception.
580         @return: True
581         """
582         self.write(cr, uid, ids, {'state': 'picking_except'})
583         return True
584
585     def action_compute(self, cr, uid, ids, properties=[]):
586         """ Computes bills of material of a product.
587         @param properties: List containing dictionaries of properties.
588         @return: No. of products.
589         """
590         results = []
591         bom_obj = self.pool.get('mrp.bom')
592         prod_line_obj = self.pool.get('mrp.production.product.line')
593         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
594         for production in self.browse(cr, uid, ids):
595             cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
596             cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
597             bom_point = production.bom_id
598             bom_id = production.bom_id.id
599             if not bom_point:
600                 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
601                 if bom_id:
602                     bom_point = bom_obj.browse(cr, uid, bom_id)
603                     routing_id = bom_point.routing_id.id or False
604                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
605
606             if not bom_id:
607                 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
608
609             factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
610             res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
611             results = res[0]
612             results2 = res[1]
613             for line in results:
614                 line['production_id'] = production.id
615                 prod_line_obj.create(cr, uid, line)
616             for line in results2:
617                 line['production_id'] = production.id
618                 workcenter_line_obj.create(cr, uid, line)
619         return len(results)
620
621     def action_cancel(self, cr, uid, ids):
622         """ Cancels the production order and related stock moves.
623         @return: True
624         """
625         move_obj = self.pool.get('stock.move')
626         for production in self.browse(cr, uid, ids):
627             if production.move_created_ids:
628                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
629             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
630         self.write(cr, uid, ids, {'state': 'cancel'})
631         return True
632
633     def action_ready(self, cr, uid, ids):
634         """ Changes the production state to Ready and location id of stock move.
635         @return: True
636         """
637         move_obj = self.pool.get('stock.move')
638         self.write(cr, uid, ids, {'state': 'ready'})
639
640         for (production_id,name) in self.name_get(cr, uid, ids):
641             production = self.browse(cr, uid, production_id)
642             if production.move_prod_id:
643                 move_obj.write(cr, uid, [production.move_prod_id.id],
644                         {'location_id': production.location_dest_id.id})
645
646             message = ("Manufacturing Order '%s' for %s %s is Ready to produce.") % (
647                                     name,
648                                     production.product_qty, 
649                                     production.product_id.name, 
650                                    )
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_finnished': 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         for (id, name) in self.pool.get('product.product').name_get(cr, uid, [production.product_id.id]):
762             message = str(production_qty) + ' ' + production.product_uom.name +" '" + name + _("' have been manufactured for ") + production.name
763         self.log(cr, uid, production_id, message)
764         return True
765
766     def _costs_generate(self, cr, uid, production):
767         """ Calculates total costs at the end of the production.
768         @param production: Id of production order.
769         @return: Calculated amount.
770         """
771         amount = 0.0
772         analytic_line_obj = self.pool.get('account.analytic.line')
773         for wc_line in production.workcenter_lines:
774             wc = wc_line.workcenter_id
775             if wc.costs_journal_id and wc.costs_general_account_id:
776                 value = wc_line.hour * wc.costs_hour
777                 account = wc.costs_hour_account_id.id
778                 if value and account:
779                     amount += value
780                     analytic_line_obj.create(cr, uid, {
781                         'name': wc_line.name + ' (H)',
782                         'amount': value,
783                         'account_id': account,
784                         'general_account_id': wc.costs_general_account_id.id,
785                         'journal_id': wc.costs_journal_id.id,
786                         'code': wc.code
787                     } )
788             if wc.costs_journal_id and wc.costs_general_account_id:
789                 value = wc_line.cycle * wc.costs_cycle
790                 account = wc.costs_cycle_account_id.id
791                 if value and account:
792                     amount += value
793                     analytic_line_obj.create(cr, uid, {
794                         'name': wc_line.name+' (C)',
795                         'amount': value,
796                         'account_id': account,
797                         'general_account_id': wc.costs_general_account_id.id,
798                         'journal_id': wc.costs_journal_id.id,
799                         'code': wc.code
800                     } )
801         return amount
802
803     def action_in_production(self, cr, uid, ids):
804         """ Changes state to In Production and writes starting date.
805         @return: True
806         """
807         self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
808         return True
809
810     def test_if_product(self, cr, uid, ids):
811         """
812         @return: True or False
813         """
814         res = True
815         for production in self.browse(cr, uid, ids):
816             if not production.product_lines:
817                 if not self.action_compute(cr, uid, [production.id]):
818                     res = False
819         return res
820
821     def _get_auto_picking(self, cr, uid, production):
822         return True
823
824     def action_confirm(self, cr, uid, ids):
825         """ Confirms production order.
826         @return: Newly generated picking Id.
827         """
828         picking_id = False
829         proc_ids = []
830         seq_obj = self.pool.get('ir.sequence')
831         pick_obj = self.pool.get('stock.picking')
832         move_obj = self.pool.get('stock.move')
833         proc_obj = self.pool.get('procurement.order')
834         wf_service = netsvc.LocalService("workflow")
835         for production in self.browse(cr, uid, ids):
836             if not production.product_lines:
837                 self.action_compute(cr, uid, [production.id])
838                 production = self.browse(cr, uid, [production.id])[0]
839             routing_loc = None
840             pick_type = 'internal'
841             address_id = False
842             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
843                 routing_loc = production.bom_id.routing_id.location_id
844                 if routing_loc.usage <> 'internal':
845                     pick_type = 'out'
846                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
847                 routing_loc = routing_loc.id
848             pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
849             picking_id = pick_obj.create(cr, uid, {
850                 'name': pick_name,
851                 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
852                 'type': pick_type,
853                 'move_type': 'one',
854                 'state': 'auto',
855                 'address_id': address_id,
856                 'auto_picking': self._get_auto_picking(cr, uid, production),
857                 'company_id': production.company_id.id,
858             })
859
860             source = production.product_id.product_tmpl_id.property_stock_production.id
861             data = {
862                 'name':'PROD:' + production.name,
863                 'date_planned': production.date_planned,
864                 'product_id': production.product_id.id,
865                 'product_qty': production.product_qty,
866                 'product_uom': production.product_uom.id,
867                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
868                 'product_uos': production.product_uos and production.product_uos.id or False,
869                 'location_id': source,
870                 'location_dest_id': production.location_dest_id.id,
871                 'move_dest_id': production.move_prod_id.id,
872                 'state': 'waiting',
873                 'company_id': production.company_id.id,
874             }
875             res_final_id = move_obj.create(cr, uid, data)
876              
877             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
878             moves = []
879             for line in production.product_lines:
880                 move_id = False
881                 newdate = production.date_planned
882                 if line.product_id.type in ('product', 'consu'):
883                     res_dest_id = move_obj.create(cr, uid, {
884                         'name':'PROD:' + production.name,
885                         'date_planned': production.date_planned,
886                         'product_id': line.product_id.id,
887                         'product_qty': line.product_qty,
888                         'product_uom': line.product_uom.id,
889                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
890                         'product_uos': line.product_uos and line.product_uos.id or False,
891                         'location_id': routing_loc or production.location_src_id.id,
892                         'location_dest_id': source,
893                         'move_dest_id': res_final_id,
894                         'state': 'waiting',
895                         'company_id': production.company_id.id,
896                     })
897                     moves.append(res_dest_id)
898                     move_id = move_obj.create(cr, uid, {
899                         'name':'PROD:' + production.name,
900                         'picking_id':picking_id,
901                         'product_id': line.product_id.id,
902                         'product_qty': line.product_qty,
903                         'product_uom': line.product_uom.id,
904                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
905                         'product_uos': line.product_uos and line.product_uos.id or False,
906                         'date_planned': newdate,
907                         'move_dest_id': res_dest_id,
908                         'location_id': production.location_src_id.id,
909                         'location_dest_id': routing_loc or production.location_src_id.id,
910                         'state': 'waiting',
911                         'company_id': production.company_id.id,
912                     })
913                 proc_id = proc_obj.create(cr, uid, {
914                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
915                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
916                     'date_planned': newdate,
917                     'product_id': line.product_id.id,
918                     'product_qty': line.product_qty,
919                     'product_uom': line.product_uom.id,
920                     'product_uos_qty': line.product_uos and line.product_qty or False,
921                     'product_uos': line.product_uos and line.product_uos.id or False,
922                     'location_id': production.location_src_id.id,
923                     'procure_method': line.product_id.procure_method,
924                     'move_id': move_id,
925                     'company_id': production.company_id.id,
926                 })
927                 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
928                 proc_ids.append(proc_id)                
929             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
930             self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
931             message = ("%s '%s' %s %s %s %s %s %s.") % (
932                                     _('Manufacturing Order'), 
933                                     production.name,
934                                     _('for'), 
935                                     production.product_qty, 
936                                     production.product_id.name, 
937                                     _('scheduled for date '), 
938                                     datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d'),
939                                     _('is waiting') 
940                                    )
941             self.log(cr, uid, production.id, message)
942         return picking_id
943
944     def force_production(self, cr, uid, ids, *args):
945         """ Assigns products.
946         @param *args: Arguments
947         @return: True
948         """
949         pick_obj = self.pool.get('stock.picking')
950         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
951         return True
952
953 mrp_production()
954
955 class mrp_production_workcenter_line(osv.osv):
956     _name = 'mrp.production.workcenter.line'
957     _description = 'Work Order'
958     _order = 'sequence'
959
960     _columns = {
961         'name': fields.char('Work Order', size=64, required=True),
962         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
963         'cycle': fields.float('Nbr of cycles', digits=(16,2)),
964         'hour': fields.float('Nbr of hours', digits=(16,2)),
965         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
966         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
967     }
968     _defaults = {
969         'sequence': lambda *a: 1,
970         'hour': lambda *a: 0,
971         'cycle': lambda *a: 0,
972     }
973 mrp_production_workcenter_line()
974
975 class mrp_production_product_line(osv.osv):
976     _name = 'mrp.production.product.line'
977     _description = 'Production Scheduled Product'
978     _columns = {
979         'name': fields.char('Name', size=64, required=True),
980         'product_id': fields.many2one('product.product', 'Product', required=True),
981         'product_qty': fields.float('Product Qty', required=True),
982         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
983         'product_uos_qty': fields.float('Product UOS Qty'),
984         'product_uos': fields.many2one('product.uom', 'Product UOS'),
985         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
986     }
987 mrp_production_product_line()
988
989 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: