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