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