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