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