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