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