Changes according to addons resolving conflicts
[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 unlink(self, cr, uid, ids):
793         procurements = self.read(cr, uid, ids, ['state'])
794         unlink_ids = []
795         for s in procurements:
796             if s['state'] in ['draft','cancel']:
797                 unlink_ids.append(s['id'])
798             else:
799                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Procurement Order(s) which are in %s State!' % s['state']))
800         osv.osv.unlink(self, cr, uid, unlink_ids)
801         return True
802     
803     def check_product(self, cr, uid, ids):
804         for procurement in self.browse(cr, uid, ids):
805             if procurement.product_id.type in ('product', 'consu'):
806                 return True
807         return False
808
809     def check_move_cancel(self, cr, uid, ids, context={}):
810         res = True
811         for procurement in self.browse(cr, uid, ids, context):
812             if procurement.move_id:
813                 if not procurement.move_id.state=='cancel':
814                     res = False
815         return res
816
817     def check_move_done(self, cr, uid, ids, context={}):
818         res = True
819         for proc in self.browse(cr, uid, ids, context):
820             if proc.move_id:
821                 if not proc.move_id.state=='done':
822                     res = False
823         return res
824
825     #
826     # This method may be overrided by objects that override mrp.procurment
827     # for computing their own purpose
828     #
829     def _quantity_compute_get(self, cr, uid, proc, context={}):
830         if proc.product_id.type=='product':
831             return proc.move_id.product_uos_qty
832         return False
833
834     def _uom_compute_get(self, cr, uid, proc, context={}):
835         if proc.product_id.type=='product':
836             if proc.move_id.product_uos:
837                 return proc.move_id.product_uos.id
838         return False
839
840     #
841     # Return the quantity of product shipped/produced/served, wich may be
842     # different from the planned quantity
843     #
844     def quantity_get(self, cr, uid, id, context={}):
845         proc = self.browse(cr, uid, id, context)
846         result = self._quantity_compute_get(cr, uid, proc, context)
847         if not result:
848             result = proc.product_qty
849         return result
850
851     def uom_get(self, cr, uid, id, context=None):
852         proc = self.browse(cr, uid, id, context)
853         result = self._uom_compute_get(cr, uid, proc, context)
854         if not result:
855             result = proc.product_uom.id
856         return result
857
858     def check_waiting(self, cr, uid, ids, context=[]):
859         for procurement in self.browse(cr, uid, ids, context=context):
860             if procurement.move_id and procurement.move_id.state=='auto':
861                 return True
862         return False
863
864     def check_produce_service(self, cr, uid, procurement, context=[]):
865         return True
866
867     def check_produce_product(self, cr, uid, procurement, context=[]):
868         properties = [x.id for x in procurement.property_ids]
869         bom_id = self.pool.get('mrp.bom')._bom_find(cr, uid, procurement.product_id.id, procurement.product_uom.id, properties)
870         if not bom_id:
871             cr.execute('update mrp_procurement set message=%s where id=%d', ('No BoM defined for this product !', procurement.id))
872             return False
873         return True
874
875     def check_make_to_stock(self, cr, uid, ids, context={}):
876         ok = True
877         for procurement in self.browse(cr, uid, ids, context=context):
878             if procurement.product_id.type=='service':
879                 ok = ok and self._check_make_to_stock_service(cr, uid, procurement, context)
880             else:
881                 ok = ok and self._check_make_to_stock_product(cr, uid, procurement, context)
882         return ok
883
884     def check_produce(self, cr, uid, ids, context={}):
885         res = True
886         user = self.pool.get('res.users').browse(cr, uid, uid)
887         for procurement in self.browse(cr, uid, ids):
888             if procurement.product_id.product_tmpl_id.supply_method=='buy':
889                 if procurement.product_id.seller_ids:
890                     partner = procurement.product_id.seller_ids[0].name
891                     if user.company_id and user.company_id.partner_id:
892                         if partner.id == user.company_id.partner_id.id:
893                             return True
894                 return False
895             if procurement.product_id.product_tmpl_id.type=='service':
896                 res = res and self.check_produce_service(cr, uid, procurement, context)
897             else:
898                 res = res and self.check_produce_product(cr, uid, procurement, context)
899             if not res:
900                 return False
901         return res
902
903     def check_buy(self, cr, uid, ids):
904         user = self.pool.get('res.users').browse(cr, uid, uid)
905         for procurement in self.browse(cr, uid, ids):
906             if procurement.product_id.product_tmpl_id.supply_method=='produce':
907                 return False
908             if not procurement.product_id.seller_ids:
909                 cr.execute('update mrp_procurement set message=%s where id=%d', ('No supplier defined for this product !', procurement.id))
910                 return False
911             partner = procurement.product_id.seller_ids[0].name
912             if user.company_id and user.company_id.partner_id:
913                 if partner.id == user.company_id.partner_id.id:
914                     return False
915             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner.id], ['delivery'])['delivery']
916             if not address_id:
917                 cr.execute('update mrp_procurement set message=%s where id=%d', ('No address defined for the supplier', procurement.id))
918                 return False
919         return True
920
921     def test_cancel(self, cr, uid, ids):
922         for record in self.browse(cr, uid, ids):
923             if record.move_id and record.move_id.state=='cancel':
924                 return True
925         return False
926
927     def action_confirm(self, cr, uid, ids, context={}):
928         for procurement in self.browse(cr, uid, ids):
929             if procurement.product_id.type in ('product', 'consu'):
930                 if not procurement.move_id:
931                     source = procurement.location_id.id
932                     if procurement.procure_method=='make_to_order':
933                         source = procurement.product_id.product_tmpl_id.property_stock_procurement.id
934                     id = self.pool.get('stock.move').create(cr, uid, {
935                         'name': 'PROC:'+procurement.name,
936                         'location_id': source,
937                         'location_dest_id': procurement.location_id.id,
938                         'product_id': procurement.product_id.id,
939                         'product_qty':procurement.product_qty,
940                         'product_uom': procurement.product_uom.id,
941                         'date_planned': procurement.date_planned,
942                         'state':'confirmed',
943                     })
944                     self.write(cr, uid, [procurement.id], {'move_id': id, 'close_move':1})
945                 else:
946                     # TODO: check this
947                     if procurement.procure_method=='make_to_stock' and procurement.move_id.state in ('waiting',):
948                         id = self.pool.get('stock.move').write(cr, uid, [procurement.move_id.id], {'state':'confirmed'})
949         self.write(cr, uid, ids, {'state':'confirmed','message':''})
950         return True
951
952     def action_move_assigned(self, cr, uid, ids):
953         self.write(cr, uid, ids, {'state':'running','message':'from stock: products assigned.'})
954         return True
955
956     def _check_make_to_stock_service(self, cr, uid, procurement, context={}):
957         return True
958
959     def _check_make_to_stock_product(self, cr, uid, procurement, context={}):
960         ok = True
961         if procurement.move_id:
962             id = procurement.move_id.id
963             if not (procurement.move_id.state in ('done','assigned','cancel')):
964                 ok = ok and self.pool.get('stock.move').action_assign(cr, uid, [id])
965                 cr.execute('select count(id) from stock_warehouse_orderpoint where product_id=%d', (procurement.product_id.id,))
966                 if not cr.fetchone()[0]:
967                     cr.execute('update mrp_procurement set message=%s where id=%d', ('from stock and no minimum orderpoint rule defined', procurement.id))
968         return ok
969
970     def action_produce_assign_service(self, cr, uid, ids, context={}):
971         for procurement in self.browse(cr, uid, ids):
972             self.write(cr, uid, [procurement.id], {'state':'running'})
973         return True
974
975     def action_produce_assign_product(self, cr, uid, ids, context={}):
976         produce_id = False
977         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
978         for procurement in self.browse(cr, uid, ids):
979             res_id = procurement.move_id.id
980             loc_id = procurement.location_id.id
981             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)
982             newdate = newdate - DateTime.RelativeDateTime(days=company.manufacturing_lead)
983             produce_id = self.pool.get('mrp.production').create(cr, uid, {
984                 'origin': procurement.origin,
985                 'product_id': procurement.product_id.id,
986                 'product_qty': procurement.product_qty,
987                 'product_uom': procurement.product_uom.id,
988                 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False,
989                 'product_uos': procurement.product_uos and procurement.product_uos.id or False,
990                 'location_src_id': procurement.location_id.id,
991                 'location_dest_id': procurement.location_id.id,
992                 'bom_id': procurement.bom_id and procurement.bom_id.id or False,
993                 'date_planned': newdate,
994                 'move_prod_id': res_id,
995             })
996             self.write(cr, uid, [procurement.id], {'state':'running'})
997             bom_result = self.pool.get('mrp.production').action_compute(cr, uid,
998                     [produce_id], properties=[x.id for x in procurement.property_ids])
999             wf_service = netsvc.LocalService("workflow")
1000             wf_service.trg_validate(uid, 'mrp.production', produce_id, 'button_confirm', cr)
1001         return produce_id
1002
1003     def action_po_assign(self, cr, uid, ids, context={}):
1004         purchase_id = False
1005         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1006         for procurement in self.browse(cr, uid, ids):
1007             res_id = procurement.move_id.id
1008             partner = procurement.product_id.seller_ids[0].name
1009             partner_id = partner.id
1010             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1011             pricelist_id = partner.property_product_pricelist_purchase.id
1012
1013             uom_id = procurement.product_id.uom_po_id.id
1014
1015             qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1016             if procurement.product_id.seller_ids[0].qty:
1017                 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1018
1019             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1020
1021             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)
1022             newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1023             context.update({'lang':partner.lang})
1024             product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1025
1026             line = {
1027                 'name': product.name,
1028                 'product_qty': qty,
1029                 'product_id': procurement.product_id.id,
1030                 'product_uom': uom_id,
1031                 'price_unit': price,
1032                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1033                 'move_dest_id': res_id,
1034                 'notes':product.description_purchase,
1035             }
1036
1037             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1038             self.pool.get('account.fiscal.position').map_tax(cr, uid, partner, taxes_ids)
1039             line.update({
1040                 'taxes_id':[(6,0,taxes_ids)]
1041             })
1042             purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1043                 'origin': procurement.origin,
1044                 'partner_id': partner_id,
1045                 'partner_address_id': address_id,
1046                 'location_id': procurement.location_id.id,
1047                 'pricelist_id': pricelist_id,
1048                 'order_line': [(0,0,line)]
1049             })
1050             self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1051         return purchase_id
1052
1053     def action_cancel(self, cr, uid, ids):
1054         todo = []
1055         for proc in self.browse(cr, uid, ids):
1056             if proc.move_id:
1057                 todo.append(proc.move_id.id)
1058         if len(todo):
1059             self.pool.get('stock.move').action_cancel(cr, uid, [proc.move_id.id])
1060         self.write(cr, uid, ids, {'state':'cancel'})
1061
1062         wf_service = netsvc.LocalService("workflow")
1063         for id in ids:
1064             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1065
1066         return True
1067
1068     def action_check_finnished(self, cr, uid, ids):
1069         return True
1070
1071     def action_check(self, cr, uid, ids):
1072         ok = False
1073         for procurement in self.browse(cr, uid, ids):
1074             if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1075                 self.action_done(cr, uid, [procurement.id])
1076                 ok = True
1077         return ok
1078
1079     def action_done(self, cr, uid, ids):
1080         for procurement in self.browse(cr, uid, ids):
1081             if procurement.move_id:
1082                 if procurement.close_move and (procurement.move_id.state <> 'done'):
1083                     self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1084         res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1085
1086         wf_service = netsvc.LocalService("workflow")
1087         for id in ids:
1088             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1089         return res
1090     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1091         '''
1092         use_new_cursor: False or the dbname
1093         '''
1094         if not context:
1095             context={}
1096         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1097         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1098                 use_new_cursor=use_new_cursor, context=context)
1099 mrp_procurement()
1100
1101
1102 class stock_warehouse_orderpoint(osv.osv):
1103     _name = "stock.warehouse.orderpoint"
1104     _description = "Orderpoint minimum rule"
1105     _columns = {
1106         'name': fields.char('Name', size=32, required=True),
1107         'active': fields.boolean('Active'),
1108         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1109         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1110         'location_id': fields.many2one('stock.location', 'Location', required=True),
1111         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1112         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1113         'product_min_qty': fields.float('Min Quantity', required=True,
1114             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1115             "a procurement to bring the virtual stock to the Max Quantity."),
1116         'product_max_qty': fields.float('Max Quantity', required=True,
1117             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1118             "a procurement to bring the virtual stock to the Max Quantity."),
1119         'qty_multiple': fields.integer('Qty Multiple', required=True,
1120             help="The procurement quantity will by rounded up to this multiple."),
1121         'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order')
1122     }
1123     _defaults = {
1124         'active': lambda *a: 1,
1125         'logic': lambda *a: 'max',
1126         'qty_multiple': lambda *a: 1,
1127         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1128         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1129     }
1130     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1131         if warehouse_id:
1132             w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1133             v = {'location_id':w.lot_stock_id.id}
1134             return {'value': v}
1135         return {}
1136     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1137         if product_id:
1138             prod=self.pool.get('product.product').browse(cr,uid,product_id)
1139             v = {'product_uom':prod.uom_id.id}
1140             return {'value': v}
1141         return {}
1142 stock_warehouse_orderpoint()
1143
1144
1145 class StockMove(osv.osv):
1146     _inherit = 'stock.move'
1147     _columns = {
1148         'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1149     }
1150     def copy(self, cr, uid, id, default=None, context=None):
1151         default = default or {}
1152         default['procurements'] = []
1153         return super(StockMove, self).copy(cr, uid, id, default, context)
1154
1155     def _action_explode(self, cr, uid, move, context={}):
1156         if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1157             bis = self.pool.get('mrp.bom').search(cr, uid, [
1158                 ('product_id','=',move.product_id.id),
1159                 ('bom_id','=',False),
1160                 ('type','=','phantom')])
1161             if bis:
1162                 factor = move.product_qty
1163                 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1164                 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1165                 dest = move.product_id.product_tmpl_id.property_stock_production.id
1166                 state = 'confirmed'
1167                 if move.state=='assigned':
1168                     state='assigned'
1169                 for line in res[0]:
1170                     valdef = {
1171                         'picking_id': move.picking_id.id,
1172                         'product_id': line['product_id'],
1173                         'product_uom': line['product_uom'],
1174                         'product_qty': line['product_qty'],
1175                         'product_uos': line['product_uos'],
1176                         'product_uos_qty': line['product_uos_qty'],
1177                         'move_dest_id': move.id,
1178                         'state': state,
1179                         'location_dest_id': dest,
1180                         'move_history_ids': [(6,0,[move.id])],
1181                         'move_history_ids2': [(6,0,[])],
1182                         'procurements': []
1183                     }
1184                     mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1185                     prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1186                     proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1187                         'name': (move.picking_id.origin or ''),
1188                         'origin': (move.picking_id.origin or ''),
1189                         'date_planned': move.date_planned,
1190                         'product_id': line['product_id'],
1191                         'product_qty': line['product_qty'],
1192                         'product_uom': line['product_uom'],
1193                         'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1194                         'product_uos':  line['product_uos'],
1195                         'location_id': move.location_id.id,
1196                         'procure_method': prodobj.procure_method,
1197                         'move_id': mid,
1198                     })
1199                     wf_service = netsvc.LocalService("workflow")
1200                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1201                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1202                     'location_id': move.location_dest_id.id,
1203                     'auto_validate': True,
1204                     'picking_id': False,
1205                     'location_id': dest,
1206                     'state': 'waiting'
1207                 })
1208                 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1209                     wf_service = netsvc.LocalService("workflow")
1210                     wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1211         return True
1212 StockMove()
1213
1214
1215 class StockPicking(osv.osv):
1216     _inherit = 'stock.picking'
1217
1218     def test_finnished(self, cursor, user, ids):
1219         wf_service = netsvc.LocalService("workflow")
1220         res = super(StockPicking, self).test_finnished(cursor, user, ids)
1221         for picking in self.browse(cursor, user, ids):
1222             for move in picking.move_lines:
1223                 if move.state == 'done' and move.procurements:
1224                     for procurement in move.procurements:
1225                         wf_service.trg_validate(user, 'mrp.procurement',
1226                                 procurement.id, 'button_check', cursor)
1227         return res
1228
1229     #
1230     # Explode picking by replacing phantom BoMs
1231     #
1232     def action_explode(self, cr, uid, picks, *args):
1233         for move in picks:
1234             self.pool.get('stock.move')._action_explode(cr, uid, move)
1235         return picks
1236
1237 StockPicking()
1238
1239 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1240