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