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