[MERGE] branch merged with lp:~openerp-dev/openobject-addons/trunk-payroll
[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 workcenters.
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 workcenter using routings.
106     """
107     _name = 'mrp.routing.workcenter'
108     _description = 'Workcenter 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 workcenters used, for how long and/or cycles." \
118                 "If Routing is indicated then,the third tab of a production order (workcenters) 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 product_id:
273             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
274             v = {'product_uom': prod.uom_id.id}
275             if not name:
276                 v['name'] = prod.name
277             return {'value': v}
278         return {}
279
280     def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
281         """ Finds BoM for particular product and product uom.
282         @param product_id: Selected product.
283         @param product_uom: Unit of measure of a product.
284         @param properties: List of related properties.
285         @return: False or BoM id.
286         """
287         cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
288         ids = map(lambda x: x[0], cr.fetchall())
289         max_prop = 0
290         result = False
291         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
292             prop = 0
293             for prop_id in bom.property_ids:
294                 if prop_id.id in properties:
295                     prop += 1
296             if (prop > max_prop) or ((max_prop == 0) and not result):
297                 result = bom.id
298                 max_prop = prop
299         return result
300
301     def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
302         """ Finds Products and Workcenters for related BoM for manufacturing order.
303         @param bom: BoM of particular product.
304         @param factor: Factor of product UoM.
305         @param properties: A List of properties Ids.
306         @param addthis: If BoM found then True else False.
307         @param level: Depth level to find BoM lines starts from 10.
308         @return: result: List of dictionaries containing product details.
309                  result2: List of dictionaries containing workcenter details.
310         """
311         factor = factor / (bom.product_efficiency or 1.0)
312         factor = rounding(factor, bom.product_rounding)
313         if factor < bom.product_rounding:
314             factor = bom.product_rounding
315         result = []
316         result2 = []
317         phantom = False
318         if bom.type == 'phantom' and not bom.bom_lines:
319             newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
320             if newbom:
321                 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
322                 result = result + res[0]
323                 result2 = result2 + res[1]
324                 phantom = True
325             else:
326                 phantom = False
327         if not phantom:
328             if addthis and not bom.bom_lines:
329                 result.append(
330                 {
331                     'name': bom.product_id.name,
332                     'product_id': bom.product_id.id,
333                     'product_qty': bom.product_qty * factor,
334                     'product_uom': bom.product_uom.id,
335                     'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
336                     'product_uos': bom.product_uos and bom.product_uos.id or False,
337                 })
338             if bom.routing_id:
339                 for wc_use in bom.routing_id.workcenter_lines:
340                     wc = wc_use.workcenter_id
341                     d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
342                     mult = (d + (m and 1.0 or 0.0))
343                     cycle = mult * wc_use.cycle_nbr
344                     result2.append({
345                         'name': tools.ustr(wc_use.name) + ' - '  + tools.ustr(bom.product_id.name),
346                         'workcenter_id': wc.id,
347                         'sequence': level+(wc_use.sequence or 0),
348                         'cycle': cycle,
349                         '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)),
350                     })
351             for bom2 in bom.bom_lines:
352                 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
353                 result = result + res[0]
354                 result2 = result2 + res[1]
355         return result, result2
356
357     def copy_data(self, cr, uid, id, default=None, context=None):
358         if default is None:
359             default = {}
360         if context is None:
361             context = {}
362         bom_data = self.read(cr, uid, id, [], context=context)
363         default.update({'name': bom_data['name'] + ' ' + _('Copy'), 'bom_id':False})
364         return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
365
366 mrp_bom()
367
368 class mrp_bom_revision(osv.osv):
369     _name = 'mrp.bom.revision'
370     _description = 'Bill of Material Revision'
371     _columns = {
372         'name': fields.char('Modification name', size=64, required=True),
373         'description': fields.text('Description'),
374         'date': fields.date('Modification Date'),
375         'indice': fields.char('Revision', size=16),
376         'last_indice': fields.char('last indice', size=64),
377         'author_id': fields.many2one('res.users', 'Author'),
378         'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
379     }
380
381     _defaults = {
382         'author_id': lambda x, y, z, c: z,
383         'date': lambda *a: time.strftime('%Y-%m-%d'),
384     }
385
386 mrp_bom_revision()
387
388 def rounding(f, r):
389     if not r:
390         return f
391     return round(f / r) * r
392
393 class mrp_production(osv.osv):
394     """
395     Production Orders / Manufacturing Orders
396     """
397     _name = 'mrp.production'
398     _description = 'Manufacturing Order'
399     _date_name  = 'date_planned'
400
401     def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
402         """ Calculates total hours and total no. of cycles for a production order.
403         @param prop: Name of field.
404         @param unknow_none:
405         @return: Dictionary of values.
406         """
407         result = {}
408         for prod in self.browse(cr, uid, ids, context=context):
409             result[prod.id] = {
410                 'hour_total': 0.0,
411                 'cycle_total': 0.0,
412             }
413             for wc in prod.workcenter_lines:
414                 result[prod.id]['hour_total'] += wc.hour
415                 result[prod.id]['cycle_total'] += wc.cycle
416         return result
417
418     def _production_date_end(self, cr, uid, ids, prop, unknow_none, context=None):
419         """ Finds production end date.
420         @param prop: Name of field.
421         @param unknow_none:
422         @return: Dictionary of values.
423         """
424         result = {}
425         for prod in self.browse(cr, uid, ids, context=context):
426             result[prod.id] = prod.date_planned
427         return result
428
429     def _production_date(self, cr, uid, ids, prop, unknow_none, context=None):
430         """ Finds production planned date.
431         @param prop: Name of field.
432         @param unknow_none:
433         @return: Dictionary of values.
434         """
435         result = {}
436         for prod in self.browse(cr, uid, ids, context=context):
437             result[prod.id] = prod.date_planned[:10]
438         return result
439
440     _columns = {
441         'name': fields.char('Reference', size=64, required=True),
442         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
443         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
444
445         'product_id': fields.many2one('product.product', 'Product', required=True, ),
446         'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
447         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
448         'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
449         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
450
451         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
452             help="Location where the system will look for components."),
453         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
454             help="Location where the system will stock the finished products."),
455
456         'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
457         'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
458         'date_planned': fields.datetime('Scheduled date', required=True, select=1),
459         'date_start': fields.datetime('Start Date', select=True),
460         'date_finished': fields.datetime('End Date', select=True),
461
462         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
463         '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."),
464
465         'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True, ondelete="restrict",
466             help="This is the internal picking list that brings the finished product to the production plan"),
467         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
468         '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)]}),
469         'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
470         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
471         'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
472         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
473         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
474         '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,
475                                     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\'.\
476                                     \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\'.'),
477         'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
478         'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
479         'company_id': fields.many2one('res.company','Company',required=True),
480     }
481     _defaults = {
482         'priority': lambda *a: '1',
483         'state': lambda *a: 'draft',
484         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
485         'product_qty':  lambda *a: 1.0,
486         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
487         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
488     }
489     _order = 'priority desc, date_planned asc';
490
491     def _check_qty(self, cr, uid, ids, context=None):
492         orders = self.browse(cr, uid, ids, context=context)
493         for order in orders:
494             if order.product_qty <= 0:
495                 return False
496         return True
497
498     _constraints = [
499         (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
500     ]
501
502     def unlink(self, cr, uid, ids, context=None):
503         productions = self.read(cr, uid, ids, ['state'])
504         unlink_ids = []
505         for s in productions:
506             if s['state'] in ['draft','cancel']:
507                 unlink_ids.append(s['id'])
508             else:
509                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!') % s['state'])
510         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
511
512     def copy(self, cr, uid, id, default=None, context=None):
513         if default is None:
514             default = {}
515         default.update({
516             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
517             'move_lines' : [],
518             'move_lines2' : [],
519             'move_created_ids' : [],
520             'move_created_ids2' : [],
521             'product_lines' : [],
522             'picking_id': False
523         })
524         return super(mrp_production, self).copy(cr, uid, id, default, context)
525
526     def location_id_change(self, cr, uid, ids, src, dest, context=None):
527         """ Changes destination location if source location is changed.
528         @param src: Source location id.
529         @param dest: Destination location id.
530         @return: Dictionary of values.
531         """
532         if dest:
533             return {}
534         if src:
535             return {'value': {'location_dest_id': src}}
536         return {}
537
538     def product_id_change(self, cr, uid, ids, product_id, context=None):
539         """ Finds UoM of changed product.
540         @param product_id: Id of changed product.
541         @return: Dictionary of values.
542         """
543         if not product_id:
544             return {'value': {
545                 'product_uom': False,
546                 'bom_id': False,
547                 'routing_id': False
548             }}
549         bom_obj = self.pool.get('mrp.bom')
550         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
551         bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
552         routing_id = False
553         if bom_id:
554             bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
555             routing_id = bom_point.routing_id.id or False
556         result = {
557             'product_uom': product.uom_id and product.uom_id.id or False,
558             'bom_id': bom_id,
559             'routing_id': routing_id
560         }
561         return {'value': result}
562
563     def bom_id_change(self, cr, uid, ids, bom_id, context=None):
564         """ Finds routing for changed BoM.
565         @param product: Id of product.
566         @return: Dictionary of values.
567         """
568         if not bom_id:
569             return {'value': {
570                 'routing_id': False
571             }}
572         bom_pool = self.pool.get('mrp.bom')
573         bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
574         routing_id = bom_point.routing_id.id or False
575         result = {
576             'routing_id': routing_id
577         }
578         return {'value': result}
579
580     def action_picking_except(self, cr, uid, ids):
581         """ Changes the state to Exception.
582         @return: True
583         """
584         self.write(cr, uid, ids, {'state': 'picking_except'})
585         return True
586
587     def action_compute(self, cr, uid, ids, properties=[]):
588         """ Computes bills of material of a product.
589         @param properties: List containing dictionaries of properties.
590         @return: No. of products.
591         """
592         results = []
593         bom_obj = self.pool.get('mrp.bom')
594         uom_obj = self.pool.get('product.uom')
595         prod_line_obj = self.pool.get('mrp.production.product.line')
596         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
597         for production in self.browse(cr, uid, ids):
598             cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
599             cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
600             bom_point = production.bom_id
601             bom_id = production.bom_id.id
602             if not bom_point:
603                 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
604                 if bom_id:
605                     bom_point = bom_obj.browse(cr, uid, bom_id)
606                     routing_id = bom_point.routing_id.id or False
607                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
608
609             if not bom_id:
610                 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
611
612             factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id)
613             res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
614             results = res[0]
615             results2 = res[1]
616             for line in results:
617                 line['production_id'] = production.id
618                 prod_line_obj.create(cr, uid, line)
619             for line in results2:
620                 line['production_id'] = production.id
621                 workcenter_line_obj.create(cr, uid, line)
622         return len(results)
623
624     def action_cancel(self, cr, uid, ids, context=None):
625         """ Cancels the production order and related stock moves.
626         @return: True
627         """
628         if context is None:
629             context = {}
630         move_obj = self.pool.get('stock.move')
631         for production in self.browse(cr, uid, ids, context=context):
632             if production.state == 'confirmed' and production.picking_id.state not in ('draft', 'cancel'):
633                 raise osv.except_osv(
634                     _('Could not cancel manufacturing order !'),
635                     _('You must first cancel related internal picking attached to this manufacturing order.'))
636             if production.move_created_ids:
637                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
638             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
639         self.write(cr, uid, ids, {'state': 'cancel'})
640         return True
641
642     def action_ready(self, cr, uid, ids):
643         """ Changes the production state to Ready and location id of stock move.
644         @return: True
645         """
646         move_obj = self.pool.get('stock.move')
647         self.write(cr, uid, ids, {'state': 'ready'})
648
649         for (production_id,name) in self.name_get(cr, uid, ids):
650             production = self.browse(cr, uid, production_id)
651             if production.move_prod_id:
652                 move_obj.write(cr, uid, [production.move_prod_id.id],
653                         {'location_id': production.location_dest_id.id})
654
655             message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
656             self.log(cr, uid, production_id, message)
657         return True
658
659     def action_production_end(self, cr, uid, ids):
660         """ Changes production state to Finish and writes finished date.
661         @return: True
662         """
663         for production in self.browse(cr, uid, ids):
664             self._costs_generate(cr, uid, production)
665         return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
666
667     def test_production_done(self, cr, uid, ids):
668         """ Tests whether production is done or not.
669         @return: True or False
670         """
671         res = True
672         for production in self.browse(cr, uid, ids):
673             if production.move_lines:
674                res = False
675
676             if production.move_created_ids:
677                res = False
678         return res
679
680     def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
681         """ To produce final product based on production mode (consume/consume&produce).
682         If Production mode is consume, all stock move lines of raw materials will be done/consumed.
683         If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
684         and stock move lines of final product will be also done/produced.
685         @param production_id: the ID of mrp.production object
686         @param production_qty: specify qty to produce
687         @param production_mode: specify production mode (consume/consume&produce).
688         @return: True
689         """
690         stock_mov_obj = self.pool.get('stock.move')
691         production = self.browse(cr, uid, production_id, context=context)
692
693
694         produced_qty = 0
695         if production_mode == 'consume_produce':
696             produced_qty = production_qty
697
698         for produced_product in production.move_created_ids2:
699             if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
700                 continue
701             produced_qty += produced_product.product_qty
702
703         if production_mode in ['consume','consume_produce']:
704             consumed_products = {}
705             check = {}
706             scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
707
708             for consumed_product in production.move_lines2:
709                 consumed = consumed_product.product_qty
710                 if consumed_product.scrapped:
711                     continue
712                 if not consumed_products.get(consumed_product.product_id.id, False):
713                     consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
714                     check[consumed_product.product_id.id] = 0
715                 for f in production.product_lines:
716                     if f.product_id.id == consumed_product.product_id.id:
717                         if (len(production.move_lines2) - scrapped) > len(production.product_lines):
718                             check[consumed_product.product_id.id] += consumed_product.product_qty
719                             consumed = check[consumed_product.product_id.id]
720                         rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
721                         consumed_products[consumed_product.product_id.id] = rest_consumed
722
723             for raw_product in production.move_lines:
724                 for f in production.product_lines:
725                     if f.product_id.id == raw_product.product_id.id:
726                         consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
727                         if consumed_qty == 0:
728                             consumed_qty = production_qty * f.product_qty / production.product_qty
729                         if consumed_qty > 0:
730                             stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
731
732         if production_mode == 'consume_produce':
733             # To produce remaining qty of final product
734             #vals = {'state':'confirmed'}
735             #final_product_todo = [x.id for x in production.move_created_ids]
736             #stock_mov_obj.write(cr, uid, final_product_todo, vals)
737             #stock_mov_obj.action_confirm(cr, uid, final_product_todo, context)
738             produced_products = {}
739             for produced_product in production.move_created_ids2:
740                 if produced_product.scrapped:
741                     continue
742                 if not produced_products.get(produced_product.product_id.id, False):
743                     produced_products[produced_product.product_id.id] = 0
744                 produced_products[produced_product.product_id.id] += produced_product.product_qty
745
746             for produce_product in production.move_created_ids:
747                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
748                 rest_qty = production.product_qty - produced_qty
749                 if rest_qty <= production_qty:
750                    production_qty = rest_qty
751                 if rest_qty > 0 :
752                     stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
753
754         for raw_product in production.move_lines2:
755             new_parent_ids = []
756             parent_move_ids = [x.id for x in raw_product.move_history_ids]
757             for final_product in production.move_created_ids2:
758                 if final_product.id not in parent_move_ids:
759                     new_parent_ids.append(final_product.id)
760             for new_parent_id in new_parent_ids:
761                 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
762
763         wf_service = netsvc.LocalService("workflow")
764         wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
765         return True
766
767     def _costs_generate(self, cr, uid, production):
768         """ Calculates total costs at the end of the production.
769         @param production: Id of production order.
770         @return: Calculated amount.
771         """
772         amount = 0.0
773         analytic_line_obj = self.pool.get('account.analytic.line')
774         for wc_line in production.workcenter_lines:
775             wc = wc_line.workcenter_id
776             if wc.costs_journal_id and wc.costs_general_account_id:
777                 value = wc_line.hour * wc.costs_hour
778                 account = wc.costs_hour_account_id.id
779                 if value and account:
780                     amount += value
781                     analytic_line_obj.create(cr, uid, {
782                         'name': wc_line.name + ' (H)',
783                         'amount': value,
784                         'account_id': account,
785                         'general_account_id': wc.costs_general_account_id.id,
786                         'journal_id': wc.costs_journal_id.id,
787                         'ref': wc.code,
788                         'product_id': wc.product_id.id,
789                         'unit_amount': wc_line.hour,
790                         'product_uom_id': wc.product_id.uom_id.id
791                     } )
792             if wc.costs_journal_id and wc.costs_general_account_id:
793                 value = wc_line.cycle * wc.costs_cycle
794                 account = wc.costs_cycle_account_id.id
795                 if value and account:
796                     amount += value
797                     analytic_line_obj.create(cr, uid, {
798                         'name': wc_line.name+' (C)',
799                         'amount': value,
800                         'account_id': account,
801                         'general_account_id': wc.costs_general_account_id.id,
802                         'journal_id': wc.costs_journal_id.id,
803                         'ref': wc.code,
804                         'product_id': wc.product_id.id,
805                         'unit_amount': wc_line.cycle,
806                         'product_uom_id': wc.product_id.uom_id.id
807                     } )
808         return amount
809
810     def action_in_production(self, cr, uid, ids):
811         """ Changes state to In Production and writes starting date.
812         @return: True
813         """
814         self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
815         return True
816
817     def test_if_product(self, cr, uid, ids):
818         """
819         @return: True or False
820         """
821         res = True
822         for production in self.browse(cr, uid, ids):
823             if not production.product_lines:
824                 if not self.action_compute(cr, uid, [production.id]):
825                     res = False
826         return res
827
828     def _get_auto_picking(self, cr, uid, production):
829         return True
830
831     def action_confirm(self, cr, uid, ids):
832         """ Confirms production order.
833         @return: Newly generated picking Id.
834         """
835         picking_id = False
836         proc_ids = []
837         seq_obj = self.pool.get('ir.sequence')
838         pick_obj = self.pool.get('stock.picking')
839         move_obj = self.pool.get('stock.move')
840         proc_obj = self.pool.get('procurement.order')
841         wf_service = netsvc.LocalService("workflow")
842         for production in self.browse(cr, uid, ids):
843             if not production.product_lines:
844                 self.action_compute(cr, uid, [production.id])
845                 production = self.browse(cr, uid, [production.id])[0]
846             routing_loc = None
847             pick_type = 'internal'
848             address_id = False
849             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
850                 routing_loc = production.bom_id.routing_id.location_id
851                 if routing_loc.usage <> 'internal':
852                     pick_type = 'out'
853                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
854                 routing_loc = routing_loc.id
855             pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
856             picking_id = pick_obj.create(cr, uid, {
857                 'name': pick_name,
858                 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
859                 'type': pick_type,
860                 'move_type': 'one',
861                 'state': 'auto',
862                 'address_id': address_id,
863                 'auto_picking': self._get_auto_picking(cr, uid, production),
864                 'company_id': production.company_id.id,
865             })
866
867             source = production.product_id.product_tmpl_id.property_stock_production.id
868             data = {
869                 'name':'PROD:' + production.name,
870                 'date': production.date_planned,
871                 'product_id': production.product_id.id,
872                 'product_qty': production.product_qty,
873                 'product_uom': production.product_uom.id,
874                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
875                 'product_uos': production.product_uos and production.product_uos.id or False,
876                 'location_id': source,
877                 'location_dest_id': production.location_dest_id.id,
878                 'move_dest_id': production.move_prod_id.id,
879                 'state': 'waiting',
880                 'company_id': production.company_id.id,
881             }
882             res_final_id = move_obj.create(cr, uid, data)
883
884             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
885             moves = []
886             for line in production.product_lines:
887                 move_id = False
888                 newdate = production.date_planned
889                 if line.product_id.type in ('product', 'consu'):
890                     res_dest_id = move_obj.create(cr, uid, {
891                         'name':'PROD:' + production.name,
892                         'date': production.date_planned,
893                         'product_id': line.product_id.id,
894                         'product_qty': line.product_qty,
895                         'product_uom': line.product_uom.id,
896                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
897                         'product_uos': line.product_uos and line.product_uos.id or False,
898                         'location_id': routing_loc or production.location_src_id.id,
899                         'location_dest_id': source,
900                         'move_dest_id': res_final_id,
901                         'state': 'waiting',
902                         'company_id': production.company_id.id,
903                     })
904                     moves.append(res_dest_id)
905                     move_id = move_obj.create(cr, uid, {
906                         'name':'PROD:' + production.name,
907                         'picking_id':picking_id,
908                         'product_id': line.product_id.id,
909                         'product_qty': line.product_qty,
910                         'product_uom': line.product_uom.id,
911                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
912                         'product_uos': line.product_uos and line.product_uos.id or False,
913                         'date': newdate,
914                         'move_dest_id': res_dest_id,
915                         'location_id': production.location_src_id.id,
916                         'location_dest_id': routing_loc or production.location_src_id.id,
917                         'state': 'waiting',
918                         'company_id': production.company_id.id,
919                     })
920                 proc_id = proc_obj.create(cr, uid, {
921                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
922                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
923                     'date_planned': newdate,
924                     'product_id': line.product_id.id,
925                     'product_qty': line.product_qty,
926                     'product_uom': line.product_uom.id,
927                     'product_uos_qty': line.product_uos and line.product_qty or False,
928                     'product_uos': line.product_uos and line.product_uos.id or False,
929                     'location_id': production.location_src_id.id,
930                     'procure_method': line.product_id.procure_method,
931                     'move_id': move_id,
932                     'company_id': production.company_id.id,
933                 })
934                 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
935                 proc_ids.append(proc_id)
936             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
937             self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
938             message = _("Manufacturing order '%s' is scheduled for the %s.") % (
939                 production.name,
940                 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
941             )
942             self.log(cr, uid, production.id, message)
943         return picking_id
944
945     def force_production(self, cr, uid, ids, *args):
946         """ Assigns products.
947         @param *args: Arguments
948         @return: True
949         """
950         pick_obj = self.pool.get('stock.picking')
951         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
952         return True
953
954 mrp_production()
955
956 class mrp_production_workcenter_line(osv.osv):
957     _name = 'mrp.production.workcenter.line'
958     _description = 'Work Order'
959     _order = 'sequence'
960
961     _columns = {
962         'name': fields.char('Work Order', size=64, required=True),
963         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
964         'cycle': fields.float('Nbr of cycles', digits=(16,2)),
965         'hour': fields.float('Nbr of hours', digits=(16,2)),
966         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
967         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
968     }
969     _defaults = {
970         'sequence': lambda *a: 1,
971         'hour': lambda *a: 0,
972         'cycle': lambda *a: 0,
973     }
974 mrp_production_workcenter_line()
975
976 class mrp_production_product_line(osv.osv):
977     _name = 'mrp.production.product.line'
978     _description = 'Production Scheduled Product'
979     _columns = {
980         'name': fields.char('Name', size=64, required=True),
981         'product_id': fields.many2one('product.product', 'Product', required=True),
982         'product_qty': fields.float('Product Qty', required=True),
983         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
984         'product_uos_qty': fields.float('Product UOS Qty'),
985         'product_uos': fields.many2one('product.uom', 'Product UOS'),
986         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
987     }
988 mrp_production_product_line()
989
990 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: