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