[FIX] mrp: Don't make the production order if the product has a
[odoo/odoo.git] / addons / mrp / mrp.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 from osv import fields
24 from osv import osv
25 import ir
26
27 import netsvc
28 import time
29 from mx import DateTime
30 from tools.translate import _
31
32 #----------------------------------------------------------
33 # Workcenters
34 #----------------------------------------------------------
35 # capacity_hour : capacity per hour. default: 1.0.
36 #          Eg: If 5 concurrent operations at one time: capacity = 5 (because 5 employees)
37 # unit_per_cycle : how many units are produced for one cycle
38 #
39 # TODO: Work Center may be recursive ?
40 #
41 class mrp_workcenter(osv.osv):
42     _name = 'mrp.workcenter'
43     _description = 'Workcenter'
44     _columns = {
45         'name': fields.char('Workcenter Name', size=64, required=True),
46         'active': fields.boolean('Active'),
47         'type': fields.selection([('machine','Machine'),('hr','Human Resource'),('tool','Tool')], 'Type', required=True),
48         'code': fields.char('Code', size=16),
49         'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="The normal working time of the workcenter."),
50         'note': fields.text('Description', help="Description of the workcenter. Explain here what's a cycle according to this workcenter."),
51
52         'capacity_per_cycle': fields.float('Capacity per Cycle', help="Number of operation this workcenter can do in parallel. If this workcenter represent a team of 5 workers, the capacity per cycle is 5."),
53
54         'time_cycle': fields.float('Time for 1 cycle (hour)', help="Time in hours for doing one cycle."),
55         'time_start': fields.float('Time before prod.', help="Time in hours for the setup."),
56         'time_stop': fields.float('Time after prod.', help="Time in hours for the cleaning."),
57         'time_efficiency': fields.float('Time Efficiency', help="Factor that multiplies all times expressed in the workcenter."),
58
59         'costs_hour': fields.float('Cost per hour'),
60         'costs_hour_account_id': fields.many2one('account.analytic.account', 'Hour Account', domain=[('type','<>','view')],
61             help="Complete this only if you want automatic analytic accounting entries on production orders."),
62         'costs_cycle': fields.float('Cost per cycle'),
63         'costs_cycle_account_id': fields.many2one('account.analytic.account', 'Cycle Account', domain=[('type','<>','view')],
64             help="Complete this only if you want automatic analytic accounting entries on production orders."),
65         'costs_journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal'),
66         'costs_general_account_id': fields.many2one('account.account', 'General Account', domain=[('type','<>','view')]),
67     }
68     _defaults = {
69         'active': lambda *a: 1,
70         'type': lambda *a: 'machine',
71         'time_efficiency': lambda *a: 1.0,
72         'capacity_per_cycle': lambda *a: 1.0,
73     }
74 mrp_workcenter()
75
76
77 class mrp_property_group(osv.osv):
78     _name = 'mrp.property.group'
79     _description = 'Property Group'
80     _columns = {
81         'name': fields.char('Property Group', size=64, required=True),
82         'description': fields.text('Description'),
83     }
84 mrp_property_group()
85
86 class mrp_property(osv.osv):
87     _name = 'mrp.property'
88     _description = 'Property'
89     _columns = {
90         'name': fields.char('Name', size=64, required=True),
91         'composition': fields.selection([('min','min'),('max','max'),('plus','plus')], 'Properties composition', required=True, help="Not used in computations, for information purpose only."),
92         'group_id': fields.many2one('mrp.property.group', 'Property Group', required=True),
93         'description': fields.text('Description'),
94     }
95     _defaults = {
96         'composition': lambda *a: 'min',
97     }
98 mrp_property()
99
100 class mrp_routing(osv.osv):
101     _name = 'mrp.routing'
102     _description = 'Routing'
103     _columns = {
104         'name': fields.char('Name', size=64, required=True),
105         'active': fields.boolean('Active'),
106         'code': fields.char('Code', size=8),
107
108         'note': fields.text('Description'),
109         'workcenter_lines': fields.one2many('mrp.routing.workcenter', 'routing_id', 'Workcenters'),
110
111         'location_id': fields.many2one('stock.location', 'Production Location',
112             help="Keep empty if you produce at the location where the finished products are needed." \
113                 "Set a location if you produce at a fixed location. This can be a partner location " \
114                 "if you subcontract the manufacturing operations."
115         ),
116     }
117     _defaults = {
118         'active': lambda *a: 1,
119     }
120 mrp_routing()
121
122 class mrp_routing_workcenter(osv.osv):
123     _name = 'mrp.routing.workcenter'
124     _description = 'Routing workcenter usage'
125     _columns = {
126         'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
127         'name': fields.char('Name', size=64, required=True),
128         'sequence': fields.integer('Sequence'),
129         'cycle_nbr': fields.float('Number of Cycle', required=True,
130             help="A cycle is defined in the workcenter definition."),
131         'hour_nbr': fields.float('Number of Hours', required=True),
132         'routing_id': fields.many2one('mrp.routing', 'Parent Routing', select=True, ondelete='cascade'),
133         'note': fields.text('Description')
134     }
135     _defaults = {
136         'cycle_nbr': lambda *a: 1.0,
137         'hour_nbr': lambda *a: 0.0,
138     }
139 mrp_routing_workcenter()
140
141 class mrp_bom(osv.osv):
142     _name = 'mrp.bom'
143     _description = 'Bill of Material'
144     def _child_compute(self, cr, uid, ids, name, arg, context={}):
145         result = {}
146         for bom in self.browse(cr, uid, ids, context=context):
147             result[bom.id] = map(lambda x: x.id, bom.bom_lines)
148             if bom.bom_lines:
149                 continue
150             ok = ((name=='child_complete_ids') and (bom.product_id.supply_method=='produce'))
151             if bom.type=='phantom' or ok:
152                 sids = self.pool.get('mrp.bom').search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
153                 if sids:
154                     bom2 = self.pool.get('mrp.bom').browse(cr, uid, sids[0], context=context)
155                     result[bom.id] += map(lambda x: x.id, bom2.bom_lines)
156         return result
157     def _compute_type(self, cr, uid, ids, field_name, arg, context):
158         res = dict(map(lambda x: (x,''), ids))
159         for line in self.browse(cr, uid, ids):
160             if line.type=='phantom' and not line.bom_id:
161                 res[line.id] = 'set'
162                 continue
163             if line.bom_lines or line.type=='phantom':
164                 continue
165             if line.product_id.supply_method=='produce':
166                 if line.product_id.procure_method=='make_to_stock':
167                     res[line.id] = 'stock'
168                 else:
169                     res[line.id] = 'order'
170         return res
171     _columns = {
172         'name': fields.char('Name', size=64, required=True),
173         'code': fields.char('Code', size=16),
174         'active': fields.boolean('Active'),
175         'type': fields.selection([('normal','Normal BoM'),('phantom','Sets / Phantom')], 'BoM Type', required=True, help=
176             "Use a phantom bill of material in raw materials lines that have to be " \
177             "automatically computed in on eproduction order and not one per level." \
178             "If you put \"Phantom/Set\" at the root level of a bill of material " \
179             "it is considered as a set or pack: the products are replaced by the components " \
180             "between the sale order to the picking without going through the production order." \
181             "The normal BoM will generate one production order per BoM level."),
182         'method': fields.function(_compute_type, string='Method', method=True, type='selection', selection=[('',''),('stock','On Stock'),('order','On Order'),('set','Set / Pack')]),
183         'date_start': fields.date('Valid From', help="Validity of this BoM or component. Keep empty if it's always valid."),
184         'date_stop': fields.date('Valid Until', help="Validity of this BoM or component. Keep empty if it's always valid."),
185         'sequence': fields.integer('Sequence'),
186         'position': fields.char('Internal Ref.', size=64, help="Reference to a position in an external plan."),
187         'product_id': fields.many2one('product.product', 'Product', required=True),
188         'product_uos_qty': fields.float('Product UOS Qty'),
189         'product_uos': fields.many2one('product.uom', 'Product UOS'),
190         'product_qty': fields.float('Product Qty', required=True),
191         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
192         'product_rounding': fields.float('Product Rounding', help="Rounding applied on the product quantity. For integer only values, put 1.0"),
193         'product_efficiency': fields.float('Product Efficiency', required=True, help="Efficiency on the production. A factor of 0.9 means a loss of 10% in the production."),
194         'bom_lines': fields.one2many('mrp.bom', 'bom_id', 'BoM Lines'),
195         'bom_id': fields.many2one('mrp.bom', 'Parent BoM', ondelete='cascade', select=True),
196         '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 futur loads on workcenters based on production plannification."),
197         'property_ids': fields.many2many('mrp.property', 'mrp_bom_property_rel', 'bom_id','property_id', 'Properties'),
198         'revision_ids': fields.one2many('mrp.bom.revision', 'bom_id', 'BoM Revisions'),
199         'revision_type': fields.selection([('numeric','numeric indices'),('alpha','alphabetical indices')], 'indice type'),
200         'child_ids': fields.function(_child_compute,relation='mrp.bom', method=True, string="BoM Hyerarchy", type='many2many'),
201         'child_complete_ids': fields.function(_child_compute,relation='mrp.bom', method=True, string="BoM Hyerarchy", type='many2many')
202     }
203     _defaults = {
204         'active': lambda *a: 1,
205         'product_efficiency': lambda *a: 1.0,
206         'product_qty': lambda *a: 1.0,
207         'product_rounding': lambda *a: 1.0,
208         'type': lambda *a: 'normal',
209     }
210     _order = "sequence"
211     _sql_constraints = [
212         ('bom_qty_zero', 'CHECK (product_qty>0)',  'All product quantities must be greater than 0.\n' \
213             'You should install the mrp_subproduct module if you want to manage extra products on BoMs !'),
214     ]
215
216     def _check_recursion(self, cr, uid, ids):
217         level = 500
218         while len(ids):
219             cr.execute('select distinct bom_id from mrp_bom where id in %s', (tuple(ids),))
220             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
221             if not level:
222                 return False
223             level -= 1
224         return True
225     _constraints = [
226         (_check_recursion, 'Error ! You can not create recursive BoM.', ['parent_id'])
227     ]
228
229
230     def onchange_product_id(self, cr, uid, ids, product_id, name, context={}):
231         if product_id:
232             prod=self.pool.get('product.product').browse(cr,uid,[product_id])[0]
233             v = {'product_uom':prod.uom_id.id}
234             if not name:
235                 v['name'] = prod.name
236             return {'value': v}
237         return {}
238
239     def _bom_find(self, cr, uid, product_id, product_uom, properties=[]):
240         bom_result = False
241         # Why searching on BoM without parent ?
242         cr.execute('select id from mrp_bom where product_id=%s and bom_id is null order by sequence', (product_id,))
243         ids = map(lambda x: x[0], cr.fetchall())
244         max_prop = 0
245         result = False
246         for bom in self.pool.get('mrp.bom').browse(cr, uid, ids):
247             prop = 0
248             for prop_id in bom.property_ids:
249                 if prop_id.id in properties:
250                     prop+=1
251             if (prop>max_prop) or ((max_prop==0) and not result):
252                 result = bom.id
253                 max_prop = prop
254         return result
255
256     def _bom_explode(self, cr, uid, bom, factor, properties, addthis=False, level=0):
257         factor = factor / (bom.product_efficiency or 1.0)
258         factor = rounding(factor, bom.product_rounding)
259         if factor<bom.product_rounding:
260             factor = bom.product_rounding
261         result = []
262         result2 = []
263         phantom=False
264         if bom.type=='phantom' and not bom.bom_lines:
265             newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
266             if newbom:
267                 res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factor*bom.product_qty, properties, addthis=True, level=level+10)
268                 result = result + res[0]
269                 result2 = result2 + res[1]
270                 phantom=True
271             else:
272                 phantom=False
273         if not phantom:
274             if addthis and not bom.bom_lines:
275                 result.append(
276                 {
277                     'name': bom.product_id.name,
278                     'product_id': bom.product_id.id,
279                     'product_qty': bom.product_qty * factor,
280                     'product_uom': bom.product_uom.id,
281                     'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
282                     'product_uos': bom.product_uos and bom.product_uos.id or False,
283                 })
284             if bom.routing_id:
285                 for wc_use in bom.routing_id.workcenter_lines:
286                     wc = wc_use.workcenter_id
287                     d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
288                     mult = (d + (m and 1.0 or 0.0))
289                     cycle = mult * wc_use.cycle_nbr
290                     result2.append({
291                         'name': bom.routing_id.name,
292                         'workcenter_id': wc.id,
293                         'sequence': level+(wc_use.sequence or 0),
294                         'cycle': cycle,
295                         'hour': float(wc_use.hour_nbr*mult + (wc.time_start+wc.time_stop+cycle*wc.time_cycle) * (wc.time_efficiency or 1.0)),
296                     })
297             for bom2 in bom.bom_lines:
298                 res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
299                 result = result + res[0]
300                 result2 = result2 + res[1]
301         return result, result2
302
303     def set_indices(self, cr, uid, ids, context = {}):
304         if not ids or (ids and not ids[0]):
305             return True
306         res = self.read(cr, uid, ids, ['revision_ids', 'revision_type'])
307         rev_ids = res[0]['revision_ids']
308         idx = 1
309         new_idx = []
310         for rev_id in rev_ids:
311             if res[0]['revision_type'] == 'numeric':
312                 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : idx})
313             else:
314                 self.pool.get('mrp.bom.revision').write(cr, uid, [rev_id], {'indice' : "%c"%(idx+96,)})
315             idx+=1
316         return True
317
318 mrp_bom()
319
320 class mrp_bom_revision(osv.osv):
321     _name = 'mrp.bom.revision'
322     _description = 'Bill of material revisions'
323     _columns = {
324         'name': fields.char('Modification name', size=64, required=True),
325         'description': fields.text('Description'),
326         'date': fields.date('Modification Date'),
327         'indice': fields.char('Revision', size=16),
328         'last_indice': fields.char('last indice', size=64),
329         'author_id': fields.many2one('res.users', 'Author'),
330         'bom_id': fields.many2one('mrp.bom', 'BoM', select=True),
331     }
332
333     _defaults = {
334         'author_id': lambda x,y,z,c: z,
335         'date': lambda *a: time.strftime('%Y-%m-%d'),
336     }
337
338 mrp_bom_revision()
339
340 def rounding(f, r):
341     if not r:
342         return f
343     return round(f / r) * r
344
345 class mrp_production(osv.osv):
346     _name = 'mrp.production'
347     _description = 'Production'
348     _date_name  = 'date_planned'
349
350     def _get_sale_order(self,cr,uid,ids,field_name=False):
351         move_obj=self.pool.get('stock.move')
352         def get_parent_move(move_id):
353             move = move_obj.browse(cr,uid,move_id)
354             if move.move_dest_id:
355                 return get_parent_move(move.move_dest_id.id)
356             return move_id
357         productions=self.read(cr,uid,ids,['id','move_prod_id'])
358         res={}
359         for production in productions:
360             res[production['id']]=False
361             if production.get('move_prod_id',False):
362                 parent_move_line=get_parent_move(production['move_prod_id'][0])
363                 if parent_move_line:
364                     move = move_obj.browse(cr,uid,parent_move_line)
365                     #TODO: fix me sale module can not be used here, 
366                     #as may be mrp can be installed without sale module
367                     if field_name=='name':
368                         res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.name or False
369                     if field_name=='client_order_ref':
370                         res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.client_order_ref or False
371         return res
372
373     def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
374         result = {}
375         for prod in self.browse(cr, uid, ids, context=context):
376             result[prod.id] = {
377                 'hour_total': 0.0,
378                 'cycle_total': 0.0,
379             }
380             for wc in prod.workcenter_lines:
381                 result[prod.id]['hour_total'] += wc.hour
382                 result[prod.id]['cycle_total'] += wc.cycle
383         return result
384
385     def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
386         result = {}
387         for prod in self.browse(cr, uid, ids, context=context):
388             result[prod.id] = prod.date_planned
389         return result
390
391     def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
392         result = {}
393         for prod in self.browse(cr, uid, ids, context=context):
394             result[prod.id] = prod.date_planned[:10]
395         return result
396
397     def _sale_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
398         return self._get_sale_order(cr,uid,ids,field_name='name')
399
400     def _sale_ref_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
401         return self._get_sale_order(cr,uid,ids,field_name='client_order_ref')
402
403     _columns = {
404         'name': fields.char('Reference', size=64, required=True),
405         'origin': fields.char('Origin', size=64),
406         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
407
408         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
409         'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
410         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
411         'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
412         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
413
414         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
415             help="Location where the system will look for products used in raw materials."),
416         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
417             help="Location where the system will stock the finished products."),
418
419         'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End'),
420         'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
421         'date_planned': fields.datetime('Scheduled date', required=True, select=1),
422         'date_start': fields.datetime('Start Date'),
423         'date_finnished': fields.datetime('End Date'),
424
425         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
426         'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null'),
427
428         'picking_id': fields.many2one('stock.picking', 'Packing list', readonly=True,
429             help="This is the internal picking list take bring the raw materials to the production plan."),
430         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
431         'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
432
433         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
434         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
435         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Workcenters Utilisation'),
436
437         'state': fields.selection([('draft','Draft'),('picking_except', 'Packing Exception'),('confirmed','Waiting Goods'),('ready','Ready to Produce'),('in_production','In Production'),('cancel','Canceled'),('done','Done')],'Status', readonly=True),
438         'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder'),
439         'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder'),
440
441         'sale_name': fields.function(_sale_name_calc, method=True, type='char', string='Sale Name'),
442         'sale_ref': fields.function(_sale_ref_calc, method=True, type='char', string='Sale Ref'),
443     }
444     _defaults = {
445         'priority': lambda *a: '1',
446         'state': lambda *a: 'draft',
447         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
448         'product_qty':  lambda *a: 1.0,
449         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
450     }
451     _order = 'date_planned asc, priority desc';
452     def unlink(self, cr, uid, ids, context=None):
453         productions = self.read(cr, uid, ids, ['state'])
454         unlink_ids = []
455         for s in productions:
456             if s['state'] in ['draft','cancel']:
457                 unlink_ids.append(s['id'])
458             else:
459                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
460         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
461
462     def copy(self, cr, uid, id, default=None,context=None):
463         if not default:
464             default = {}
465         default.update({
466             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
467             'move_lines' : [],
468             'move_created_ids': [],
469             'state': 'draft'
470         })
471         return super(mrp_production, self).copy(cr, uid, id, default, context)
472
473     def location_id_change(self, cr, uid, ids, src, dest, context={}):
474         if dest:
475             return {}
476         if src:
477             return {'value': {'location_dest_id': src}}
478         return {}
479
480     def product_id_change(self, cr, uid, ids, product):
481         if not product:
482             return {}
483         res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
484         uom = res['uom_id'] and res['uom_id'][0]
485         result = {'product_uom':uom}
486         return {'value':result}
487
488     def bom_id_change(self, cr, uid, ids, product):
489         if not product:
490             return {}
491         res = self.pool.get('mrp.bom').read(cr, uid, [product], ['routing_id'])[0]
492         routing_id = res['routing_id'] and res['routing_id'][0]
493         result = {'routing_id':routing_id}
494         return {'value':result}
495
496     def action_picking_except(self, cr, uid, ids):
497         self.write(cr, uid, ids, {'state':'picking_except'})
498         return True
499
500     def action_compute(self, cr, uid, ids, properties=[]):
501         results = []
502         for production in self.browse(cr, uid, ids):
503             cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
504             cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
505             bom_point = production.bom_id
506             bom_id = production.bom_id.id
507             if not bom_point:
508                 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
509                 if bom_id:
510                     bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id)
511                     routing_id = bom_point.routing_id.id or False
512                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
513
514             if not bom_id:
515                 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
516
517             #if bom_point.routing_id and bom_point.routing_id.location_id:
518             #   self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
519
520             factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
521             res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
522             results = res[0]
523             results2 = res[1]
524             for line in results:
525                 line['production_id'] = production.id
526                 self.pool.get('mrp.production.product.line').create(cr, uid, line)
527             for line in results2:
528                 line['production_id'] = production.id
529                 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
530         return len(results)
531
532     def action_cancel(self, cr, uid, ids):
533         for production in self.browse(cr, uid, ids):
534             if production.move_created_ids:
535                 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
536             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
537         self.write(cr, uid, ids, {'state':'cancel'}) #,'move_lines':[(6,0,[])]})
538         return True
539
540     #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
541     #     between the end of the picking list and the call to this function
542     def action_ready(self, cr, uid, ids):
543         self.write(cr, uid, ids, {'state':'ready'})
544         for production in self.browse(cr, uid, ids):
545             if production.move_prod_id:
546                 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
547                         {'location_id':production.location_dest_id.id})
548         return True
549
550     #TODO Review materials in function in_prod and prod_end.
551     def action_production_end(self, cr, uid, ids):
552 #        move_ids = []
553         for production in self.browse(cr, uid, ids):
554             for res in production.move_lines:
555                 for move in production.move_created_ids:
556                     #XXX must use the orm
557                     cr.execute('INSERT INTO stock_move_history_ids \
558                             (parent_id, child_id) VALUES (%s,%s)',
559                             (res.id, move.id))
560 #                move_ids.append(res.id)
561             vals= {'state':'confirmed'}
562             new_moves = [x.id for x in production.move_created_ids]
563             self.pool.get('stock.move').write(cr, uid, new_moves, vals)
564             if not production.date_finnished:
565                 self.write(cr, uid, [production.id],
566                         {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
567             self.pool.get('stock.move').check_assign(cr, uid, new_moves)
568             self.pool.get('stock.move').action_done(cr, uid, new_moves)
569             self._costs_generate(cr, uid, production)
570 #        self.pool.get('stock.move').action_done(cr, uid, move_ids)
571         self.write(cr,  uid, ids, {'state': 'done'})
572         return True
573
574     def _costs_generate(self, cr, uid, production):
575         amount = 0.0
576         for wc_line in production.workcenter_lines:
577             wc = wc_line.workcenter_id
578             if wc.costs_journal_id and wc.costs_general_account_id:
579                 value = wc_line.hour * wc.costs_hour
580                 account = wc.costs_hour_account_id.id
581                 if value and account:
582                     amount += value
583                     self.pool.get('account.analytic.line').create(cr, uid, {
584                         'name': wc_line.name+' (H)',
585                         'amount': value,
586                         'account_id': account,
587                         'general_account_id': wc.costs_general_account_id.id,
588                         'journal_id': wc.costs_journal_id.id,
589                         'code': wc.code
590                     } )
591             if wc.costs_journal_id and wc.costs_general_account_id:
592                 value = wc_line.cycle * wc.costs_cycle
593                 account = wc.costs_cycle_account_id.id
594                 if value and account:
595                     amount += value
596                     self.pool.get('account.analytic.line').create(cr, uid, {
597                         'name': wc_line.name+' (C)',
598                         'amount': value,
599                         'account_id': account,
600                         'general_account_id': wc.costs_general_account_id.id,
601                         'journal_id': wc.costs_journal_id.id,
602                         'code': wc.code
603                     } )
604         return amount
605
606     def action_in_production(self, cr, uid, ids):
607         move_ids = []
608         for production in self.browse(cr, uid, ids):
609             for res in production.move_lines:
610                 move_ids.append(res.id)
611             if not production.date_start:
612                 self.write(cr, uid, [production.id],
613                         {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
614         self.pool.get('stock.move').action_done(cr, uid, move_ids)
615         self.write(cr, uid, ids, {'state': 'in_production'})
616         return True
617
618     def test_if_product(self, cr, uid, ids):
619         res = True
620         for production in self.browse(cr, uid, ids):
621             if not production.product_lines:
622                 if not self.action_compute(cr, uid, [production.id]):
623                     res = False
624         return res
625
626     def _get_auto_picking(self, cr, uid, production):
627         return True
628
629     def action_confirm(self, cr, uid, ids):
630         picking_id=False
631         proc_ids = []
632         for production in self.browse(cr, uid, ids):
633             if not production.product_lines:
634                 self.action_compute(cr, uid, [production.id])
635                 production = self.browse(cr, uid, [production.id])[0]
636             routing_loc = None
637             pick_type = 'internal'
638             address_id = False
639             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
640                 routing_loc = production.bom_id.routing_id.location_id
641                 if routing_loc.usage<>'internal':
642                     pick_type = 'out'
643                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
644                 routing_loc = routing_loc.id
645             picking_id = self.pool.get('stock.picking').create(cr, uid, {
646                 'origin': (production.origin or '').split(':')[0] +':'+production.name,
647                 'type': pick_type,
648                 'move_type': 'one',
649                 'state': 'auto',
650                 'address_id': address_id,
651                 'auto_picking': self._get_auto_picking(cr, uid, production),
652             })
653
654             source = production.product_id.product_tmpl_id.property_stock_production.id
655             data = {
656                 'name':'PROD:'+production.name,
657                 'date_planned': production.date_planned,
658                 'product_id': production.product_id.id,
659                 'product_qty': production.product_qty,
660                 'product_uom': production.product_uom.id,
661                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
662                 'product_uos': production.product_uos and production.product_uos.id or False,
663                 'location_id': source,
664                 'location_dest_id': production.location_dest_id.id,
665                 'move_dest_id': production.move_prod_id.id,
666                 'state': 'waiting'
667             }
668             res_final_id = self.pool.get('stock.move').create(cr, uid, data)
669
670             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
671             moves = []
672             for line in production.product_lines:
673                 move_id=False
674                 newdate = production.date_planned
675                 if line.product_id.type in ('product', 'consu'):
676                     res_dest_id = self.pool.get('stock.move').create(cr, uid, {
677                         'name':'PROD:'+production.name,
678                         'date_planned': production.date_planned,
679                         'product_id': line.product_id.id,
680                         'product_qty': line.product_qty,
681                         'product_uom': line.product_uom.id,
682                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
683                         'product_uos': line.product_uos and line.product_uos.id or False,
684                         'location_id': routing_loc or production.location_src_id.id,
685                         'location_dest_id': source,
686                         'move_dest_id': res_final_id,
687                         'state': 'waiting',
688                     })
689                     moves.append(res_dest_id)
690                     move_id = self.pool.get('stock.move').create(cr, uid, {
691                         'name':'PROD:'+production.name,
692                         'picking_id':picking_id,
693                         'product_id': line.product_id.id,
694                         'product_qty': line.product_qty,
695                         'product_uom': line.product_uom.id,
696                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
697                         'product_uos': line.product_uos and line.product_uos.id or False,
698                         'date_planned': newdate,
699                         'move_dest_id': res_dest_id,
700                         'location_id': production.location_src_id.id,
701                         'location_dest_id': routing_loc or production.location_src_id.id,
702                         'state': 'waiting',
703                     })
704                 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
705                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
706                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
707                     'date_planned': newdate,
708                     'product_id': line.product_id.id,
709                     'product_qty': line.product_qty,
710                     'product_uom': line.product_uom.id,
711                     'product_uos_qty': line.product_uos and line.product_qty or False,
712                     'product_uos': line.product_uos and line.product_uos.id or False,
713                     'location_id': production.location_src_id.id,
714                     'procure_method': line.product_id.procure_method,
715                     'move_id': move_id,
716                 })
717                 wf_service = netsvc.LocalService("workflow")
718                 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
719                 proc_ids.append(proc_id)
720             wf_service = netsvc.LocalService("workflow")
721             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
722             self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
723         return picking_id
724
725     def force_production(self, cr, uid, ids, *args):
726         pick_obj = self.pool.get('stock.picking')
727         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
728         return True
729
730 mrp_production()
731
732
733 class stock_move(osv.osv):
734     _name = 'stock.move'
735     _inherit = 'stock.move'
736     _columns = {
737         'production_id': fields.many2one('mrp.production', 'Production', select=True),
738     }
739 stock_move()
740
741 class mrp_production_workcenter_line(osv.osv):
742     _name = 'mrp.production.workcenter.line'
743     _description = 'Work Orders'
744     _order = 'sequence'
745     _columns = {
746         'name': fields.char('Work Order', size=64, required=True),
747         'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
748         'cycle': fields.float('Nbr of cycle', digits=(16,2)),
749         'hour': fields.float('Nbr of hour', digits=(16,2)),
750         'sequence': fields.integer('Sequence', required=True),
751         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
752     }
753     _defaults = {
754         'sequence': lambda *a: 1,
755         'hour': lambda *a: 0,
756         'cycle': lambda *a: 0,
757     }
758 mrp_production_workcenter_line()
759
760 class mrp_production_product_line(osv.osv):
761     _name = 'mrp.production.product.line'
762     _description = 'Production scheduled products'
763     _columns = {
764         'name': fields.char('Name', size=64, required=True),
765         'product_id': fields.many2one('product.product', 'Product', required=True),
766         'product_qty': fields.float('Product Qty', required=True),
767         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
768         'product_uos_qty': fields.float('Product UOS Qty'),
769         'product_uos': fields.many2one('product.uom', 'Product UOS'),
770         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
771     }
772 mrp_production_product_line()
773
774 # ------------------------------------------------------------------
775 # Procurement
776 # ------------------------------------------------------------------
777 #
778 # Produce, Buy or Find products and place a move
779 #     then wizard for picking lists & move
780 #
781 class mrp_procurement(osv.osv):
782     _name = "mrp.procurement"
783     _description = "Procurement"
784     _columns = {
785         'name': fields.char('Name', size=64, required=True),
786         'origin': fields.char('Origin', size=64,
787             help="Reference of the document that created this procurement.\n"
788             "This is automatically completed by Open ERP."),
789         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
790         'date_planned': fields.datetime('Scheduled date', required=True),
791         'date_close': fields.datetime('Date Closed'),
792         'product_id': fields.many2one('product.product', 'Product', required=True),
793         'product_qty': fields.float('Quantity', required=True, states={'draft':[('readonly',False)]}, readonly=True),
794         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
795         'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
796         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
797         'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
798
799         'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
800
801         'close_move': fields.boolean('Close Move at end', required=True),
802         'location_id': fields.many2one('stock.location', 'Location', required=True),
803         'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
804             readonly=True, required=True, help="If you encode manually a procurement, you probably want to use" \
805             " a make to order method."),
806
807         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
808         'note': fields.text('Note'),
809
810         'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
811
812         'message': fields.char('Latest error', size=64),
813         'state': fields.selection([
814             ('draft','Draft'),
815             ('confirmed','Confirmed'),
816             ('exception','Exception'),
817             ('running','Running'),
818             ('cancel','Cancel'),
819             ('ready','Ready'),
820             ('done','Done'),
821             ('waiting','Waiting')], 'Status', required=True),
822         'note' : fields.text('Note'),
823     }
824     _defaults = {
825         'state': lambda *a: 'draft',
826         'priority': lambda *a: '1',
827         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
828         'close_move': lambda *a: 0,
829         'procure_method': lambda *a: 'make_to_order',
830     }
831
832     def unlink(self, cr, uid, ids, context=None):
833         procurements = self.read(cr, uid, ids, ['state'])
834         unlink_ids = []
835         for s in procurements:
836             if s['state'] in ['draft','cancel']:
837                 unlink_ids.append(s['id'])
838             else:
839                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Procurement Order(s) which are in %s State!' % s['state']))
840         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
841
842     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
843         if product_id:
844             w=self.pool.get('product.product').browse(cr,uid,product_id, context)
845             v = {
846                 'product_uom':w.uom_id.id,
847                 'product_uos':w.uos_id and w.uos_id.id or w.uom_id.id
848             }
849             return {'value': v}
850         return {}
851
852     def check_product(self, cr, uid, ids):
853         for procurement in self.browse(cr, uid, ids):
854             if procurement.product_id.type in ('product', 'consu'):
855                 return True
856         return False
857
858     def get_phantom_bom_id(self, cr, uid, ids, context=None):
859         for procurement in self.browse(cr, uid, ids, context=context):
860             if procurement.move_id and procurement.move_id.product_id.supply_method=='produce' \
861                  and procurement.move_id.product_id.procure_method=='make_to_order':
862                     phantom_bom_id = self.pool.get('mrp.bom').search(cr, uid, [
863                         ('product_id', '=', procurement.move_id.product_id.id),
864                         ('bom_id', '=', False),
865                         ('type', '=', 'phantom')]) 
866                     return phantom_bom_id 
867         return False
868
869     def check_move_cancel(self, cr, uid, ids, context={}):
870         res = True
871         ok = False
872         for procurement in self.browse(cr, uid, ids, context):
873             if procurement.move_id:
874                 ok = True
875                 if not procurement.move_id.state=='cancel':
876                     res = False
877         return res and ok
878
879     def check_move_done(self, cr, uid, ids, context={}):
880         res = True
881         for proc in self.browse(cr, uid, ids, context):
882             if proc.move_id:
883                 if not proc.move_id.state=='done':
884                     res = False
885         return res
886
887     #
888     # This method may be overrided by objects that override mrp.procurment
889     # for computing their own purpose
890     #
891     def _quantity_compute_get(self, cr, uid, proc, context={}):
892         if proc.product_id.type=='product':
893             if proc.move_id.product_uos:
894                 return proc.move_id.product_uos_qty
895         return False
896
897     def _uom_compute_get(self, cr, uid, proc, context={}):
898         if proc.product_id.type=='product':
899             if proc.move_id.product_uos:
900                 return proc.move_id.product_uos.id
901         return False
902
903     #
904     # Return the quantity of product shipped/produced/served, wich may be
905     # different from the planned quantity
906     #
907     def quantity_get(self, cr, uid, id, context={}):
908         proc = self.browse(cr, uid, id, context)
909         result = self._quantity_compute_get(cr, uid, proc, context)
910         if not result:
911             result = proc.product_qty
912         return result
913
914     def uom_get(self, cr, uid, id, context=None):
915         proc = self.browse(cr, uid, id, context)
916         result = self._uom_compute_get(cr, uid, proc, context)
917         if not result:
918             result = proc.product_uom.id
919         return result
920
921     def check_waiting(self, cr, uid, ids, context=[]):
922         for procurement in self.browse(cr, uid, ids, context=context):
923             if procurement.move_id and procurement.move_id.state=='auto':
924                 return True
925         return False
926
927     def check_produce_service(self, cr, uid, procurement, context=[]):
928         return True
929
930     def check_produce_product(self, cr, uid, procurement, context=[]):
931         properties = [x.id for x in procurement.property_ids]
932         bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
933         if not bom_id:
934             cr.execute('update mrp_procurement set message=%s where id=%s', (_('No BoM defined for this product !'), procurement.id))
935             return False
936         return True
937
938     def check_make_to_stock(self, cr, uid, ids, context={}):
939         ok = True
940         for procurement in self.browse(cr, uid, ids, context=context):
941             if procurement.product_id.type=='service':
942                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
943             else:
944                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
945         return ok
946
947     def check_produce(self, cr, uid, ids, context={}):
948         res = True
949         user = self.pool.get('res.users').browse(cr, uid, uid)
950         for procurement in self.browse(cr, uid, ids):
951             if procurement.product_id.product_tmpl_id.supply_method<>'produce':
952                 if procurement.product_id.seller_ids:
953                     partner = procurement.product_id.seller_ids[0].name
954                     if user.company_id and user.company_id.partner_id:
955                         if partner.id == user.company_id.partner_id.id:
956                             return True
957                 return False
958             if procurement.product_id.product_tmpl_id.type=='service':
959                 res = res and self.check_produce_service(cr, uid, procurement, context)
960             else:
961                 res = res and self.check_produce_product(cr, uid, procurement, context)
962             if not res:
963                 return False
964         return res
965
966     def check_buy(self, cr, uid, ids):
967         user = self.pool.get('res.users').browse(cr, uid, uid)
968         for procurement in self.browse(cr, uid, ids):
969             if procurement.product_id.product_tmpl_id.supply_method<>'buy':
970                 return False
971             if not procurement.product_id.seller_ids:
972                 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No supplier defined for this product !'), procurement.id))
973                 return False
974             partner = procurement.product_id.seller_ids[0].name
975             if user.company_id and user.company_id.partner_id:
976                 if partner.id == user.company_id.partner_id.id:
977                     return False
978             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
979             if not address_id:
980                 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No address defined for the supplier'), procurement.id))
981                 return False
982         return True
983
984     def test_cancel(self, cr, uid, ids):
985         for record in self.browse(cr, uid, ids):
986             if record.move_id and record.move_id.state=='cancel':
987                 return True
988         return False
989
990     def action_confirm(self, cr, uid, ids, context={}):
991         for procurement in self.browse(cr, uid, ids):
992             if procurement.product_qty <= 0.00:
993                 raise osv.except_osv(_('Data Insufficient !'), _('Please check the Quantity of Procurement Order(s), it should not be less than 1!'))
994             if procurement.product_id.type in ('product', 'consu'):
995                 if not procurement.move_id:
996                     source = procurement.location_id.id
997                     if procurement.procure_method=='make_to_order':
998                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
999                     id = self.pool.get('stock.move').create(cr, uid, {
1000                         'name': 'PROC:'+procurement.name,
1001                         'location_id': source,
1002                         'location_dest_id': procurement.location_id.id,
1003                         'product_id': procurement.product_id.id,
1004                         'product_qty':procurement.product_qty,
1005                         'product_uom': procurement.product_uom.id,
1006                         'date_planned': procurement.date_planned,
1007                         'state':'confirmed',
1008                     })
1009                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
1010                 else:
1011                     if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('draft','waiting',):
1012                         # properly call action_confirm() on stock.move to abide by the location chaining etc.
1013                         id = self.pool.get('stock.move').action_confirm(cr, uid, [procurement.move_id.id], context=context)
1014         self.write(cr, uid, ids, {'state':'confirmed','message':''})
1015         return True
1016
1017     def action_move_assigned(self, cr, uid, ids, context={}):
1018         self.write(cr, uid, ids, {'state':'running','message':_('from stock: products assigned.')})
1019         return True
1020
1021     def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
1022         return True
1023
1024     def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
1025         ok = True
1026         if procurement.move_id:
1027             id = procurement.move_id.id
1028             if not (procurement.move_id.state in ('done','assigned','cancel')):
1029                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
1030                 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
1031                 if not cr.fetchone()[0]:
1032                     cr.execute('update mrp_procurement set message=%s where id=%s', (_('from stock and no minimum orderpoint rule defined'), procurement.id))
1033         return ok
1034
1035     def action_produce_assign_service(self, cr, uid, ids, context={}):
1036         for procurement in self.browse(cr, uid, ids):
1037             self.write(cr, uid, [procurement.id], {'state':'running'})
1038         return True
1039
1040     def action_produce_assign_product(self, cr, uid, ids, context={}):
1041         produce_id = False
1042         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1043         for procurement in self.browse(cr, uid, ids):
1044             res_id = procurement.move_id.id
1045             loc_id = procurement.location_id.id
1046             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - DateTime.RelativeDateTime(days=procurement.product_id.product_tmpl_id.produce_delay or 0.0)
1047             newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
1048             produce_id = self.pool.get('mrp.production').create(cr, uid, {
1049                 'origin': procurement.origin,
1050                 'product_id': procurement.product_id.id,
1051                 'product_qty': procurement.product_qty,
1052                 'product_uom': procurement.product_uom.id,
1053                 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
1054                 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
1055                 'location_src_id': procurement.location_id.id,
1056                 'location_dest_id': procurement.location_id.id,
1057                 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
1058                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1059                 'move_prod_id': res_id,
1060             })
1061             self.write(cr, uid, [procurement.id], {'state':'running'})
1062             bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
1063                     [produce_id], properties=[x.id for x in procurement.property_ids])
1064             wf_service = netsvc.LocalService("workflow")
1065             wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1066             self.pool.get('stock.move').write(cr, uid, [res_id],
1067                     {'location_id':procurement.location_id.id})
1068         return produce_id
1069
1070     def action_po_assign(self, cr, uid, ids, context={}):
1071         purchase_id = False
1072         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1073         for procurement in self.browse(cr, uid, ids):
1074             res_id = procurement.move_id.id
1075             partner = procurement.product_id.seller_ids[0].name
1076             partner_id = partner.id
1077             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1078             pricelist_id = partner.property_product_pricelist_purchase.id
1079
1080             uom_id = procurement.product_id.uom_po_id.id
1081
1082             qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1083             if procurement.product_id.seller_ids[0].qty:
1084                 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1085
1086             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1087
1088             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
1089             newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1090             newdate = newdate - procurement.product_id.seller_ids[0].delay
1091
1092             #Passing partner_id to context for purchase order line integrity of Line name
1093             context.update({'lang':partner.lang, 'partner_id':partner_id})
1094             
1095             product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1096
1097             line = {
1098                 'name': product.partner_ref,
1099                 'product_qty': qty,
1100                 'product_id': procurement.product_id.id,
1101                 'product_uom': uom_id,
1102                 'price_unit': price,
1103                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1104                 'move_dest_id': res_id,
1105                 'notes':product.description_purchase,
1106             }
1107
1108             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1109             taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes_ids)
1110             line.update({
1111                 'taxes_id':[(6,0,taxes)]
1112             })
1113             purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1114                 'origin': procurement.origin,
1115                 'partner_id': partner_id,
1116                 'partner_address_id': address_id,
1117                 'location_id': procurement.location_id.id,
1118                 'pricelist_id': pricelist_id,
1119                 'order_line': [(0,0,line)],
1120                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1121             })
1122             self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1123         return purchase_id
1124
1125     def action_cancel(self, cr, uid, ids):
1126         todo = []
1127         todo2 = []
1128         for proc in self.browse(cr, uid, ids):
1129             if proc.close_move:
1130                 if proc.move_id.state not in ('done','cancel'):
1131                     todo2.append(proc.move_id.id)
1132             else:
1133                 if proc.move_id and proc.move_id.state=='waiting':
1134                     todo.append(proc.move_id.id)
1135         if len(todo2):
1136             self.pool.get('stock.move').action_cancel(cr, uid, todo2)
1137         if len(todo):
1138             self.pool.get('stock.move').write(cr, uid, todo, {'state':'assigned'})
1139         self.write(cr, uid, ids, {'state':'cancel'})
1140         wf_service = netsvc.LocalService("workflow")
1141         for id in ids:
1142             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1143         return True
1144
1145     def action_check_finnished(self, cr, uid, ids):
1146         return self.check_move_done(cr, uid, ids)
1147
1148     def action_check(self, cr, uid, ids):
1149         ok = False
1150         for procurement in self.browse(cr, uid, ids):
1151             if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1152                 self.action_done(cr, uid, [procurement.id])
1153                 ok = True
1154         return ok
1155
1156     def action_ready(self, cr, uid, ids):
1157         res = self.write(cr, uid, ids, {'state':'ready'})
1158         return res
1159
1160     def action_done(self, cr, uid, ids):
1161         for procurement in self.browse(cr, uid, ids):
1162             if procurement.move_id:
1163                 if procurement.close_move and (procurement.move_id.state <> 'done'):
1164                     self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1165         res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1166         wf_service = netsvc.LocalService("workflow")
1167         for id in ids:
1168             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1169         return res
1170
1171     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1172         '''
1173         use_new_cursor: False or the dbname
1174         '''
1175         if not context:
1176             context={}
1177         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1178         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1179                 use_new_cursor=use_new_cursor, context=context)
1180 mrp_procurement()
1181
1182
1183 class stock_warehouse_orderpoint(osv.osv):
1184     _name = "stock.warehouse.orderpoint"
1185     _description = "Orderpoint minimum rule"
1186     _columns = {
1187         'name': fields.char('Name', size=32, required=True),
1188         'active': fields.boolean('Active'),
1189         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1190         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
1191         'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
1192         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')], ondelete="cascade"),
1193         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1194         'product_min_qty': fields.float('Min Quantity', required=True,
1195             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1196             "a procurement to bring the virtual stock to the Max Quantity."),
1197         'product_max_qty': fields.float('Max Quantity', required=True,
1198             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1199             "a procurement to bring the virtual stock to the Max Quantity."),
1200         'qty_multiple': fields.integer('Qty Multiple', required=True,
1201             help="The procurement quantity will by rounded up to this multiple."),
1202         'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order', ondelete="set null")
1203     }
1204     _defaults = {
1205         'active': lambda *a: 1,
1206         'logic': lambda *a: 'max',
1207         'qty_multiple': lambda *a: 1,
1208         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1209         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1210     }
1211     
1212     _sql_constraints = [
1213         ( 'qty_multiple_check', 'CHECK( qty_multiple > 0 )', _('Qty Multiple must be greater than zero.')),
1214     ]
1215     
1216     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1217         if warehouse_id:
1218             w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1219             v = {'location_id':w.lot_stock_id.id}
1220             return {'value': v}
1221         return {}
1222     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1223         if product_id:
1224             prod=self.pool.get('product.product').browse(cr,uid,product_id)
1225             v = {'product_uom':prod.uom_id.id}
1226             return {'value': v}
1227         return {}
1228     def copy(self, cr, uid, id, default=None,context={}):
1229         if not default:
1230             default = {}
1231         default.update({
1232             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.warehouse.orderpoint') or '',
1233         })
1234         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
1235 stock_warehouse_orderpoint()
1236
1237
1238 class StockMove(osv.osv):
1239     _inherit = 'stock.move'
1240     _columns = {
1241         'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1242     }
1243     def copy(self, cr, uid, id, default=None, context=None):
1244         default = default or {}
1245         default['procurements'] = []
1246         return super(StockMove, self).copy(cr, uid, id, default, context)
1247
1248     def _action_explode(self, cr, uid, move, context={}):
1249         if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1250             bis = self.pool.get('mrp.bom').search(cr, uid, [
1251                 ('product_id','=',move.product_id.id),
1252                 ('bom_id','=',False),
1253                 ('type','=','phantom')])
1254             if bis:
1255                 factor = move.product_qty
1256                 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1257                 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1258                 state = 'confirmed'
1259                 if move.state=='assigned':
1260                     state='assigned'
1261                 for line in res[0]:
1262                     valdef = {
1263                         'picking_id': move.picking_id.id,
1264                         'product_id': line['product_id'],
1265                         'product_uom': line['product_uom'],
1266                         'product_qty': line['product_qty'],
1267                         'product_uos': line['product_uos'],
1268                         'product_uos_qty': line['product_uos_qty'],
1269                         'state': state,
1270                         'name': line['name'],
1271                         'move_history_ids': [(6,0,[move.id])],
1272                         'move_history_ids2': [(6,0,[])],
1273                         'procurements': []
1274                     }
1275                     mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1276                     prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1277                     proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1278                         'name': (move.picking_id.origin or ''),
1279                         'origin': (move.picking_id.origin or ''),
1280                         'date_planned': move.date_planned,
1281                         'product_id': line['product_id'],
1282                         'product_qty': line['product_qty'],
1283                         'product_uom': line['product_uom'],
1284                         'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1285                         'product_uos':  line['product_uos'],
1286                         'location_id': move.location_id.id,
1287                         'procure_method': prodobj.procure_method,
1288                         'move_id': mid,
1289                     })
1290                     wf_service = netsvc.LocalService("workflow")
1291                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1292                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1293                     'location_id': move.location_dest_id.id, # src and dest locations identical to have correct inventory, dummy move
1294                     'auto_validate': True,
1295                     'picking_id': False,
1296                     'state': 'waiting'
1297                 })
1298                 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1299                     wf_service = netsvc.LocalService("workflow")
1300                     wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1301         return True
1302 StockMove()
1303
1304
1305 class StockPicking(osv.osv):
1306     _inherit = 'stock.picking'
1307
1308     def test_finnished(self, cursor, user, ids):
1309         wf_service = netsvc.LocalService("workflow")
1310         res = super(StockPicking, self).test_finnished(cursor, user, ids)
1311         for picking in self.browse(cursor, user, ids):
1312             for move in picking.move_lines:
1313                 if move.state == 'done' and move.procurements:
1314                     for procurement in move.procurements:
1315                         wf_service.trg_validate(user, 'mrp.procurement',
1316                                 procurement.id, 'button_check', cursor)
1317         return res
1318
1319     #
1320     # Explode picking by replacing phantom BoMs
1321     #
1322     def action_explode(self, cr, uid, picks, *args):
1323         for move in self.pool.get('stock.move').browse(cr, uid, picks):
1324             self.pool.get('stock.move')._action_explode(cr, uid, move)
1325         return picks
1326
1327 StockPicking()
1328
1329 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1330