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