[FIX]:lp-500931 No analytic entry is generated finishing one production order
[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 true, 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={}):
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         bom_obj = self.pool.get('mrp.bom')
136         bom_id = context and context.get('active_id', False) or False
137         cr.execute('select id from mrp_bom')
138         if all(bom_id != r[0] for r in cr.fetchall()):
139             ids.sort()
140             bom_id = ids[0]
141         bom_parent = bom_obj.browse(cr, uid, bom_id)
142         for bom in self.browse(cr, uid, ids, context=context):
143             if (bom_parent) or (bom.id == bom_id):
144                 result[bom.id] = map(lambda x: x.id, bom.bom_lines)
145             else:
146                 result[bom.id] = []
147             if bom.bom_lines:
148                 continue
149             ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
150             if (bom.type=='phantom' or ok):
151                 sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
152                 if sids:
153                     bom2 = bom_obj.browse(cr, uid, sids[0], context=context)
154                     result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
155
156         return result
157
158     def _compute_type(self, cr, uid, ids, field_name, arg, context):
159         """ Sets particular method for the selected bom type.
160         @param field_name: Name of the field
161         @param arg: User defined argument
162         @return:  Dictionary of values
163         """
164         res = dict(map(lambda x: (x,''), ids))
165         for line in self.browse(cr, uid, ids):
166             if line.type == 'phantom' and not line.bom_id:
167                 res[line.id] = 'set'
168                 continue
169             if line.bom_lines or line.type == 'phantom':
170                 continue
171             if line.product_id.supply_method == 'produce':
172                 if line.product_id.procure_method == 'make_to_stock':
173                     res[line.id] = 'stock'
174                 else:
175                     res[line.id] = 'order'
176         return res
177
178     _columns = {
179         'name': fields.char('Name', size=64, required=True),
180         'code': fields.char('Reference', size=16),
181         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the bills of material without removing it."),
182         'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True,
183                                  help= "If a sub-product is used in several products, it can be useful to create its own BoM. "\
184                                  "Though if you don't want separated production orders for this sub-product, select Set/Phantom as BoM type. "\
185                                  "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."),
186         'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
187         'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
188         'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
189         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of bills of material."),
190         'position': fields.char('Internal Reference', size=64, help="Reference to a position in an external plan."),
191         'product_id': fields.many2one('product.product', 'Product', required=True),
192         'product_uos_qty': fields.float('Product UOS Qty'),
193         '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."),
194         'product_qty': fields.float('Product Qty', required=True),
195         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, help="UoM (Unit of Measure) is the unit of measurement for the inventory control"),
196         'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity."),
197         'product_efficiency': fields.float('Manufacturing Efficiency', required=True, help="A factor of 0.9 means a loss of 10% within the production process."),
198         'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
199         'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
200         '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."),
201         'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
202         'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
203         'child_complete_ids': fields.function(_child_compute, relation='mrp.bom', method=True, string="BoM Hierarchy", type='many2many'),
204         'company_id': fields.many2one('res.company','Company',required=True),
205     }
206     _defaults = {
207         'active': lambda *a: 1,
208         'product_efficiency': lambda *a: 1.0,
209         'product_qty': lambda *a: 1.0,
210         'product_rounding': lambda *a: 0.0,
211         'type': lambda *a: 'normal',
212         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.bom', context=c),
213     }
214     _order = "sequence"
215     _sql_constraints = [
216         ('bom_qty_zero', 'CHECK (product_qty>0)',  'All product quantities must be greater than 0.\n' \
217             'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
218     ]
219
220     def _check_recursion(self, cr, uid, ids):
221         level = 100
222         while len(ids):
223             cr.execute('select distinct bom_id from mrp_bom where id IN %s',(tuple(ids),))
224             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
225             if not level:
226                 return False
227             level -= 1
228         return True
229     _constraints = [
230         (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
231     ]
232
233
234     def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
235         """ Changes UoM and name if product_id changes.
236         @param name: Name of the field
237         @param product_id: Changed product_id
238         @return:  Dictionary of changed values
239         """
240         if product_id:
241             prod = self.pool.get('product.product').browse(cr, uid, [product_id])[0]
242             v = {'product_uom': prod.uom_id.id}
243             if not name:
244                 v['name'] = prod.name
245             return {'value': v}
246         return {}
247
248     def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
249         """ Finds BoM for particular product and product uom.
250         @param product_id: Selected product.
251         @param product_uom: Unit of measure of a product.
252         @param properties: List of related properties.
253         @return: False or BoM id.
254         """
255         cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
256         ids = map(lambda x: x[0], cr.fetchall())
257         max_prop = 0
258         result = False
259         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
260             prop = 0
261             for prop_id in bom.property_ids:
262                 if prop_id.id in properties:
263                     prop += 1
264             if (prop > max_prop) or ((max_prop == 0) and not result):
265                 result = bom.id
266                 max_prop = prop
267         return result
268
269     def _bom_explode(self, cr, uid, bom, factor, properties=[], addthis=False, level=0):
270         """ Finds Products and Workcenters for related BoM for manufacturing order.
271         @param bom: BoM of particular product.
272         @param factor: Factor of product UoM.
273         @param properties: A List of properties Ids.
274         @param addthis: If BoM found then True else False.
275         @param level: Depth level to find BoM lines starts from 10.
276         @return: result: List of dictionaries containing product details.
277                  result2: List of dictionaries containing workcenter details.
278         """
279         factor = factor / (bom.product_efficiency or 1.0)
280         factor = rounding(factor, bom.product_rounding)
281         if factor < bom.product_rounding:
282             factor = bom.product_rounding
283         result = []
284         result2 = []
285         phantom = False
286         if bom.type == 'phantom' and not bom.bom_lines:
287             newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
288             if newbom:
289                 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
290                 result = result + res[0]
291                 result2 = result2 + res[1]
292                 phantom = True
293             else:
294                 phantom = False
295         if not phantom:
296             if addthis and not bom.bom_lines:
297                 result.append(
298                 {
299                     'name': bom.product_id.name,
300                     'product_id': bom.product_id.id,
301                     'product_qty': bom.product_qty * factor,
302                     'product_uom': bom.product_uom.id,
303                     'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
304                     'product_uos': bom.product_uos and bom.product_uos.id or False,
305                 })
306             if bom.routing_id:
307                 for wc_use in bom.routing_id.workcenter_lines:
308                     wc = wc_use.workcenter_id
309                     d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
310                     mult = (d + (m and 1.0 or 0.0))
311                     cycle = mult * wc_use.cycle_nbr
312                     result2.append({
313                         'name': tools.ustr(wc_use.name) + ' - '  + tools.ustr(bom.product_id.name),
314                         'workcenter_id': wc.id,
315                         'sequence': level+(wc_use.sequence or 0),
316                         'cycle': cycle,
317                         '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)),
318                     })
319             for bom2 in bom.bom_lines:
320                 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
321                 result = result + res[0]
322                 result2 = result2 + res[1]
323         return result, result2
324
325 mrp_bom()
326
327 class mrp_bom_revision(osv.osv):
328     _name = 'mrp.bom.revision'
329     _description = 'Bill of Material Revision'
330     _columns = {
331         'name': fields.char('Modification name', size=64, required=True),
332         'description': fields.text('Description'),
333         'date': fields.date('Modification Date'),
334         'indice': fields.char('Revision', size=16),
335         'last_indice': fields.char('last indice', size=64),
336         'author_id': fields.many2one('res.users', 'Author'),
337         'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
338     }
339
340     _defaults = {
341         'author_id': lambda x, y, z, c: z,
342         'date': lambda *a: time.strftime('%Y-%m-%d'),
343     }
344
345 mrp_bom_revision()
346
347 def rounding(f, r):
348     if not r:
349         return f
350     return round(f / r) * r
351
352 class mrp_production(osv.osv):
353     """
354     Production Orders / Manufacturing Orders
355     """
356     _name = 'mrp.production'
357     _description = 'Manufacturing Order'
358     _date_name  = 'date_planned'
359
360     def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
361         """ Calculates total hours and total no. of cycles for a production order.
362         @param prop: Name of field.
363         @param unknow_none:
364         @return: Dictionary of values.
365         """
366         result = {}
367         for prod in self.browse(cr, uid, ids, context=context):
368             result[prod.id] = {
369                 'hour_total': 0.0,
370                 'cycle_total': 0.0,
371             }
372             for wc in prod.workcenter_lines:
373                 result[prod.id]['hour_total'] += wc.hour
374                 result[prod.id]['cycle_total'] += wc.cycle
375         return result
376
377     def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
378         """ Finds production end date.
379         @param prop: Name of field.
380         @param unknow_none:
381         @return: Dictionary of values.
382         """
383         result = {}
384         for prod in self.browse(cr, uid, ids, context=context):
385             result[prod.id] = prod.date_planned
386         return result
387
388     def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
389         """ Finds production planned date.
390         @param prop: Name of field.
391         @param unknow_none:
392         @return: Dictionary of values.
393         """
394         result = {}
395         for prod in self.browse(cr, uid, ids, context=context):
396             result[prod.id] = prod.date_planned[:10]
397         return result
398
399     _columns = {
400         'name': fields.char('Reference', size=64, required=True),
401         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this production order request."),
402         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
403
404         'product_id': fields.many2one('product.product', 'Product', required=True, ),
405         'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
406         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
407         'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
408         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
409
410         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
411             help="Location where the system will look for components."),
412         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
413             help="Location where the system will stock the finished products."),
414
415         'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End Date'),
416         'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
417         'date_planned': fields.datetime('Scheduled date', required=True, select=1),
418         'date_start': fields.datetime('Start Date'),
419         'date_finished': fields.datetime('End Date'),
420
421         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
422         '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."),
423
424         'picking_id': fields.many2one('stock.picking', 'Picking list', readonly=True,
425             help="This is the internal picking list that brings the finished product to the production plan"),
426         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
427         '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)]}),
428         'move_lines2': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Consumed Products', domain=[('state','in', ('done', 'cancel'))]),
429         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','not in', ('done', 'cancel'))], states={'done':[('readonly',True)]}),
430         'move_created_ids2': fields.one2many('stock.move', 'production_id', 'Moves Created', domain=[('state','in', ('done', 'cancel'))]),
431         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
432         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Work Centers Utilisation'),
433         '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,
434                                     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\'.\
435                                     \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\'.'),
436         'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder', store=True),
437         'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder', store=True),
438         'company_id': fields.many2one('res.company','Company',required=True),
439     }
440     _defaults = {
441         'priority': lambda *a: '1',
442         'state': lambda *a: 'draft',
443         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
444         'product_qty':  lambda *a: 1.0,
445         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'mrp.production') or '/',
446         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c),
447     }
448     _order = 'priority desc, date_planned asc';
449
450     def _check_qty(self, cr, uid, ids):
451         orders = self.browse(cr, uid, ids)
452         for order in orders:
453             if order.product_qty <= 0:
454                 return False
455         return True
456
457     _constraints = [
458         (_check_qty, 'Order quantity cannot be negative or zero !', ['product_qty']),
459     ]
460
461     def unlink(self, cr, uid, ids, context=None):
462         productions = self.read(cr, uid, ids, ['state'])
463         unlink_ids = []
464         for s in productions:
465             if s['state'] in ['draft','cancel']:
466                 unlink_ids.append(s['id'])
467             else:
468                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
469         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
470
471     def copy(self, cr, uid, id, default=None, context=None):
472         if default is None:
473             default = {}
474         default.update({
475             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
476             'move_lines' : [],
477             'move_lines2' : [],
478             'move_created_ids' : [],
479             'move_created_ids2' : [],
480             'product_lines' : [],
481             'picking_id': False
482         })
483         return super(mrp_production, self).copy(cr, uid, id, default, context)
484
485     def location_id_change(self, cr, uid, ids, src, dest, context={}):
486         """ Changes destination location if source location is changed.
487         @param src: Source location id.
488         @param dest: Destination location id.
489         @return: Dictionary of values.
490         """
491         if dest:
492             return {}
493         if src:
494             return {'value': {'location_dest_id': src}}
495         return {}
496
497     def product_id_change(self, cr, uid, ids, product_id, context=None):
498         """ Finds UoM of changed product.
499         @param product_id: Id of changed product.
500         @return: Dictionary of values.
501         """
502         if not product_id:
503             return {'value': {
504                 'product_uom': False,
505                 'bom_id': False,
506                 'routing_id': False
507             }}
508         bom_obj = self.pool.get('mrp.bom')
509         product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
510         bom_id = bom_obj._bom_find(cr, uid, product.id, product.uom_id and product.uom_id.id, [])
511         routing_id = False
512         if bom_id:
513             bom_point = bom_obj.browse(cr, uid, bom_id, context=context)
514             routing_id = bom_point.routing_id.id or False
515         result = {
516             'product_uom': product.uom_id and product.uom_id.id or False,
517             'bom_id': bom_id,
518             'routing_id': routing_id
519         }
520         return {'value': result}
521
522     def bom_id_change(self, cr, uid, ids, bom_id, context=None):
523         """ Finds routing for changed BoM.
524         @param product: Id of product.
525         @return: Dictionary of values.
526         """
527         if not bom_id:
528             return {'value': {
529                 'routing_id': False
530             }}
531         bom_pool = self.pool.get('mrp.bom')
532         bom_point = bom_pool.browse(cr, uid, bom_id, context=context)
533         routing_id = bom_point.routing_id.id or False
534         result = {
535             'routing_id': routing_id
536         }
537         return {'value': result}
538
539     def action_picking_except(self, cr, uid, ids):
540         """ Changes the state to Exception.
541         @return: True
542         """
543         self.write(cr, uid, ids, {'state': 'picking_except'})
544         return True
545
546     def action_compute(self, cr, uid, ids, properties=[]):
547         """ Computes bills of material of a product.
548         @param properties: List containing dictionaries of properties.
549         @return: No. of products.
550         """
551         results = []
552         bom_obj = self.pool.get('mrp.bom')
553         prod_line_obj = self.pool.get('mrp.production.product.line')
554         workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
555         for production in self.browse(cr, uid, ids):
556             cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
557             cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
558             bom_point = production.bom_id
559             bom_id = production.bom_id.id
560             if not bom_point:
561                 bom_id = bom_obj._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
562                 if bom_id:
563                     bom_point = bom_obj.browse(cr, uid, bom_id)
564                     routing_id = bom_point.routing_id.id or False
565                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
566
567             if not bom_id:
568                 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
569
570             factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
571             res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
572             results = res[0]
573             results2 = res[1]
574             for line in results:
575                 line['production_id'] = production.id
576                 prod_line_obj.create(cr, uid, line)
577             for line in results2:
578                 line['production_id'] = production.id
579                 workcenter_line_obj.create(cr, uid, line)
580         return len(results)
581
582     def action_cancel(self, cr, uid, ids):
583         """ Cancels the production order and related stock moves.
584         @return: True
585         """
586         move_obj = self.pool.get('stock.move')
587         for production in self.browse(cr, uid, ids):
588             if production.move_created_ids:
589                 move_obj.action_cancel(cr, uid, [x.id for x in production.move_created_ids])
590             move_obj.action_cancel(cr, uid, [x.id for x in production.move_lines])
591         self.write(cr, uid, ids, {'state': 'cancel'})
592         return True
593
594     def action_ready(self, cr, uid, ids):
595         """ Changes the production state to Ready and location id of stock move.
596         @return: True
597         """
598         move_obj = self.pool.get('stock.move')
599         self.write(cr, uid, ids, {'state': 'ready'})
600
601         for (production_id,name) in self.name_get(cr, uid, ids):
602             production = self.browse(cr, uid, production_id)
603             if production.move_prod_id:
604                 move_obj.write(cr, uid, [production.move_prod_id.id],
605                         {'location_id': production.location_dest_id.id})
606
607             message = _("Manufacturing order '%s' is ready to produce.") % ( name,)
608             self.log(cr, uid, production_id, message)
609         return True
610
611     def action_production_end(self, cr, uid, ids):
612         """ Changes production state to Finish and writes finished date.
613         @return: True
614         """
615         for production in self.browse(cr, uid, ids):
616             self._costs_generate(cr, uid, production)
617         return self.write(cr, uid, ids, {'state': 'done', 'date_finished': time.strftime('%Y-%m-%d %H:%M:%S')})
618
619     def test_production_done(self, cr, uid, ids):
620         """ Tests whether production is done or not.
621         @return: True or False
622         """
623         res = True
624         for production in self.browse(cr, uid, ids):
625             if production.move_lines:
626                res = False
627
628             if production.move_created_ids:
629                res = False
630         return res
631
632     def action_produce(self, cr, uid, production_id, production_qty, production_mode, context=None):
633         """ To produce final product based on production mode (consume/consume&produce).
634         If Production mode is consume, all stock move lines of raw materials will be done/consumed.
635         If Production mode is consume & produce, all stock move lines of raw materials will be done/consumed
636         and stock move lines of final product will be also done/produced.
637         @param production_id: the ID of mrp.production object
638         @param production_qty: specify qty to produce
639         @param production_mode: specify production mode (consume/consume&produce).
640         @return: True
641         """
642
643         stock_mov_obj = self.pool.get('stock.move')
644         production = self.browse(cr, uid, production_id)
645
646         final_product_todo = []
647
648         produced_qty = 0
649         if production_mode == 'consume_produce':
650             produced_qty = production_qty
651
652         for produced_product in production.move_created_ids2:
653             if (produced_product.scrapped) or (produced_product.product_id.id<>production.product_id.id):
654                 continue
655             produced_qty += produced_product.product_qty
656
657         if production_mode in ['consume','consume_produce']:
658             consumed_products = {}
659             check = {}
660             scrapped = map(lambda x:x.scrapped,production.move_lines2).count(True)
661
662             for consumed_product in production.move_lines2:
663                 consumed = consumed_product.product_qty
664                 if consumed_product.scrapped:
665                     continue
666                 if not consumed_products.get(consumed_product.product_id.id, False):
667                     consumed_products[consumed_product.product_id.id] = consumed_product.product_qty
668                     check[consumed_product.product_id.id] = 0
669                 for f in production.product_lines:
670                     if f.product_id.id == consumed_product.product_id.id:
671                         if (len(production.move_lines2) - scrapped) > len(production.product_lines):
672                             check[consumed_product.product_id.id] += consumed_product.product_qty
673                             consumed = check[consumed_product.product_id.id]
674                         rest_consumed = produced_qty * f.product_qty / production.product_qty - consumed
675                         consumed_products[consumed_product.product_id.id] = rest_consumed
676
677             for raw_product in production.move_lines:
678                 for f in production.product_lines:
679                     if f.product_id.id == raw_product.product_id.id:
680                         consumed_qty = consumed_products.get(raw_product.product_id.id, 0)
681                         if consumed_qty == 0:
682                             consumed_qty = production_qty * f.product_qty / production.product_qty
683                         if consumed_qty > 0:
684                             stock_mov_obj.action_consume(cr, uid, [raw_product.id], consumed_qty, production.location_src_id.id, context=context)
685
686         if production_mode == 'consume_produce':
687             # To produce remaining qty of final product
688             vals = {'state':'confirmed'}
689             final_product_todo = [x.id for x in production.move_created_ids]
690             stock_mov_obj.write(cr, uid, final_product_todo, vals)
691             produced_products = {}
692             for produced_product in production.move_created_ids2:
693                 if produced_product.scrapped:
694                     continue
695                 if not produced_products.get(produced_product.product_id.id, False):
696                     produced_products[produced_product.product_id.id] = 0
697                 produced_products[produced_product.product_id.id] += produced_product.product_qty
698
699             for produce_product in production.move_created_ids:
700                 produced_qty = produced_products.get(produce_product.product_id.id, 0)
701                 rest_qty = production.product_qty - produced_qty
702                 if rest_qty <= production_qty:
703                    production_qty = rest_qty
704                 if rest_qty > 0 :
705                     stock_mov_obj.action_consume(cr, uid, [produce_product.id], production_qty, context=context)
706
707         for raw_product in production.move_lines2:
708             new_parent_ids = []
709             parent_move_ids = [x.id for x in raw_product.move_history_ids]
710             for final_product in production.move_created_ids2:
711                 if final_product.id not in parent_move_ids:
712                     new_parent_ids.append(final_product.id)
713             for new_parent_id in new_parent_ids:
714                 stock_mov_obj.write(cr, uid, [raw_product.id], {'move_history_ids': [(4,new_parent_id)]})
715
716         wf_service = netsvc.LocalService("workflow")
717         wf_service.trg_validate(uid, 'mrp.production', production_id, 'button_produce_done', cr)
718         return True
719
720     def _costs_generate(self, cr, uid, production):
721         """ Calculates total costs at the end of the production.
722         @param production: Id of production order.
723         @return: Calculated amount.
724         """
725         amount = 0.0
726         analytic_line_obj = self.pool.get('account.analytic.line')
727         for wc_line in production.workcenter_lines:
728             wc = wc_line.workcenter_id
729             if wc.costs_journal_id and wc.costs_general_account_id:
730                 value = wc_line.hour * wc.costs_hour
731                 account = wc.costs_hour_account_id.id
732                 if value and account:
733                     amount += value
734                     analytic_line_obj.create(cr, uid, {
735                         'name': wc_line.name + ' (H)',
736                         'amount': value,
737                         'account_id': account,
738                         'general_account_id': wc.costs_general_account_id.id,
739                         'journal_id': wc.costs_journal_id.id,
740                         'ref': wc.code
741                     } )
742             if wc.costs_journal_id and wc.costs_general_account_id:
743                 value = wc_line.cycle * wc.costs_cycle
744                 account = wc.costs_cycle_account_id.id
745                 if value and account:
746                     amount += value
747                     analytic_line_obj.create(cr, uid, {
748                         'name': wc_line.name+' (C)',
749                         'amount': value,
750                         'account_id': account,
751                         'general_account_id': wc.costs_general_account_id.id,
752                         'journal_id': wc.costs_journal_id.id,
753                         'ref': wc.code,
754                         'product_id': production.product_id.id
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: