[MERGE]: Merged with trunk-addons.
[odoo/odoo.git] / addons / mrp / mrp.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime
23 from osv import osv, fields
24 from tools.translate import _
25 import netsvc
26 import time
27 import tools
28
29
30 #----------------------------------------------------------
31 # Work Centers
32 #----------------------------------------------------------
33 # capacity_hour : capacity per hour. default: 1.0.
34 #          Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
35 # unit_per_cycle : how many units are produced for one cycle
36
37 class mrp_workcenter(osv.osv):
38     _name = 'mrp.workcenter'
39     _description = 'Work Center'
40     _inherits = {'resource.resource':"resource_id"}
41     _columns = {
42         'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
43         'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this workcenter can do in parallel. If this workcenter represents a team of 5 workers, the capacity per cycle is 5."),
44         'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
45         'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
46         'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
47         'costs_hour': fields.float('Cost per hour', help="Specify Cost of Workcenter per hour."),
48         'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
49             help="Complete this only if you want automatic analytic accounting entries on production orders."),
50         'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Workcenter per cycle."),
51         'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
52             help="Complete this only if you want automatic analytic accounting entries on production orders."),
53         'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
54         'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
55         'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
56     }
57     _defaults = {
58         'capacity_per_cycle': 1.0,
59         'resource_type': 'material',
60      }
61 mrp_workcenter()
62
63
64 class mrp_routing(osv.osv):
65     """
66     For specifying the routings of workcenters.
67     """
68     _name = 'mrp.routing'
69     _description = 'Routing'
70     _columns = {
71         'name': fields.char('Name', size=64, required=True),
72         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
73         'code': fields.char('Code', size=8),
74
75         'note': fields.text('Description'),
76         'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
77
78         'location_id': fields.many2one('stock.location', 'Production Location',
79             help="Keep empty if you produce at the location where the finished products are needed." \
80                 "Set a location if you produce at a fixed location. This can be a partner location " \
81                 "if you subcontract the manufacturing operations."
82         ),
83         'company_id': fields.many2one('res.company', 'Company'),
84     }
85     _defaults = {
86         'active': lambda *a: 1,
87         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
88     }
89 mrp_routing()
90
91 class mrp_routing_workcenter(osv.osv):
92     """
93     Defines working cycles and hours of a workcenter using routings.
94     """
95     _name = 'mrp.routing.workcenter'
96     _description = 'Workcenter Usage'
97     _columns = {
98         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
99         'name': fields.char('Name', size=64, required=True),
100         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing workcenters."),
101         'cycle_nbr': fields.float('Number of Cycles', required=True,
102             help="Number of operations this workcenter can do."),
103         'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for doing one cycle."),
104         'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
105              help="Routing indicates all the workcenters used, for how long and/or cycles." \
106                 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
107         'note': fields.text('Description'),
108         'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company'),
109     }
110     _defaults = {
111         'cycle_nbr': lambda *a: 1.0,
112         'hour_nbr': lambda *a: 0.0,
113     }
114 mrp_routing_workcenter()
115
116 class mrp_bom(osv.osv):
117     """
118     Defines bills of material for a product.
119     """
120     _name = 'mrp.bom'
121     _description = 'Bill of Material'
122
123     def _child_compute(self, cr, uid, ids, name, arg, context=None):
124         """ Gets child bom.
125         @param self: The object pointer
126         @param cr: The current row, from the database cursor,
127         @param uid: The current user ID for security checks
128         @param ids: List of selected IDs
129         @param name: Name of the field
130         @param arg: User defined argument
131         @param context: A standard dictionary for contextual values
132         @return:  Dictionary of values
133         """
134         result = {}
135         if context is None:
136             context = {}
137         bom_obj = self.pool.get('mrp.bom')
138         bom_id = context and context.get('active_id', False) or False
139         cr.execute('select id from mrp_bom')
140         if all(bom_id != r[0] for r in cr.fetchall()):
141             ids.sort()
142             bom_id = ids[0]
143         bom_parent = bom_obj.browse(cr, uid, bom_id, context=context)
144         for bom in self.browse(cr, uid, ids, context=context):
145             if (bom_parent) or (bom.id == bom_id):
146                 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
147             else:
148                 result[bom.id] = []
149             if bom.bom_lines:
150                 continue
151             ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
152             if (bom.type=='phantom' or ok):
153                 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
154                 if sids:
155                     bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
156                     result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
157
158         return result
159
160     def _compute_type(self, cr, uid, ids, field_name, arg, context=None):
161         """ Sets particular method for the selected bom type.
162         @param field_name: Name of the field
163         @param arg: User defined argument
164         @return:  Dictionary of values
165         """
166         res = dict(map(lambda x: (x,''), ids))
167         for line in self.browse(cr, uid, ids, context=context):
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 False, 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, context=None):
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=None):
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, context=context)
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=None):
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=None):
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=None):
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, context=None):
453         orders = self.browse(cr, uid, ids, context=context)
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=None):
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         if context is None:
645             context = {}
646         stock_mov_obj = self.pool.get('stock.move')
647         production = self.browse(cr, uid, production_id, context=context)
648         
649         final_product_todo = []
650
651         produced_qty = 0
652         if production_mode == 'consume_produce':
653             produced_qty = production_qty
654
655         for produced_product in production.move_created_ids2:
656             if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
657                 continue
658             produced_qty += produced_product.product_qty
659
660         if production_mode in ['consume','consume_produce']:
661             consumed_products = {}
662             check = {}
663             scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
664
665             for consumed_product in production.move_lines2:
666                 consumed = consumed_product.product_qty
667                 if consumed_product.scrapped:
668                     continue
669                 if not consumed_products.get(consumed_product.product_id.id, False):
670                     consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
671                     check[consumed_product.product_id.id] = 0
672                 for f in production.product_lines:
673                     if f.product_id.id == consumed_product.product_id.id:
674                         if (len(production.move_lines2) - scrapped) > len(production.product_lines):
675                             check[consumed_product.product_id.id] += consumed_product.product_qty
676                             consumed = check[consumed_product.product_id.id]
677                         rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
678                         consumed_products[consumed_product.product_id.id] = rest_consumed
679
680             for raw_product in production.move_lines:
681                 for f in production.product_lines:
682                     if f.product_id.id == raw_product.product_id.id:
683                         consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
684                         if consumed_qty == 0:
685                             consumed_qty = production_qty * f.product_qty / production.product_qty
686                         if consumed_qty > 0:
687                             stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
688
689         if production_mode == 'consume_produce':
690             # To produce remaining qty of final product
691             vals = {'state':'confirmed'}
692             final_product_todo = [x.id for x in production.move_created_ids]
693             stock_mov_obj.write(cr, uid, final_product_todo, vals)
694             produced_products = {}
695             for produced_product in production.move_created_ids2:
696                 if produced_product.scrapped:
697                     continue
698                 if not produced_products.get(produced_product.product_id.id, False):
699                     produced_products[produced_product.product_id.id] = 0
700                 produced_products[produced_product.product_id.id] += produced_product.product_qty
701
702             for produce_product in production.move_created_ids:
703                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
704                 rest_qty = production.product_qty - produced_qty
705                 if rest_qty <= production_qty:
706                    production_qty = rest_qty
707                 if rest_qty > 0 :
708                     stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
709
710         for raw_product in production.move_lines2:
711             new_parent_ids = []
712             parent_move_ids = [x.id for x in raw_product.move_history_ids]
713             for final_product in production.move_created_ids2:
714                 if final_product.id not in parent_move_ids:
715                     new_parent_ids.append(final_product.id)
716             for new_parent_id in new_parent_ids:
717                 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
718
719         wf_service = netsvc.LocalService("workflow")
720         wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
721         return True
722
723     def _costs_generate(self, cr, uid, production):
724         """ Calculates total costs at the end of the production.
725         @param production: Id of production order.
726         @return: Calculated amount.
727         """
728         amount = 0.0
729         analytic_line_obj = self.pool.get('account.analytic.line')
730         for wc_line in production.workcenter_lines:
731             wc = wc_line.workcenter_id
732             if wc.costs_journal_id and wc.costs_general_account_id:
733                 value = wc_line.hour * wc.costs_hour
734                 account = wc.costs_hour_account_id.id
735                 if value and account:
736                     amount += value
737                     analytic_line_obj.create(cr, uid, {
738                         'name': wc_line.name + ' (H)',
739                         'amount': value,
740                         'account_id': account,
741                         'general_account_id': wc.costs_general_account_id.id,
742                         'journal_id': wc.costs_journal_id.id,
743                         'code': wc.code
744                     } )
745             if wc.costs_journal_id and wc.costs_general_account_id:
746                 value = wc_line.cycle * wc.costs_cycle
747                 account = wc.costs_cycle_account_id.id
748                 if value and account:
749                     amount += value
750                     analytic_line_obj.create(cr, uid, {
751                         'name': wc_line.name+' (C)',
752                         'amount': value,
753                         'account_id': account,
754                         'general_account_id': wc.costs_general_account_id.id,
755                         'journal_id': wc.costs_journal_id.id,
756                         'code': wc.code,
757                     } )
758         return amount
759
760     def action_in_production(self, cr, uid, ids):
761         """ Changes state to In Production and writes starting date.
762         @return: True
763         """
764         self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
765         return True
766
767     def test_if_product(self, cr, uid, ids):
768         """
769         @return: True or False
770         """
771         res = True
772         for production in self.browse(cr, uid, ids):
773             if not production.product_lines:
774                 if not self.action_compute(cr, uid, [production.id]):
775                     res = False
776         return res
777
778     def _get_auto_picking(self, cr, uid, production):
779         return True
780
781     def action_confirm(self, cr, uid, ids):
782         """ Confirms production order.
783         @return: Newly generated picking Id.
784         """
785         picking_id = False
786         proc_ids = []
787         seq_obj = self.pool.get('ir.sequence')
788         pick_obj = self.pool.get('stock.picking')
789         move_obj = self.pool.get('stock.move')
790         proc_obj = self.pool.get('procurement.order')
791         wf_service = netsvc.LocalService("workflow")
792         for production in self.browse(cr, uid, ids):
793             if not production.product_lines:
794                 self.action_compute(cr, uid, [production.id])
795                 production = self.browse(cr, uid, [production.id])[0]
796             routing_loc = None
797             pick_type = 'internal'
798             address_id = False
799             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
800                 routing_loc = production.bom_id.routing_id.location_id
801                 if routing_loc.usage <> 'internal':
802                     pick_type = 'out'
803                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
804                 routing_loc = routing_loc.id
805             pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
806             picking_id = pick_obj.create(cr, uid, {
807                 'name': pick_name,
808                 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
809                 'type': pick_type,
810                 'move_type': 'one',
811                 'state': 'auto',
812                 'address_id': address_id,
813                 'auto_picking': self._get_auto_picking(cr, uid, production),
814                 'company_id': production.company_id.id,
815             })
816
817             source = production.product_id.product_tmpl_id.property_stock_production.id
818             data = {
819                 'name':'PROD:' + production.name,
820                 'date': production.date_planned,
821                 'product_id': production.product_id.id,
822                 'product_qty': production.product_qty,
823                 'product_uom': production.product_uom.id,
824                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
825                 'product_uos': production.product_uos and production.product_uos.id or False,
826                 'location_id': source,
827                 'location_dest_id': production.location_dest_id.id,
828                 'move_dest_id': production.move_prod_id.id,
829                 'state': 'waiting',
830                 'company_id': production.company_id.id,
831             }
832             res_final_id = move_obj.create(cr, uid, data)
833
834             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
835             moves = []
836             for line in production.product_lines:
837                 move_id = False
838                 newdate = production.date_planned
839                 if line.product_id.type in ('product', 'consu'):
840                     res_dest_id = move_obj.create(cr, uid, {
841                         'name':'PROD:' + production.name,
842                         'date': production.date_planned,
843                         'product_id': line.product_id.id,
844                         'product_qty': line.product_qty,
845                         'product_uom': line.product_uom.id,
846                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
847                         'product_uos': line.product_uos and line.product_uos.id or False,
848                         'location_id': routing_loc or production.location_src_id.id,
849                         'location_dest_id': source,
850                         'move_dest_id': res_final_id,
851                         'state': 'waiting',
852                         'company_id': production.company_id.id,
853                     })
854                     moves.append(res_dest_id)
855                     move_id = move_obj.create(cr, uid, {
856                         'name':'PROD:' + production.name,
857                         'picking_id':picking_id,
858                         'product_id': line.product_id.id,
859                         'product_qty': line.product_qty,
860                         'product_uom': line.product_uom.id,
861                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
862                         'product_uos': line.product_uos and line.product_uos.id or False,
863                         'date': newdate,
864                         'move_dest_id': res_dest_id,
865                         'location_id': production.location_src_id.id,
866                         'location_dest_id': routing_loc or production.location_src_id.id,
867                         'state': 'waiting',
868                         'company_id': production.company_id.id,
869                     })
870                 proc_id = proc_obj.create(cr, uid, {
871                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
872                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
873                     'date_planned': newdate,
874                     'product_id': line.product_id.id,
875                     'product_qty': line.product_qty,
876                     'product_uom': line.product_uom.id,
877                     'product_uos_qty': line.product_uos and line.product_qty or False,
878                     'product_uos': line.product_uos and line.product_uos.id or False,
879                     'location_id': production.location_src_id.id,
880                     'procure_method': line.product_id.procure_method,
881                     'move_id': move_id,
882                     'company_id': production.company_id.id,
883                 })
884                 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
885                 proc_ids.append(proc_id)
886             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
887             self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
888             message = _("Manufacturing order '%s' is scheduled for the %s.") % (
889                 production.name,
890                 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
891             )
892             self.log(cr, uid, production.id, message)
893         return picking_id
894
895     def force_production(self, cr, uid, ids, *args):
896         """ Assigns products.
897         @param *args: Arguments
898         @return: True
899         """
900         pick_obj = self.pool.get('stock.picking')
901         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
902         return True
903
904 mrp_production()
905
906 class mrp_production_workcenter_line(osv.osv):
907     _name = 'mrp.production.workcenter.line'
908     _description = 'Work Order'
909     _order = 'sequence'
910
911     _columns = {
912         'name': fields.char('Work Order', size=64, required=True),
913         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
914         'cycle': fields.float('Nbr of cycles', digits=(16,2)),
915         'hour': fields.float('Nbr of hours', digits=(16,2)),
916         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
917         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
918     }
919     _defaults = {
920         'sequence': lambda *a: 1,
921         'hour': lambda *a: 0,
922         'cycle': lambda *a: 0,
923     }
924 mrp_production_workcenter_line()
925
926 class mrp_production_product_line(osv.osv):
927     _name = 'mrp.production.product.line'
928     _description = 'Production Scheduled Product'
929     _columns = {
930         'name': fields.char('Name', size=64, required=True),
931         'product_id': fields.many2one('product.product', 'Product', required=True),
932         'product_qty': fields.float('Product Qty', required=True),
933         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
934         'product_uos_qty': fields.float('Product UOS Qty'),
935         'product_uos': fields.many2one('product.uom', 'Product UOS'),
936         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
937     }
938 mrp_production_product_line()
939
940 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: