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