merge
[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, ondelete='cascade'),
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             self.pool.get('stock.move').write(cr, uid, [res_id],
1035                     {'location_id':procurement.location_id.id})
1036         return produce_id
1037
1038     def action_po_assign(self, cr, uid, ids, context={}):
1039         purchase_id = False
1040         company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
1041         for procurement in self.browse(cr, uid, ids):
1042             res_id = procurement.move_id.id
1043             partner = procurement.product_id.seller_ids[0].name
1044             partner_id = partner.id
1045             address_id = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['delivery'])['delivery']
1046             pricelist_id = partner.property_product_pricelist_purchase.id
1047
1048             uom_id = procurement.product_id.uom_po_id.id
1049
1050             qty = self.pool.get('product.uom')._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
1051             if procurement.product_id.seller_ids[0].qty:
1052                 qty=max(qty,procurement.product_id.seller_ids[0].qty)
1053
1054             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, False, {'uom': uom_id})[pricelist_id]
1055
1056             newdate = DateTime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S')
1057             newdate = newdate - DateTime.RelativeDateTime(days=company.po_lead)
1058             newdate = newdate - procurement.product_id.seller_ids[0].delay
1059
1060             context.update({'lang':partner.lang})
1061             product=self.pool.get('product.product').browse(cr,uid,procurement.product_id.id,context=context)
1062
1063             line = {
1064                 'name': product.name,
1065                 'product_qty': qty,
1066                 'product_id': procurement.product_id.id,
1067                 'product_uom': uom_id,
1068                 'price_unit': price,
1069                 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'),
1070                 'move_dest_id': res_id,
1071                 'notes':product.description_purchase,
1072             }
1073
1074             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
1075             taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes_ids)
1076             line.update({
1077                 'taxes_id':[(6,0,taxes)]
1078             })
1079             purchase_id = self.pool.get('purchase.order').create(cr, uid, {
1080                 'origin': procurement.origin,
1081                 'partner_id': partner_id,
1082                 'partner_address_id': address_id,
1083                 'location_id': procurement.location_id.id,
1084                 'pricelist_id': pricelist_id,
1085                 'order_line': [(0,0,line)],
1086                 'fiscal_position': partner.property_account_position and partner.property_account_position.id or False
1087             })
1088             self.write(cr, uid, [procurement.id], {'state':'running', 'purchase_id':purchase_id})
1089         return purchase_id
1090
1091     def action_cancel(self, cr, uid, ids):
1092         todo = []
1093         todo2 = []
1094         for proc in self.browse(cr, uid, ids):
1095             if proc.close_move:
1096                 if proc.move_id.state not in ('done','cancel'):
1097                     todo2.append(proc.move_id.id)
1098             else:
1099                 if proc.move_id and proc.move_id.state=='waiting':
1100                     todo.append(proc.move_id.id)
1101         if len(todo2):
1102             self.pool.get('stock.move').action_cancel(cr, uid, todo2)
1103         if len(todo):
1104             self.pool.get('stock.move').write(cr, uid, todo, {'state':'assigned'})
1105         self.write(cr, uid, ids, {'state':'cancel'})
1106         wf_service = netsvc.LocalService("workflow")
1107         for id in ids:
1108             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1109         return True
1110
1111     def action_check_finnished(self, cr, uid, ids):
1112         return self.check_move_done(cr, uid, ids)
1113
1114     def action_check(self, cr, uid, ids):
1115         ok = False
1116         for procurement in self.browse(cr, uid, ids):
1117             if procurement.move_id.state=='assigned' or procurement.move_id.state=='done':
1118                 self.action_done(cr, uid, [procurement.id])
1119                 ok = True
1120         return ok
1121
1122     def action_ready(self, cr, uid, ids):
1123         res = self.write(cr, uid, ids, {'state':'ready'})
1124         return res
1125
1126     def action_done(self, cr, uid, ids):
1127         for procurement in self.browse(cr, uid, ids):
1128             if procurement.move_id:
1129                 if procurement.close_move and (procurement.move_id.state <> 'done'):
1130                     self.pool.get('stock.move').action_done(cr, uid, [procurement.move_id.id])
1131         res = self.write(cr, uid, ids, {'state':'done', 'date_close':time.strftime('%Y-%m-%d')})
1132         wf_service = netsvc.LocalService("workflow")
1133         for id in ids:
1134             wf_service.trg_trigger(uid, 'mrp.procurement', id, cr)
1135         return res
1136
1137     def run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
1138         '''
1139         use_new_cursor: False or the dbname
1140         '''
1141         if not context:
1142             context={}
1143         self._procure_confirm(cr, uid, use_new_cursor=use_new_cursor, context=context)
1144         self._procure_orderpoint_confirm(cr, uid, automatic=automatic,\
1145                 use_new_cursor=use_new_cursor, context=context)
1146 mrp_procurement()
1147
1148
1149 class stock_warehouse_orderpoint(osv.osv):
1150     _name = "stock.warehouse.orderpoint"
1151     _description = "Orderpoint minimum rule"
1152     _columns = {
1153         'name': fields.char('Name', size=32, required=True),
1154         'active': fields.boolean('Active'),
1155         'logic': fields.selection([('max','Order to Max'),('price','Best price (not yet active!)')], 'Reordering Mode', required=True),
1156         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
1157         'location_id': fields.many2one('stock.location', 'Location', required=True),
1158         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type','=','product')]),
1159         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1160         'product_min_qty': fields.float('Min Quantity', required=True,
1161             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1162             "a procurement to bring the virtual stock to the Max Quantity."),
1163         'product_max_qty': fields.float('Max Quantity', required=True,
1164             help="When the virtual stock goes belong the Min Quantity, Open ERP generates "\
1165             "a procurement to bring the virtual stock to the Max Quantity."),
1166         'qty_multiple': fields.integer('Qty Multiple', required=True,
1167             help="The procurement quantity will by rounded up to this multiple."),
1168         'procurement_id': fields.many2one('mrp.procurement', 'Purchase Order')
1169     }
1170     _defaults = {
1171         'active': lambda *a: 1,
1172         'logic': lambda *a: 'max',
1173         'qty_multiple': lambda *a: 1,
1174         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'mrp.warehouse.orderpoint') or '',
1175         'product_uom': lambda sel, cr, uid, context: context.get('product_uom', False),
1176     }
1177     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context={}):
1178         if warehouse_id:
1179             w=self.pool.get('stock.warehouse').browse(cr,uid,warehouse_id, context)
1180             v = {'location_id':w.lot_stock_id.id}
1181             return {'value': v}
1182         return {}
1183     def onchange_product_id(self, cr, uid, ids, product_id, context={}):
1184         if product_id:
1185             prod=self.pool.get('product.product').browse(cr,uid,product_id)
1186             v = {'product_uom':prod.uom_id.id}
1187             return {'value': v}
1188         return {}
1189     def copy(self, cr, uid, id, default=None,context={}):
1190         if not default:
1191             default = {}
1192         default.update({
1193             'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.warehouse.orderpoint') or '',
1194         })
1195         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context)
1196 stock_warehouse_orderpoint()
1197
1198
1199 class StockMove(osv.osv):
1200     _inherit = 'stock.move'
1201     _columns = {
1202         'procurements': fields.one2many('mrp.procurement', 'move_id', 'Procurements'),
1203     }
1204     def copy(self, cr, uid, id, default=None, context=None):
1205         default = default or {}
1206         default['procurements'] = []
1207         return super(StockMove, self).copy(cr, uid, id, default, context)
1208
1209     def _action_explode(self, cr, uid, move, context={}):
1210         if move.product_id.supply_method=='produce' and move.product_id.procure_method=='make_to_order':
1211             bis = self.pool.get('mrp.bom').search(cr, uid, [
1212                 ('product_id','=',move.product_id.id),
1213                 ('bom_id','=',False),
1214                 ('type','=','phantom')])
1215             if bis:
1216                 factor = move.product_qty
1217                 bom_point = self.pool.get('mrp.bom').browse(cr, uid, bis[0])
1218                 res = self.pool.get('mrp.bom')._bom_explode(cr, uid, bom_point, factor, [])
1219                 dest = move.product_id.product_tmpl_id.property_stock_production.id
1220                 state = 'confirmed'
1221                 if move.state=='assigned':
1222                     state='assigned'
1223                 for line in res[0]:
1224                     valdef = {
1225                         'picking_id': move.picking_id.id,
1226                         'product_id': line['product_id'],
1227                         'product_uom': line['product_uom'],
1228                         'product_qty': line['product_qty'],
1229                         'product_uos': line['product_uos'],
1230                         'product_uos_qty': line['product_uos_qty'],
1231                         'move_dest_id': move.id,
1232                         'state': state,
1233                         'name': line['name'],
1234                         'location_dest_id': dest,
1235                         'move_history_ids': [(6,0,[move.id])],
1236                         'move_history_ids2': [(6,0,[])],
1237                         'procurements': []
1238                     }
1239                     mid = self.pool.get('stock.move').copy(cr, uid, move.id, default=valdef)
1240                     prodobj = self.pool.get('product.product').browse(cr, uid, line['product_id'], context=context)
1241                     proc_id = self.pool.get('mrp.procurement').create(cr, uid, {
1242                         'name': (move.picking_id.origin or ''),
1243                         'origin': (move.picking_id.origin or ''),
1244                         'date_planned': move.date_planned,
1245                         'product_id': line['product_id'],
1246                         'product_qty': line['product_qty'],
1247                         'product_uom': line['product_uom'],
1248                         'product_uos_qty': line['product_uos'] and line['product_uos_qty'] or False,
1249                         'product_uos':  line['product_uos'],
1250                         'location_id': move.location_id.id,
1251                         'procure_method': prodobj.procure_method,
1252                         'move_id': mid,
1253                     })
1254                     wf_service = netsvc.LocalService("workflow")
1255                     wf_service.trg_validate(uid, 'mrp.procurement', proc_id, 'button_confirm', cr)
1256                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1257                     'location_id': move.location_dest_id.id,
1258                     'auto_validate': True,
1259                     'picking_id': False,
1260                     'location_id': dest,
1261                     'state': 'waiting'
1262                 })
1263                 for m in self.pool.get('mrp.procurement').search(cr, uid, [('move_id','=',move.id)], context):
1264                     wf_service = netsvc.LocalService("workflow")
1265                     wf_service.trg_validate(uid, 'mrp.procurement', m, 'button_wait_done', cr)
1266         return True
1267 StockMove()
1268
1269
1270 class StockPicking(osv.osv):
1271     _inherit = 'stock.picking'
1272
1273     def test_finnished(self, cursor, user, ids):
1274         wf_service = netsvc.LocalService("workflow")
1275         res = super(StockPicking, self).test_finnished(cursor, user, ids)
1276         for picking in self.browse(cursor, user, ids):
1277             for move in picking.move_lines:
1278                 if move.state == 'done' and move.procurements:
1279                     for procurement in move.procurements:
1280                         wf_service.trg_validate(user, 'mrp.procurement',
1281                                 procurement.id, 'button_check', cursor)
1282         return res
1283
1284     #
1285     # Explode picking by replacing phantom BoMs
1286     #
1287     def action_explode(self, cr, uid, picks, *args):
1288         for move in self.pool.get('stock.move').browse(cr, uid, picks):
1289             self.pool.get('stock.move')._action_explode(cr, uid, move)
1290         return picks
1291
1292 StockPicking()
1293
1294 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1295