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