[IMP] MRP: Improvement In PO file for Work Center
[odoo/odoo.git] / addons / mrp / mrp.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime
23 from osv import osv, fields
24 from tools.translate import _
25 import netsvc
26 import time
27 import tools
28
29
30 #----------------------------------------------------------
31 # Work Centers
32 #----------------------------------------------------------
33 # capacity_hour : capacity per hour. default: 1.0.
34 #          Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
35 # unit_per_cycle : how many units are produced for one cycle
36
37 class mrp_workcenter(osv.osv):
38     _name = 'mrp.workcenter'
39     _description = 'Work Center'
40     _inherits = {'resource.resource':"resource_id"}
41     _columns = {
42         'note': fields.text('Description', help="Description of the work center. Explain here what's a cycle according to this work center."),
43         'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operations this work center can do in parallel. If this work center represents a team of 5 workers, the capacity per cycle is 5."),
44         'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
45         'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
46         'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
47         'costs_hour': fields.float('Cost per hour', help="Specify Cost of Work center per hour."),
48         'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
49             help="Complete this only if you want automatic analytic accounting entries on production orders."),
50         'costs_cycle': fields.float('Cost per cycle', help="Specify Cost of Work center per cycle."),
51         'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
52             help="Complete this only if you want automatic analytic accounting entries on production orders."),
53         'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
54         'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
55         'resource_id': fields.many2one('resource.resource','Resource', ondelete='cascade', required=True),
56         'product_id': fields.many2one('product.product','Work Center Product', help="Fill this product to track easily your production costs in the analytic accounting."),
57     }
58     _defaults = {
59         'capacity_per_cycle': 1.0,
60         'resource_type': 'material',
61      }
62
63     def on_change_product_cost(self, cr, uid, ids, product_id, context=None):
64         if context is None:
65             context = {}
66         value = {}
67
68         if product_id:
69             cost = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
70             value = {'costs_hour' :cost.standard_price}
71         return {'value': value}
72
73 mrp_workcenter()
74
75
76 class mrp_routing(osv.osv):
77     """
78     For specifying the routings of workcenters.
79     """
80     _name = 'mrp.routing'
81     _description = 'Routing'
82     _columns = {
83         'name': fields.char('Name', size=64, required=True),
84         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the routing without removing it."),
85         'code': fields.char('Code', size=8),
86
87         'note': fields.text('Description'),
88         'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Work Centers'),
89
90         'location_id': fields.many2one('stock.location', 'Production Location',
91             help="Keep empty if you produce at the location where the finished products are needed." \
92                 "Set a location if you produce at a fixed location. This can be a partner location " \
93                 "if you subcontract the manufacturing operations."
94         ),
95         'company_id': fields.many2one('res.company', 'Company'),
96     }
97     _defaults = {
98         'active': lambda *a: 1,
99         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.routing', context=context)
100     }
101 mrp_routing()
102
103 class mrp_routing_workcenter(osv.osv):
104     """
105     Defines working cycles and hours of a workcenter using routings.
106     """
107     _name = 'mrp.routing.workcenter'
108     _description = 'Workcenter Usage'
109     _columns = {
110         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
111         'name': fields.char('Name', size=64, required=True),
112         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of routing work centers."),
113         'cycle_nbr': fields.float('Number of Cycles', required=True,
114             help="Number of iterations this work center has to do in the specified operation of the routing."),
115         'hour_nbr': fields.float('Number of Hours', required=True, help="Time in hours for this work center to achieve the operation of the specified routing."),
116         'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade',
117              help="Routing indicates all the workcenters used, for how long and/or cycles." \
118                 "If Routing is indicated then,the third tab of a production order (workcenters) will be automatically pre-completed."),
119         'note': fields.text('Description'),
120         'company_id': fields.related('routing_id', 'company_id', type='many2one', relation='res.company', string='Company'),
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 False, 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 work centers) to produce the finished product. The routing is mainly used to compute work center costs during operations and to plan future loads on work centers based on production planning."),
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     def copy_data(self, cr, uid, id, default=None, context=None):
338         if default is None:
339             default = {}
340         if context is None:
341             context = {}
342         bom_data = self.read(cr, uid, id, [], context=context)
343         default.update({'name': bom_data['name'] + _(' (copy)')})
344         return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
345
346 mrp_bom()
347
348 class mrp_bom_revision(osv.osv):
349     _name = 'mrp.bom.revision'
350     _description = 'Bill of Material Revision'
351     _columns = {
352         'name': fields.char('Modification name', size=64, required=True),
353         'description': fields.text('Description'),
354         'date': fields.date('Modification Date'),
355         'indice': fields.char('Revision', size=16),
356         'last_indice': fields.char('last indice', size=64),
357         'author_id': fields.many2one('res.users', 'Author'),
358         'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
359     }
360
361     _defaults = {
362         'author_id': lambda x, y, z, c: z,
363         'date': lambda *a: time.strftime('%Y-%m-%d'),
364     }
365
366 mrp_bom_revision()
367
368 def rounding(f, r):
369     if not r:
370         return f
371     return round(f / r) * r
372
373 class mrp_production(osv.osv):
374     """
375     Production Orders / Manufacturing Orders
376     """
377     _name = 'mrp.production'
378     _description = 'Manufacturing Order'
379     _date_name  = 'date_planned'
380
381     def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
382         """ Calculates total hours and total no. of cycles for a production order.
383         @param prop: Name of field.
384         @param unknow_none:
385         @return: Dictionary of values.
386         """
387         result = {}
388         for prod in self.browse(cr, uid, ids, context=context):
389             result[prod.id] = {
390                 'hour_total': 0.0,
391                 'cycle_total': 0.0,
392             }
393             for wc in prod.workcenter_lines:
394                 result[prod.id]['hour_total'] += wc.hour
395                 result[prod.id]['cycle_total'] += wc.cycle
396         return result
397
398     def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
399         """ Finds production end date.
400         @param prop: Name of field.
401         @param unknow_none:
402         @return: Dictionary of values.
403         """
404         result = {}
405         for prod in self.browse(cr, uid, ids, context=context):
406             result[prod.id] = prod.date_planned
407         return result
408
409     def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
410         """ Finds production planned date.
411         @param prop: Name of field.
412         @param unknow_none:
413         @return: Dictionary of values.
414         """
415         result = {}
416         for prod in self.browse(cr, uid, ids, context=context):
417             result[prod.id] = prod.date_planned[:10]
418         return result
419
420     _columns = {
421         'name': fields.char('Reference', size=64, required=True),
422         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
423         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
424
425         'product_id': fields.many2one('product.product', 'Product', required=True, ),
426         'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
427         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
428         'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
429         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
430
431         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
432             help="Location where the system will look for components."),
433         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
434             help="Location where the system will stock the finished products."),
435
436         'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
437         'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
438         'date_planned': fields.datetime('Scheduled date', required=True, select=1),
439         'date_start': fields.datetime('Start Date'),
440         'date_finished': fields.datetime('End Date'),
441
442         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
443         '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."),
444
445         'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
446             help="This is the internal picking list that brings the finished product to the production plan"),
447         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
448         '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)]}),
449         'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
450         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
451         'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
452         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
453         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
454         '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,
455                                     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\'.\
456                                     \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\'.'),
457         'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
458         'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
459         'company_id': fields.many2one('res.company','Company',required=True),
460     }
461     _defaults = {
462         'priority': lambda *a: '1',
463         'state': lambda *a: 'draft',
464         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
465         'product_qty':  lambda *a: 1.0,
466         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
467         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
468     }
469     _order = 'priority desc, date_planned asc';
470
471     def _check_qty(self, cr, uid, ids):
472         orders = self.browse(cr, uid, ids)
473         for order in orders:
474             if order.product_qty <= 0:
475                 return False
476         return True
477
478     _constraints = [
479         (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
480     ]
481
482     def unlink(self, cr, uid, ids, context=None):
483         productions = self.read(cr, uid, ids, ['state'])
484         unlink_ids = []
485         for s in productions:
486             if s['state'] in ['draft','cancel']:
487                 unlink_ids.append(s['id'])
488             else:
489                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!') % s['state'])
490         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
491
492     def copy(self, cr, uid, id, default=None, context=None):
493         if default is None:
494             default = {}
495         default.update({
496             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
497             'move_lines' : [],
498             'move_lines2' : [],
499             'move_created_ids' : [],
500             'move_created_ids2' : [],
501             'product_lines' : [],
502             'picking_id': False
503         })
504         return super(mrp_production, self).copy(cr, uid, id, default, context)
505
506     def location_id_change(self, cr, uid, ids, src, dest, context={}):
507         """ Changes destination location if source location is changed.
508         @param src: Source location id.
509         @param dest: Destination location id.
510         @return: Dictionary of values.
511         """
512         if dest:
513             return {}
514         if src:
515             return {'value': {'location_dest_id': src}}
516         return {}
517
518     def product_id_change(self, cr, uid, ids, product_id, context=None):
519         """ Finds UoM of changed product.
520         @param product_id: Id of changed product.
521         @return: Dictionary of values.
522         """
523         if not product_id:
524             return {'value': {
525                 'product_uom': False,
526                 'bom_id': False,
527                 'routing_id': False
528             }}
529         bom_obj = self.pool.get('mrp.bom')
530         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
531         bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
532         routing_id = False
533         if bom_id:
534             bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
535             routing_id = bom_point.routing_id.id or False
536         result = {
537             'product_uom': product.uom_id and product.uom_id.id or False,
538             'bom_id': bom_id,
539             'routing_id': routing_id
540         }
541         return {'value': result}
542
543     def bom_id_change(self, cr, uid, ids, bom_id, context=None):
544         """ Finds routing for changed BoM.
545         @param product: Id of product.
546         @return: Dictionary of values.
547         """
548         if not bom_id:
549             return {'value': {
550                 'routing_id': False
551             }}
552         bom_pool = self.pool.get('mrp.bom')
553         bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
554         routing_id = bom_point.routing_id.id or False
555         result = {
556             'routing_id': routing_id
557         }
558         return {'value': result}
559
560     def action_picking_except(self, cr, uid, ids):
561         """ Changes the state to Exception.
562         @return: True
563         """
564         self.write(cr, uid, ids, {'state': 'picking_except'})
565         return True
566
567     def action_compute(self, cr, uid, ids, properties=[]):
568         """ Computes bills of material of a product.
569         @param properties: List containing dictionaries of properties.
570         @return: No. of products.
571         """
572         results = []
573         bom_obj = self.pool.get('mrp.bom')
574         prod_line_obj = self.pool.get('mrp.production.product.line')
575         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
576         for production in self.browse(cr, uid, ids):
577             cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
578             cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
579             bom_point = production.bom_id
580             bom_id = production.bom_id.id
581             if not bom_point:
582                 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
583                 if bom_id:
584                     bom_point = bom_obj.browse(cr, uid, bom_id)
585                     routing_id = bom_point.routing_id.id or False
586                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
587
588             if not bom_id:
589                 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
590
591             factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
592             res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
593             results = res[0]
594             results2 = res[1]
595             for line in results:
596                 line['production_id'] = production.id
597                 prod_line_obj.create(cr, uid, line)
598             for line in results2:
599                 line['production_id'] = production.id
600                 workcenter_line_obj.create(cr, uid, line)
601         return len(results)
602
603     def action_cancel(self, cr, uid, ids):
604         """ Cancels the production order and related stock moves.
605         @return: True
606         """
607         move_obj = self.pool.get('stock.move')
608         for production in self.browse(cr, uid, ids):
609             if production.move_created_ids:
610                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
611             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
612         self.write(cr, uid, ids, {'state': 'cancel'})
613         return True
614
615     def action_ready(self, cr, uid, ids):
616         """ Changes the production state to Ready and location id of stock move.
617         @return: True
618         """
619         move_obj = self.pool.get('stock.move')
620         self.write(cr, uid, ids, {'state': 'ready'})
621
622         for (production_id,name) in self.name_get(cr, uid, ids):
623             production = self.browse(cr, uid, production_id)
624             if production.move_prod_id:
625                 move_obj.write(cr, uid, [production.move_prod_id.id],
626                         {'location_id': production.location_dest_id.id})
627
628             message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
629             self.log(cr, uid, production_id, message)
630         return True
631
632     def action_production_end(self, cr, uid, ids):
633         """ Changes production state to Finish and writes finished date.
634         @return: True
635         """
636         for production in self.browse(cr, uid, ids):
637             self._costs_generate(cr, uid, production)
638         return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
639
640     def test_production_done(self, cr, uid, ids):
641         """ Tests whether production is done or not.
642         @return: True or False
643         """
644         res = True
645         for production in self.browse(cr, uid, ids):
646             if production.move_lines:
647                res = False
648
649             if production.move_created_ids:
650                res = False
651         return res
652
653     def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
654         """ To produce final product based on production mode (consume/consume&produce).
655         If Production mode is consume, all stock move lines of raw materials will be done/consumed.
656         If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
657         and stock move lines of final product will be also done/produced.
658         @param production_id: the ID of mrp.production object
659         @param production_qty: specify qty to produce
660         @param production_mode: specify production mode (consume/consume&produce).
661         @return: True
662         """
663
664         stock_mov_obj = self.pool.get('stock.move')
665         production = self.browse(cr, uid, production_id)
666
667         final_product_todo = []
668
669         produced_qty = 0
670         if production_mode == 'consume_produce':
671             produced_qty = production_qty
672
673         for produced_product in production.move_created_ids2:
674             if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
675                 continue
676             produced_qty += produced_product.product_qty
677
678         if production_mode in ['consume','consume_produce']:
679             consumed_products = {}
680             check = {}
681             scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
682
683             for consumed_product in production.move_lines2:
684                 consumed = consumed_product.product_qty
685                 if consumed_product.scrapped:
686                     continue
687                 if not consumed_products.get(consumed_product.product_id.id, False):
688                     consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
689                     check[consumed_product.product_id.id] = 0
690                 for f in production.product_lines:
691                     if f.product_id.id == consumed_product.product_id.id:
692                         if (len(production.move_lines2) - scrapped) > len(production.product_lines):
693                             check[consumed_product.product_id.id] += consumed_product.product_qty
694                             consumed = check[consumed_product.product_id.id]
695                         rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
696                         consumed_products[consumed_product.product_id.id] = rest_consumed
697
698             for raw_product in production.move_lines:
699                 for f in production.product_lines:
700                     if f.product_id.id == raw_product.product_id.id:
701                         consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
702                         if consumed_qty == 0:
703                             consumed_qty = production_qty * f.product_qty / production.product_qty
704                         if consumed_qty > 0:
705                             stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
706
707         if production_mode == 'consume_produce':
708             # To produce remaining qty of final product
709             vals = {'state':'confirmed'}
710             final_product_todo = [x.id for x in production.move_created_ids]
711             stock_mov_obj.write(cr, uid, final_product_todo, vals)
712             produced_products = {}
713             for produced_product in production.move_created_ids2:
714                 if produced_product.scrapped:
715                     continue
716                 if not produced_products.get(produced_product.product_id.id, False):
717                     produced_products[produced_product.product_id.id] = 0
718                 produced_products[produced_product.product_id.id] += produced_product.product_qty
719
720             for produce_product in production.move_created_ids:
721                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
722                 rest_qty = production.product_qty - produced_qty
723                 if rest_qty <= production_qty:
724                    production_qty = rest_qty
725                 if rest_qty > 0 :
726                     stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
727
728         for raw_product in production.move_lines2:
729             new_parent_ids = []
730             parent_move_ids = [x.id for x in raw_product.move_history_ids]
731             for final_product in production.move_created_ids2:
732                 if final_product.id not in parent_move_ids:
733                     new_parent_ids.append(final_product.id)
734             for new_parent_id in new_parent_ids:
735                 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
736
737         wf_service = netsvc.LocalService("workflow")
738         wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
739         return True
740
741     def _costs_generate(self, cr, uid, production):
742         """ Calculates total costs at the end of the production.
743         @param production: Id of production order.
744         @return: Calculated amount.
745         """
746         amount = 0.0
747         analytic_line_obj = self.pool.get('account.analytic.line')
748         for wc_line in production.workcenter_lines:
749             wc = wc_line.workcenter_id
750             if wc.costs_journal_id and wc.costs_general_account_id:
751                 value = wc_line.hour * wc.costs_hour
752                 account = wc.costs_hour_account_id.id
753                 if value and account:
754                     amount += value
755                     analytic_line_obj.create(cr, uid, {
756                         'name': wc_line.name + ' (H)',
757                         'amount': value,
758                         'account_id': account,
759                         'general_account_id': wc.costs_general_account_id.id,
760                         'journal_id': wc.costs_journal_id.id,
761                         'ref': wc.code,
762                         'product_id': wc.product_id.id,
763                         'unit_amount': wc_line.hour,
764                         'product_uom_id': wc.product_id.uom_id.id
765                     } )
766             if wc.costs_journal_id and wc.costs_general_account_id:
767                 value = wc_line.cycle * wc.costs_cycle
768                 account = wc.costs_cycle_account_id.id
769                 if value and account:
770                     amount += value
771                     analytic_line_obj.create(cr, uid, {
772                         'name': wc_line.name+' (C)',
773                         'amount': value,
774                         'account_id': account,
775                         'general_account_id': wc.costs_general_account_id.id,
776                         'journal_id': wc.costs_journal_id.id,
777                         'ref': wc.code,
778                         'product_id': wc.product_id.id,
779                         'unit_amount': wc_line.cycle,
780                         'product_uom_id': wc.product_id.uom_id.id
781                     } )
782         return amount
783
784     def action_in_production(self, cr, uid, ids):
785         """ Changes state to In Production and writes starting date.
786         @return: True
787         """
788         self.write(cr, uid, ids, {'state': 'in_production', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
789         return True
790
791     def test_if_product(self, cr, uid, ids):
792         """
793         @return: True or False
794         """
795         res = True
796         for production in self.browse(cr, uid, ids):
797             if not production.product_lines:
798                 if not self.action_compute(cr, uid, [production.id]):
799                     res = False
800         return res
801
802     def _get_auto_picking(self, cr, uid, production):
803         return True
804
805     def action_confirm(self, cr, uid, ids):
806         """ Confirms production order.
807         @return: Newly generated picking Id.
808         """
809         picking_id = False
810         proc_ids = []
811         seq_obj = self.pool.get('ir.sequence')
812         pick_obj = self.pool.get('stock.picking')
813         move_obj = self.pool.get('stock.move')
814         proc_obj = self.pool.get('procurement.order')
815         wf_service = netsvc.LocalService("workflow")
816         for production in self.browse(cr, uid, ids):
817             if not production.product_lines:
818                 self.action_compute(cr, uid, [production.id])
819                 production = self.browse(cr, uid, [production.id])[0]
820             routing_loc = None
821             pick_type = 'internal'
822             address_id = False
823             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
824                 routing_loc = production.bom_id.routing_id.location_id
825                 if routing_loc.usage <> 'internal':
826                     pick_type = 'out'
827                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
828                 routing_loc = routing_loc.id
829             pick_name = seq_obj.get(cr, uid, 'stock.picking.' + pick_type)
830             picking_id = pick_obj.create(cr, uid, {
831                 'name': pick_name,
832                 'origin': (production.origin or '').split(':')[0] + ':' + production.name,
833                 'type': pick_type,
834                 'move_type': 'one',
835                 'state': 'auto',
836                 'address_id': address_id,
837                 'auto_picking': self._get_auto_picking(cr, uid, production),
838                 'company_id': production.company_id.id,
839             })
840
841             source = production.product_id.product_tmpl_id.property_stock_production.id
842             data = {
843                 'name':'PROD:' + production.name,
844                 'date': production.date_planned,
845                 'product_id': production.product_id.id,
846                 'product_qty': production.product_qty,
847                 'product_uom': production.product_uom.id,
848                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
849                 'product_uos': production.product_uos and production.product_uos.id or False,
850                 'location_id': source,
851                 'location_dest_id': production.location_dest_id.id,
852                 'move_dest_id': production.move_prod_id.id,
853                 'state': 'waiting',
854                 'company_id': production.company_id.id,
855             }
856             res_final_id = move_obj.create(cr, uid, data)
857
858             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
859             moves = []
860             for line in production.product_lines:
861                 move_id = False
862                 newdate = production.date_planned
863                 if line.product_id.type in ('product', 'consu'):
864                     res_dest_id = move_obj.create(cr, uid, {
865                         'name':'PROD:' + production.name,
866                         'date': production.date_planned,
867                         'product_id': line.product_id.id,
868                         'product_qty': line.product_qty,
869                         'product_uom': line.product_uom.id,
870                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
871                         'product_uos': line.product_uos and line.product_uos.id or False,
872                         'location_id': routing_loc or production.location_src_id.id,
873                         'location_dest_id': source,
874                         'move_dest_id': res_final_id,
875                         'state': 'waiting',
876                         'company_id': production.company_id.id,
877                     })
878                     moves.append(res_dest_id)
879                     move_id = move_obj.create(cr, uid, {
880                         'name':'PROD:' + production.name,
881                         'picking_id':picking_id,
882                         'product_id': line.product_id.id,
883                         'product_qty': line.product_qty,
884                         'product_uom': line.product_uom.id,
885                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
886                         'product_uos': line.product_uos and line.product_uos.id or False,
887                         'date': newdate,
888                         'move_dest_id': res_dest_id,
889                         'location_id': production.location_src_id.id,
890                         'location_dest_id': routing_loc or production.location_src_id.id,
891                         'state': 'waiting',
892                         'company_id': production.company_id.id,
893                     })
894                 proc_id = proc_obj.create(cr, uid, {
895                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
896                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
897                     'date_planned': newdate,
898                     'product_id': line.product_id.id,
899                     'product_qty': line.product_qty,
900                     'product_uom': line.product_uom.id,
901                     'product_uos_qty': line.product_uos and line.product_qty or False,
902                     'product_uos': line.product_uos and line.product_uos.id or False,
903                     'location_id': production.location_src_id.id,
904                     'procure_method': line.product_id.procure_method,
905                     'move_id': move_id,
906                     'company_id': production.company_id.id,
907                 })
908                 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
909                 proc_ids.append(proc_id)
910             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
911             self.write(cr, uid, [production.id], {'picking_id': picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
912             message = _("Manufacturing order '%s' is scheduled for the %s.") % (
913                 production.name,
914                 datetime.strptime(production.date_planned,'%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y'),
915             )
916             self.log(cr, uid, production.id, message)
917         return picking_id
918
919     def force_production(self, cr, uid, ids, *args):
920         """ Assigns products.
921         @param *args: Arguments
922         @return: True
923         """
924         pick_obj = self.pool.get('stock.picking')
925         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
926         return True
927
928 mrp_production()
929
930 class mrp_production_workcenter_line(osv.osv):
931     _name = 'mrp.production.workcenter.line'
932     _description = 'Work Order'
933     _order = 'sequence'
934
935     _columns = {
936         'name': fields.char('Work Order', size=64, required=True),
937         'workcenter_id': fields.many2one('mrp.workcenter', 'Work Center', required=True),
938         'cycle': fields.float('Nbr of cycles', digits=(16,2)),
939         'hour': fields.float('Nbr of hours', digits=(16,2)),
940         'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of work orders."),
941         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade', required=True),
942     }
943     _defaults = {
944         'sequence': lambda *a: 1,
945         'hour': lambda *a: 0,
946         'cycle': lambda *a: 0,
947     }
948 mrp_production_workcenter_line()
949
950 class mrp_production_product_line(osv.osv):
951     _name = 'mrp.production.product.line'
952     _description = 'Production Scheduled Product'
953     _columns = {
954         'name': fields.char('Name', size=64, required=True),
955         'product_id': fields.many2one('product.product', 'Product', required=True),
956         'product_qty': fields.float('Product Qty', required=True),
957         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
958         'product_uos_qty': fields.float('Product UOS Qty'),
959         'product_uos': fields.many2one('product.uom', 'Product UOS'),
960         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
961     }
962 mrp_production_product_line()
963
964 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: