[FIX] Scheduler fix, introduced 5 days ago
[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),
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 ('+','.join(map(str,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                     if field_name=='name':
366                         res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.name or False
367                     if field_name=='client_order_ref':
368                         res[production['id']]=move.sale_line_id and move.sale_line_id.order_id.client_order_ref or False
369         return res
370
371     def _production_calc(self, cr, uid, ids, prop, unknow_none, context={}):
372         result = {}
373         for prod in self.browse(cr, uid, ids, context=context):
374             result[prod.id] = {
375                 'hour_total': 0.0,
376                 'cycle_total': 0.0,
377             }
378             for wc in prod.workcenter_lines:
379                 result[prod.id]['hour_total'] += wc.hour
380                 result[prod.id]['cycle_total'] += wc.cycle
381         return result
382
383     def _production_date_end(self, cr, uid, ids, prop, unknow_none, context={}):
384         result = {}
385         for prod in self.browse(cr, uid, ids, context=context):
386             result[prod.id] = prod.date_planned
387         return result
388
389     def _production_date(self, cr, uid, ids, prop, unknow_none, context={}):
390         result = {}
391         for prod in self.browse(cr, uid, ids, context=context):
392             result[prod.id] = prod.date_planned[:10]
393         return result
394
395     def _sale_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
396         return self._get_sale_order(cr,uid,ids,field_name='name')
397
398     def _sale_ref_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
399         return self._get_sale_order(cr,uid,ids,field_name='client_order_ref')
400
401     _columns = {
402         'name': fields.char('Reference', size=64, required=True),
403         'origin': fields.char('Origin', size=64),
404         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority'),
405
406         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','<>','service')]),
407         'product_qty': fields.float('Product Qty', required=True, states={'draft':[('readonly',False)]}, readonly=True),
408         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
409         'product_uos_qty': fields.float('Product UoS Qty', states={'draft':[('readonly',False)]}, readonly=True),
410         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
411
412         'location_src_id': fields.many2one('stock.location', 'Raw Materials Location', required=True,
413             help="Location where the system will look for products used in raw materials."),
414         'location_dest_id': fields.many2one('stock.location', 'Finished Products Location', required=True,
415             help="Location where the system will stock the finished products."),
416
417         'date_planned_end': fields.function(_production_date_end, method=True, type='date', string='Scheduled End'),
418         'date_planned_date': fields.function(_production_date, method=True, type='date', string='Scheduled Date'),
419         'date_planned': fields.datetime('Scheduled date', required=True, select=1),
420         'date_start': fields.datetime('Start Date'),
421         'date_finnished': fields.datetime('End Date'),
422
423         'bom_id': fields.many2one('mrp.bom', 'Bill of Material', domain=[('bom_id','=',False)]),
424         'routing_id': fields.many2one('mrp.routing', string='Routing', on_delete='set null'),
425
426         'picking_id': fields.many2one('stock.picking', 'Packing list', readonly=True,
427             help="This is the internal picking list take bring the raw materials to the production plan."),
428         'move_prod_id': fields.many2one('stock.move', 'Move product', readonly=True),
429         'move_lines': fields.many2many('stock.move', 'mrp_production_move_ids', 'production_id', 'move_id', 'Products Consummed'),
430
431         'move_created_ids': fields.one2many('stock.move', 'production_id', 'Moves Created'),
432         'product_lines': fields.one2many('mrp.production.product.line', 'production_id', 'Scheduled goods'),
433         'workcenter_lines': fields.one2many('mrp.production.workcenter.line', 'production_id', 'Workcenters Utilisation'),
434
435         '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),
436         'hour_total': fields.function(_production_calc, method=True, type='float', string='Total Hours', multi='workorder'),
437         'cycle_total': fields.function(_production_calc, method=True, type='float', string='Total Cycles', multi='workorder'),
438
439         'sale_name': fields.function(_sale_name_calc, method=True, type='char', string='Sale Name'),
440         'sale_ref': fields.function(_sale_ref_calc, method=True, type='char', string='Sale Ref'),
441     }
442     _defaults = {
443         'priority': lambda *a: '1',
444         'state': lambda *a: 'draft',
445         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
446         'product_qty':  lambda *a: 1.0,
447         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.production') or '/',
448     }
449     _order = 'date_planned asc, priority desc';
450     def unlink(self, cr, uid, ids, context=None):
451         productions = self.read(cr, uid, ids, ['state'])
452         unlink_ids = []
453         for s in productions:
454             if s['state'] in ['draft','cancel']:
455                 unlink_ids.append(s['id'])
456             else:
457                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Production Order(s) which are in %s State!' % s['state']))
458         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
459
460     def copy(self, cr, uid, id, default=None,context=None):
461         if not default:
462             default = {}
463         default.update({
464             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.production'),
465             'move_lines' : [],
466             'move_created_ids': [],
467             'state': 'draft'
468         })
469         return super(mrp_production, self).copy(cr, uid, id, default, context)
470
471     def location_id_change(self, cr, uid, ids, src, dest, context={}):
472         if dest:
473             return {}
474         if src:
475             return {'value': {'location_dest_id': src}}
476         return {}
477
478     def product_id_change(self, cr, uid, ids, product):
479         if not product:
480             return {}
481         res = self.pool.get('product.product').read(cr, uid, [product], ['uom_id'])[0]
482         uom = res['uom_id'] and res['uom_id'][0]
483         result = {'product_uom':uom}
484         return {'value':result}
485
486     def action_picking_except(self, cr, uid, ids):
487         self.write(cr, uid, ids, {'state':'picking_except'})
488         return True
489
490     def action_compute(self, cr, uid, ids, properties=[]):
491         results = []
492         for production in self.browse(cr, uid, ids):
493             cr.execute('delete from mrp_production_product_line where production_id=%s', (production.id,))
494             cr.execute('delete from mrp_production_workcenter_line where production_id=%s', (production.id,))
495             bom_point = production.bom_id
496             bom_id = production.bom_id.id
497             if not bom_point:
498                 bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, production.product_id.id, production.product_uom.id, properties)
499                 if bom_id:
500                     bom_point = self.pool.get('mrp.bom').browse(cr, uid, bom_id)
501                     routing_id = bom_point.routing_id.id or False
502                     self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
503
504             if not bom_id:
505                 raise osv.except_osv(_('Error'), _("Couldn't find bill of material for product"))
506
507             #if bom_point.routing_id and bom_point.routing_id.location_id:
508             #   self.write(cr, uid, [production.id], {'location_src_id': bom_point.routing_id.location_id.id})
509
510             factor = production.product_qty * production.product_uom.factor / bom_point.product_uom.factor
511             res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties)
512             results = res[0]
513             results2 = res[1]
514             for line in results:
515                 line['production_id'] = production.id
516                 self.pool.get('mrp.production.product.line').create(cr, uid, line)
517             for line in results2:
518                 line['production_id'] = production.id
519                 self.pool.get('mrp.production.workcenter.line').create(cr, uid, line)
520         return len(results)
521
522     def action_cancel(self, cr, uid, ids):
523         for production in self.browse(cr, uid, ids):
524             if production.move_created_ids:
525                 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_created_ids])
526             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in production.move_lines])
527         self.write(cr, uid, ids, {'state':'cancel'}) #,'move_lines':[(6,0,[])]})
528         return True
529
530     #XXX: may be a bug here; lot_lines are unreserved for a few seconds;
531     #     between the end of the picking list and the call to this function
532     def action_ready(self, cr, uid, ids):
533         self.write(cr, uid, ids, {'state':'ready'})
534         for production in self.browse(cr, uid, ids):
535             if production.move_prod_id:
536                 self.pool.get('stock.move').write(cr, uid, [production.move_prod_id.id],
537                         {'location_id':production.location_dest_id.id})
538         return True
539
540     #TODO Review materials in function in_prod and prod_end.
541     def action_production_end(self, cr, uid, ids):
542         move_ids = []
543         for production in self.browse(cr, uid, ids):
544             for res in production.move_lines:
545                 for move in production.move_created_ids:
546                     #XXX must use the orm
547                     cr.execute('INSERT INTO stock_move_history_ids \
548                             (parent_id, child_id) VALUES (%s,%s)',
549                             (res.id, move.id))
550                 move_ids.append(res.id)
551             vals= {'state':'confirmed'}
552             new_moves = [x.id for x in production.move_created_ids]
553             self.pool.get('stock.move').write(cr, uid, new_moves, vals)
554             if not production.date_finnished:
555                 self.write(cr, uid, [production.id],
556                         {'date_finnished': time.strftime('%Y-%m-%d %H:%M:%S')})
557             self.pool.get('stock.move').check_assign(cr, uid, new_moves)
558             self.pool.get('stock.move').action_done(cr, uid, new_moves)
559             self._costs_generate(cr, uid, production)
560         self.pool.get('stock.move').action_done(cr, uid, move_ids)
561         self.write(cr,  uid, ids, {'state': 'done'})
562         return True
563
564     def _costs_generate(self, cr, uid, production):
565         amount = 0.0
566         for wc_line in production.workcenter_lines:
567             wc = wc_line.workcenter_id
568             if wc.costs_journal_id and wc.costs_general_account_id:
569                 value = wc_line.hour * wc.costs_hour
570                 account = wc.costs_hour_account_id.id
571                 if value and account:
572                     amount += value
573                     self.pool.get('account.analytic.line').create(cr, uid, {
574                         'name': wc_line.name+' (H)',
575                         'amount': value,
576                         'account_id': account,
577                         'general_account_id': wc.costs_general_account_id.id,
578                         'journal_id': wc.costs_journal_id.id,
579                         'code': wc.code
580                     } )
581             if wc.costs_journal_id and wc.costs_general_account_id:
582                 value = wc_line.cycle * wc.costs_cycle
583                 account = wc.costs_cycle_account_id.id
584                 if value and account:
585                     amount += value
586                     self.pool.get('account.analytic.line').create(cr, uid, {
587                         'name': wc_line.name+' (C)',
588                         'amount': value,
589                         'account_id': account,
590                         'general_account_id': wc.costs_general_account_id.id,
591                         'journal_id': wc.costs_journal_id.id,
592                         'code': wc.code
593                     } )
594         return amount
595
596     def action_in_production(self, cr, uid, ids):
597         move_ids = []
598         for production in self.browse(cr, uid, ids):
599             for res in production.move_lines:
600                 move_ids.append(res.id)
601             if not production.date_start:
602                 self.write(cr, uid, [production.id],
603                         {'date_start': time.strftime('%Y-%m-%d %H:%M:%S')})
604         self.pool.get('stock.move').action_done(cr, uid, move_ids)
605         self.write(cr, uid, ids, {'state': 'in_production'})
606         return True
607
608     def test_if_product(self, cr, uid, ids):
609         res = True
610         for production in self.browse(cr, uid, ids):
611             if not production.product_lines:
612                 if not self.action_compute(cr, uid, [production.id]):
613                     res = False
614         return res
615
616     def _get_auto_picking(self, cr, uid, production):
617         return True
618
619     def action_confirm(self, cr, uid, ids):
620         picking_id=False
621         proc_ids = []
622         for production in self.browse(cr, uid, ids):
623             if not production.product_lines:
624                 self.action_compute(cr, uid, [production.id])
625                 production = self.browse(cr, uid, [production.id])[0]
626             routing_loc = None
627             pick_type = 'internal'
628             address_id = False
629             if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
630                 routing_loc = production.bom_id.routing_id.location_id
631                 if routing_loc.usage<>'internal':
632                     pick_type = 'out'
633                 address_id = routing_loc.address_id and routing_loc.address_id.id or False
634                 routing_loc = routing_loc.id
635             picking_id = self.pool.get('stock.picking').create(cr, uid, {
636                 'origin': (production.origin or '').split(':')[0] +':'+production.name,
637                 'type': pick_type,
638                 'move_type': 'one',
639                 'state': 'auto',
640                 'address_id': address_id,
641                 'auto_picking': self._get_auto_picking(cr, uid, production),
642             })
643
644             source = production.product_id.product_tmpl_id.property_stock_production.id
645             data = {
646                 'name':'PROD:'+production.name,
647                 'date_planned': production.date_planned,
648                 'product_id': production.product_id.id,
649                 'product_qty': production.product_qty,
650                 'product_uom': production.product_uom.id,
651                 'product_uos_qty': production.product_uos and production.product_uos_qty or False,
652                 'product_uos': production.product_uos and production.product_uos.id or False,
653                 'location_id': source,
654                 'location_dest_id': production.location_dest_id.id,
655                 'move_dest_id': production.move_prod_id.id,
656                 'state': 'waiting'
657             }
658             res_final_id = self.pool.get('stock.move').create(cr, uid, data)
659
660             self.write(cr, uid, [production.id], {'move_created_ids': [(6, 0, [res_final_id])]})
661             moves = []
662             for line in production.product_lines:
663                 move_id=False
664                 newdate = production.date_planned
665                 if line.product_id.type in ('product', 'consu'):
666                     res_dest_id = self.pool.get('stock.move').create(cr, uid, {
667                         'name':'PROD:'+production.name,
668                         'date_planned': production.date_planned,
669                         'product_id': line.product_id.id,
670                         'product_qty': line.product_qty,
671                         'product_uom': line.product_uom.id,
672                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
673                         'product_uos': line.product_uos and line.product_uos.id or False,
674                         'location_id': routing_loc or production.location_src_id.id,
675                         'location_dest_id': source,
676                         'move_dest_id': res_final_id,
677                         'state': 'waiting',
678                     })
679                     moves.append(res_dest_id)
680                     move_id = self.pool.get('stock.move').create(cr, uid, {
681                         'name':'PROD:'+production.name,
682                         'picking_id':picking_id,
683                         'product_id': line.product_id.id,
684                         'product_qty': line.product_qty,
685                         'product_uom': line.product_uom.id,
686                         'product_uos_qty': line.product_uos and line.product_uos_qty or False,
687                         'product_uos': line.product_uos and line.product_uos.id or False,
688                         'date_planned': newdate,
689                         'move_dest_id': res_dest_id,
690                         'location_id': production.location_src_id.id,
691                         'location_dest_id': routing_loc or production.location_src_id.id,
692                         'state': 'waiting',
693                     })
694                 proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
695                     'name': (production.origin or '').split(':')[0] + ':' + production.name,
696                     'origin': (production.origin or '').split(':')[0] + ':' + production.name,
697                     'date_planned': newdate,
698                     'product_id': line.product_id.id,
699                     'product_qty': line.product_qty,
700                     'product_uom': line.product_uom.id,
701                     'product_uos_qty': line.product_uos and line.product_qty or False,
702                     'product_uos': line.product_uos and line.product_uos.id or False,
703                     'location_id': production.location_src_id.id,
704                     'procure_method': line.product_id.procure_method,
705                     'move_id': move_id,
706                 })
707                 wf_service = netsvc.LocalService("workflow")
708                 wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
709                 proc_ids.append(proc_id)
710             wf_service = netsvc.LocalService("workflow")
711             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
712             self.write(cr, uid, [production.id], {'picking_id':picking_id, 'move_lines': [(6,0,moves)], 'state':'confirmed'})
713         return picking_id
714
715     def force_production(self, cr, uid, ids, *args):
716         pick_obj = self.pool.get('stock.picking')
717         pick_obj.force_assign(cr, uid, [prod.picking_id.id for prod in self.browse(cr, uid, ids)])
718         return True
719
720 mrp_production()
721
722
723 class stock_move(osv.osv):
724     _name = 'stock.move'
725     _inherit = 'stock.move'
726     _columns = {
727         'production_id': fields.many2one('mrp.production', 'Production', select=True),
728     }
729 stock_move()
730
731 class mrp_production_workcenter_line(osv.osv):
732     _name = 'mrp.production.workcenter.line'
733     _description = 'Work Orders'
734     _order = 'sequence'
735     _columns = {
736         'name': fields.char('Work Order', size=64, required=True),
737         'workcenter_id': fields.many2one('mrp.workcenter', 'Workcenter', required=True),
738         'cycle': fields.float('Nbr of cycle', digits=(16,2)),
739         'hour': fields.float('Nbr of hour', digits=(16,2)),
740         'sequence': fields.integer('Sequence', required=True),
741         'production_id': fields.many2one('mrp.production', 'Production Order', select=True, ondelete='cascade'),
742     }
743     _defaults = {
744         'sequence': lambda *a: 1,
745         'hour': lambda *a: 0,
746         'cycle': lambda *a: 0,
747     }
748 mrp_production_workcenter_line()
749
750 class mrp_production_product_line(osv.osv):
751     _name = 'mrp.production.product.line'
752     _description = 'Production scheduled products'
753     _columns = {
754         'name': fields.char('Name', size=64, required=True),
755         'product_id': fields.many2one('product.product', 'Product', required=True),
756         'product_qty': fields.float('Product Qty', required=True),
757         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
758         'product_uos_qty': fields.float('Product UOS Qty'),
759         'product_uos': fields.many2one('product.uom', 'Product UOS'),
760         'production_id': fields.many2one('mrp.production', 'Production Order', select=True),
761     }
762 mrp_production_product_line()
763
764 # ------------------------------------------------------------------
765 # Procurement
766 # ------------------------------------------------------------------
767 #
768 # Produce, Buy or Find products and place a move
769 #     then wizard for picking lists & move
770 #
771 class mrp_procurement(osv.osv):
772     _name = "mrp.procurement"
773     _description = "Procurement"
774     _columns = {
775         'name': fields.char('Name', size=64, required=True),
776         'origin': fields.char('Origin', size=64,
777             help="Reference of the document that created this procurement.\n"
778             "This is automatically completed by Open ERP."),
779         'priority': fields.selection([('0','Not urgent'),('1','Normal'),('2','Urgent'),('3','Very Urgent')], 'Priority', required=True),
780         'date_planned': fields.datetime('Scheduled date', required=True),
781         'date_close': fields.datetime('Date Closed'),
782         'product_id': fields.many2one('product.product', 'Product', required=True),
783         'product_qty': fields.float('Quantity', required=True, states={'draft':[('readonly',False)]}, readonly=True),
784         'product_uom': fields.many2one('product.uom', 'Product UoM', required=True, states={'draft':[('readonly',False)]}, readonly=True),
785         'product_uos_qty': fields.float('UoS Quantity', states={'draft':[('readonly',False)]}, readonly=True),
786         'product_uos': fields.many2one('product.uom', 'Product UoS', states={'draft':[('readonly',False)]}, readonly=True),
787         'move_id': fields.many2one('stock.move', 'Reservation', ondelete='set null'),
788
789         'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True),
790
791         'close_move': fields.boolean('Close Move at end', required=True),
792         'location_id': fields.many2one('stock.location', 'Location', required=True),
793         'procure_method': fields.selection([('make_to_stock','from stock'),('make_to_order','on order')], 'Procurement Method', states={'draft':[('readonly',False)], 'confirmed':[('readonly',False)]},
794             readonly=True, required=True, help="If you encode manually a procurement, you probably want to use" \
795             " a make to order method."),
796
797         'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
798         'note': fields.text('Note'),
799
800         'property_ids': fields.many2many('mrp.property', 'mrp_procurement_property_rel', 'procurement_id','property_id', 'Properties'),
801
802         'message': fields.char('Latest error', size=64),
803         'state': fields.selection([
804             ('draft','Draft'),
805             ('confirmed','Confirmed'),
806             ('exception','Exception'),
807             ('running','Running'),
808             ('cancel','Cancel'),
809             ('ready','Ready'),
810             ('done','Done'),
811             ('waiting','Waiting')], 'Status', required=True),
812         'note' : fields.text('Note'),
813     }
814     _defaults = {
815         'state': lambda *a: 'draft',
816         'priority': lambda *a: '1',
817         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
818         'close_move': lambda *a: 0,
819         'procure_method': lambda *a: 'make_to_order',
820     }
821
822     def unlink(self, cr, uid, ids, context=None):
823         procurements = self.read(cr, uid, ids, ['state'])
824         unlink_ids = []
825         for s in procurements:
826             if s['state'] in ['draft','cancel']:
827                 unlink_ids.append(s['id'])
828             else:
829                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Procurement Order(s) which are in %s State!' % s['state']))
830         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
831
832     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
833         if product_id:
834             w=self.pool.get('product.product').browse(cr,uid,product_id, context)
835             v = {
836                 'product_uom':w.uom_id.id,
837                 'product_uos':w.uos_id and w.uos_id.id or w.uom_id.id
838             }
839             return {'value': v}
840         return {}
841
842     def check_product(self, cr, uid, ids):
843         for procurement in self.browse(cr, uid, ids):
844             if procurement.product_id.type in ('product', 'consu'):
845                 return True
846         return False
847
848     def check_move_cancel(self, cr, uid, ids, context={}):
849         res = True
850         ok = False
851         for procurement in self.browse(cr, uid, ids, context):
852             if procurement.move_id:
853                 ok = True
854                 if not procurement.move_id.state=='cancel':
855                     res = False
856         return res and ok
857
858     def check_move_done(self, cr, uid, ids, context={}):
859         res = True
860         for proc in self.browse(cr, uid, ids, context):
861             if proc.move_id:
862                 if not proc.move_id.state=='done':
863                     res = False
864         return res
865
866     #
867     # This method may be overrided by objects that override mrp.procurment
868     # for computing their own purpose
869     #
870     def _quantity_compute_get(self, cr, uid, proc, context={}):
871         if proc.product_id.type=='product':
872             if proc.move_id.product_uos:
873                 return proc.move_id.product_uos_qty
874         return False
875
876     def _uom_compute_get(self, cr, uid, proc, context={}):
877         if proc.product_id.type=='product':
878             if proc.move_id.product_uos:
879                 return proc.move_id.product_uos.id
880         return False
881
882     #
883     # Return the quantity of product shipped/produced/served, wich may be
884     # different from the planned quantity
885     #
886     def quantity_get(self, cr, uid, id, context={}):
887         proc = self.browse(cr, uid, id, context)
888         result = self._quantity_compute_get(cr, uid, proc, context)
889         if not result:
890             result = proc.product_qty
891         return result
892
893     def uom_get(self, cr, uid, id, context=None):
894         proc = self.browse(cr, uid, id, context)
895         result = self._uom_compute_get(cr, uid, proc, context)
896         if not result:
897             result = proc.product_uom.id
898         return result
899
900     def check_waiting(self, cr, uid, ids, context=[]):
901         for procurement in self.browse(cr, uid, ids, context=context):
902             if procurement.move_id and procurement.move_id.state=='auto':
903                 return True
904         return False
905
906     def check_produce_service(self, cr, uid, procurement, context=[]):
907         return True
908
909     def check_produce_product(self, cr, uid, procurement, context=[]):
910         properties = [x.id for x in procurement.property_ids]
911         bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
912         if not bom_id:
913             cr.execute('update mrp_procurement set message=%s where id=%s', (_('No BoM defined for this product !'), procurement.id))
914             return False
915         return True
916
917     def check_make_to_stock(self, cr, uid, ids, context={}):
918         ok = True
919         for procurement in self.browse(cr, uid, ids, context=context):
920             if procurement.product_id.type=='service':
921                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
922             else:
923                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
924         return ok
925
926     def check_produce(self, cr, uid, ids, context={}):
927         res = True
928         user = self.pool.get('res.users').browse(cr, uid, uid)
929         for procurement in self.browse(cr, uid, ids):
930             if procurement.product_id.product_tmpl_id.supply_method<>'produce':
931                 if procurement.product_id.seller_ids:
932                     partner = procurement.product_id.seller_ids[0].name
933                     if user.company_id and user.company_id.partner_id:
934                         if partner.id == user.company_id.partner_id.id:
935                             return True
936                 return False
937             if procurement.product_id.product_tmpl_id.type=='service':
938                 res = res and self.check_produce_service(cr, uid, procurement, context)
939             else:
940                 res = res and self.check_produce_product(cr, uid, procurement, context)
941             if not res:
942                 return False
943         return res
944
945     def check_buy(self, cr, uid, ids):
946         user = self.pool.get('res.users').browse(cr, uid, uid)
947         for procurement in self.browse(cr, uid, ids):
948             if procurement.product_id.product_tmpl_id.supply_method<>'buy':
949                 return False
950             if not procurement.product_id.seller_ids:
951                 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No supplier defined for this product !'), procurement.id))
952                 return False
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 False
957             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
958             if not address_id:
959                 cr.execute('update mrp_procurement set message=%s where id=%s', (_('No address defined for the supplier'), procurement.id))
960                 return False
961         return True
962
963     def test_cancel(self, cr, uid, ids):
964         for record in self.browse(cr, uid, ids):
965             if record.move_id and record.move_id.state=='cancel':
966                 return True
967         return False
968
969     def action_confirm(self, cr, uid, ids, context={}):
970         for procurement in self.browse(cr, uid, ids):
971             if procurement.product_id.type in ('product', 'consu'):
972                 if not procurement.move_id:
973                     source = procurement.location_id.id
974                     if procurement.procure_method=='make_to_order':
975                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
976                     id = self.pool.get('stock.move').create(cr, uid, {
977                         'name': 'PROC:'+procurement.name,
978                         'location_id': source,
979                         'location_dest_id': procurement.location_id.id,
980                         'product_id': procurement.product_id.id,
981                         'product_qty':procurement.product_qty,
982                         'product_uom': procurement.product_uom.id,
983                         'date_planned': procurement.date_planned,
984                         'state':'confirmed',
985                     })
986                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
987                 else:
988                     # TODO: check this
989                     if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('waiting',):
990                         id = self.pool.get('stock.move').write(cr, uid, [procurement.move_id.id], {'state':'confirmed'})
991         self.write(cr, uid, ids, {'state':'confirmed','message':''})
992         return True
993
994     def action_move_assigned(self, cr, uid, ids):
995         self.write(cr, uid, ids, {'state':'running','message':_('from stock: products assigned.')})
996         return True
997
998     def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
999         return True
1000
1001     def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
1002         ok = True
1003         if procurement.move_id:
1004             id = procurement.move_id.id
1005             if not (procurement.move_id.state in ('done','assigned','cancel')):
1006                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
1007                 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%s', (procurement.product_id.id,))
1008                 if not cr.fetchone()[0]:
1009                     cr.execute('update mrp_procurement set message=%s where id=%s', (_('from stock and no minimum orderpoint rule defined'), procurement.id))
1010         return ok
1011
1012     def action_produce_assign_service(self, cr, uid, ids, context={}):
1013         for procurement in self.browse(cr, uid, ids):
1014             self.write(cr, uid, [procurement.id], {'state':'running'})
1015         return True
1016
1017     def action_produce_assign_product(self, cr, uid, ids, context={}):
1018         produce_id = False
1019         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1020         for procurement in self.browse(cr, uid, ids):
1021             res_id = procurement.move_id.id
1022             loc_id = procurement.location_id.id
1023             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)
1024             newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
1025             produce_id = self.pool.get('mrp.production').create(cr, uid, {
1026                 'origin': procurement.origin,
1027                 'product_id': procurement.product_id.id,
1028                 'product_qty': procurement.product_qty,
1029                 'product_uom': procurement.product_uom.id,
1030                 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
1031                 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
1032                 'location_src_id': procurement.location_id.id,
1033                 'location_dest_id': procurement.location_id.id,
1034                 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
1035                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1036                 'move_prod_id': res_id,
1037             })
1038             self.write(cr, uid, [procurement.id], {'state':'running'})
1039             bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
1040                     [produce_id], properties=[x.id for x in procurement.property_ids])
1041             wf_service = netsvc.LocalService("workflow")
1042             wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1043             self.pool.get('stock.move').write(cr, uid, [res_id],
1044                     {'location_id':procurement.location_id.id})
1045         return produce_id
1046
1047     def action_po_assign(self, cr, uid, ids, context={}):
1048         purchase_id = False
1049         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1050         for procurement in self.browse(cr, uid, ids):
1051             res_id = procurement.move_id.id
1052             partner = procurement.product_id.seller_ids[0].name
1053             partner_id = partner.id
1054             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1055             pricelist_id = partner.property_product_pricelist_purchase.id
1056
1057             uom_id = procurement.product_id.uom_po_id.id
1058
1059             qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1060             if procurement.product_id.seller_ids[0].qty:
1061                 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1062
1063             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1064
1065             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
1066             newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1067             newdate = newdate - procurement.product_id.seller_ids[0].delay
1068
1069             context.update({'lang':partner.lang})
1070             product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1071
1072             line = {
1073                 'name': product.name,
1074                 'product_qty': qty,
1075                 'product_id': procurement.product_id.id,
1076                 'product_uom': uom_id,
1077                 'price_unit': price,
1078                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1079                 'move_dest_id': res_id,
1080                 'notes':product.description_purchase,
1081             }
1082
1083             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1084             taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes_ids)
1085             line.update({
1086                 'taxes_id':[(6,0,taxes)]
1087             })
1088             purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1089                 'origin': procurement.origin,
1090                 'partner_id': partner_id,
1091                 'partner_address_id': address_id,
1092                 'location_id': procurement.location_id.id,
1093                 'pricelist_id': pricelist_id,
1094                 'order_line': [(0,0,line)],
1095                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1096             })
1097             self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1098         return purchase_id
1099
1100     def action_cancel(self, cr, uid, ids):
1101         todo = []
1102         todo2 = []
1103         for proc in self.browse(cr, uid, ids):
1104             if proc.close_move:
1105                 if proc.move_id.state not in ('done','cancel'):
1106                     todo2.append(proc.move_id.id)
1107             else:
1108                 if proc.move_id and proc.move_id.state=='waiting':
1109                     todo.append(proc.move_id.id)
1110         if len(todo2):
1111             self.pool.get('stock.move').action_cancel(cr, uid, todo2)
1112         if len(todo):
1113             self.pool.get('stock.move').write(cr, uid, todo, {'state':'assigned'})
1114         self.write(cr, uid, ids, {'state':'cancel'})
1115         wf_service = netsvc.LocalService("workflow")
1116         for id in ids:
1117             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1118         return True
1119
1120     def action_check_finnished(self, cr, uid, ids):
1121         return self.check_move_done(cr, uid, ids)
1122
1123     def action_check(self, cr, uid, ids):
1124         ok = False
1125         for procurement in self.browse(cr, uid, ids):
1126             if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1127                 self.action_done(cr, uid, [procurement.id])
1128                 ok = True
1129         return ok
1130
1131     def action_ready(self, cr, uid, ids):
1132         res = self.write(cr, uid, ids, {'state':'ready'})
1133         return res
1134
1135     def action_done(self, cr, uid, ids):
1136         for procurement in self.browse(cr, uid, ids):
1137             if procurement.move_id:
1138                 if procurement.close_move and (procurement.move_id.state <> 'done'):
1139                     self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1140         res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1141         wf_service = netsvc.LocalService("workflow")
1142         for id in ids:
1143             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1144         return res
1145
1146     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1147         '''
1148         use_new_cursor: False or the dbname
1149         '''
1150         if not context:
1151             context={}
1152         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1153         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1154                 use_new_cursor=use_new_cursor, context=context)
1155 mrp_procurement()
1156
1157
1158 class stock_warehouse_orderpoint(osv.osv):
1159     _name = "stock.warehouse.orderpoint"
1160     _description = "Orderpoint minimum rule"
1161     _columns = {
1162         'name': fields.char('Name', size=32, required=True),
1163         'active': fields.boolean('Active'),
1164         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1165         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1166         'location_id': fields.many2one('stock.location', 'Location', required=True),
1167         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1168         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1169         'product_min_qty': fields.float('Min Quantity', required=True,
1170             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1171             "a procurement to bring the virtual stock to the Max Quantity."),
1172         'product_max_qty': fields.float('Max Quantity', required=True,
1173             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1174             "a procurement to bring the virtual stock to the Max Quantity."),
1175         'qty_multiple': fields.integer('Qty Multiple', required=True,
1176             help="The procurement quantity will by rounded up to this multiple."),
1177         'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order')
1178     }
1179     _defaults = {
1180         'active': lambda *a: 1,
1181         'logic': lambda *a: 'max',
1182         'qty_multiple': lambda *a: 1,
1183         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1184         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1185     }
1186     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1187         if warehouse_id:
1188             w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1189             v = {'location_id':w.lot_stock_id.id}
1190             return {'value': v}
1191         return {}
1192     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1193         if product_id:
1194             prod=self.pool.get('product.product').browse(cr,uid,product_id)
1195             v = {'product_uom':prod.uom_id.id}
1196             return {'value': v}
1197         return {}
1198     def copy(self, cr, uid, id, default=None,context={}):
1199         if not default:
1200             default = {}
1201         default.update({
1202             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.warehouse.orderpoint') or '',
1203         })
1204         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
1205 stock_warehouse_orderpoint()
1206
1207
1208 class StockMove(osv.osv):
1209     _inherit = 'stock.move'
1210     _columns = {
1211         'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1212     }
1213     def copy(self, cr, uid, id, default=None, context=None):
1214         default = default or {}
1215         default['procurements'] = []
1216         return super(StockMove, self).copy(cr, uid, id, default, context)
1217
1218     def _action_explode(self, cr, uid, move, context={}):
1219         if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1220             bis = self.pool.get('mrp.bom').search(cr, uid, [
1221                 ('product_id','=',move.product_id.id),
1222                 ('bom_id','=',False),
1223                 ('type','=','phantom')])
1224             if bis:
1225                 factor = move.product_qty
1226                 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1227                 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1228                 dest = move.product_id.product_tmpl_id.property_stock_production.id
1229                 state = 'confirmed'
1230                 if move.state=='assigned':
1231                     state='assigned'
1232                 for line in res[0]:
1233                     valdef = {
1234                         'picking_id': move.picking_id.id,
1235                         'product_id': line['product_id'],
1236                         'product_uom': line['product_uom'],
1237                         'product_qty': line['product_qty'],
1238                         'product_uos': line['product_uos'],
1239                         'product_uos_qty': line['product_uos_qty'],
1240                         'move_dest_id': move.id,
1241                         'state': state,
1242                         'name': line['name'],
1243                         'location_dest_id': dest,
1244                         'move_history_ids': [(6,0,[move.id])],
1245                         'move_history_ids2': [(6,0,[])],
1246                         'procurements': []
1247                     }
1248                     mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1249                     prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1250                     proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1251                         'name': (move.picking_id.origin or ''),
1252                         'origin': (move.picking_id.origin or ''),
1253                         'date_planned': move.date_planned,
1254                         'product_id': line['product_id'],
1255                         'product_qty': line['product_qty'],
1256                         'product_uom': line['product_uom'],
1257                         'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1258                         'product_uos':  line['product_uos'],
1259                         'location_id': move.location_id.id,
1260                         'procure_method': prodobj.procure_method,
1261                         'move_id': mid,
1262                     })
1263                     wf_service = netsvc.LocalService("workflow")
1264                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1265                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1266                     'location_id': move.location_dest_id.id,
1267                     'auto_validate': True,
1268                     'picking_id': False,
1269                     'location_id': dest,
1270                     'state': 'waiting'
1271                 })
1272                 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1273                     wf_service = netsvc.LocalService("workflow")
1274                     wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1275         return True
1276 StockMove()
1277
1278
1279 class StockPicking(osv.osv):
1280     _inherit = 'stock.picking'
1281
1282     def test_finnished(self, cursor, user, ids):
1283         wf_service = netsvc.LocalService("workflow")
1284         res = super(StockPicking, self).test_finnished(cursor, user, ids)
1285         for picking in self.browse(cursor, user, ids):
1286             for move in picking.move_lines:
1287                 if move.state == 'done' and move.procurements:
1288                     for procurement in move.procurements:
1289                         wf_service.trg_validate(user, 'mrp.procurement',
1290                                 procurement.id, 'button_check', cursor)
1291         return res
1292
1293     #
1294     # Explode picking by replacing phantom BoMs
1295     #
1296     def action_explode(self, cr, uid, picks, *args):
1297         for move in self.pool.get('stock.move').browse(cr, uid, picks):
1298             self.pool.get('stock.move')._action_explode(cr, uid, move)
1299         return picks
1300
1301 StockPicking()
1302
1303 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1304