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