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