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