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