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