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