cahnges in profiling
[odoo/odoo.git] / addons / stock / stock.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 #
6 # $Id$
7 #
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
13 # Service Company
14 #
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 #
29 ##############################################################################
30
31 from mx import DateTime
32 import time
33 import netsvc
34 from osv import fields,osv
35 import ir
36 from tools import config
37 from tools.translate import _
38 import tools
39 from xml.dom import minidom
40
41 #----------------------------------------------------------
42 # Incoterms
43 #----------------------------------------------------------
44 class stock_incoterms(osv.osv):
45     _name = "stock.incoterms"
46     _description = "Incoterms"
47     _columns = {
48         'name': fields.char('Name', size=64, required=True),
49         'code': fields.char('Code', size=3, required=True),
50         'active': fields.boolean('Active'),
51     }
52     _defaults = {
53         'active': lambda *a: True,
54     }
55 stock_incoterms()
56
57 #----------------------------------------------------------
58 # Stock Location
59 #----------------------------------------------------------
60 class stock_location(osv.osv):
61     _name = "stock.location"
62     _description = "Location"
63     _parent_name = "location_id"
64     _parent_store = True
65     _parent_order = 'name'
66     _order = 'parent_left'
67
68     def _complete_name(self, cr, uid, ids, name, args, context):
69         def _get_one_full_name(location, level=4):
70             if location.location_id:
71                 parent_path = _get_one_full_name(location.location_id, level-1) + "/"
72             else:
73                 parent_path = ''
74             return parent_path + location.name
75         res = {}
76         for m in self.browse(cr, uid, ids, context=context):
77             res[m.id] = _get_one_full_name(m)
78         return res
79
80     def _product_qty_available(self, cr, uid, ids, field_names, arg, context={}):
81         res = {}
82         for id in ids:
83             res[id] = {}.fromkeys(field_names, 0.0)
84         if ('product_id' not in context) or not ids:
85             return res
86         #location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
87         for loc in ids:
88             context['location'] = [loc]
89             prod = self.pool.get('product.product').browse(cr, uid, context['product_id'], context)
90             if 'stock_real' in field_names:
91                 res[loc]['stock_real'] = prod.qty_available
92             if 'stock_virtual' in field_names:
93                 res[loc]['stock_virtual'] = prod.virtual_available
94         return res
95
96     _columns = {
97         'name': fields.char('Location Name', size=64, required=True, translate=True),
98         'active': fields.boolean('Active'),
99         'usage': fields.selection([('supplier','Supplier Location'),('view','View'),('internal','Internal Location'),('customer','Customer Location'),('inventory','Inventory'),('procurement','Procurement'),('production','Production')], 'Location type', required=True),
100         'allocation_method': fields.selection([('fifo','FIFO'),('lifo','LIFO'),('nearest','Nearest')], 'Allocation Method', required=True),
101
102         'complete_name': fields.function(_complete_name, method=True, type='char', size=100, string="Location Name"),
103
104         'stock_real': fields.function(_product_qty_available, method=True, type='float', string='Real Stock', multi="stock"),
105         'stock_virtual': fields.function(_product_qty_available, method=True, type='float', string='Virtual Stock', multi="stock"),
106
107         'account_id': fields.many2one('account.account', string='Inventory Account', domain=[('type','!=','view')]),
108         'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
109         'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
110
111         'chained_location_id': fields.many2one('stock.location', 'Chained Location If Fixed'),
112         'chained_location_type': fields.selection([('','None'),('customer', 'Customer'),('fixed','Fixed Location')],
113             'Chained Location Type', required=True),
114         'chained_auto_packing': fields.selection(
115             [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')], 
116             'Automatic Move', 
117             required=True,
118             help="This is used only if you selected a chained location type.\n" \
119                 "The 'Automatic Move' value will create a stock move after the current one that will be "\
120                 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
121                 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
122             ),
123         'chained_delay': fields.integer('Chained Delay (days)'),
124         'address_id': fields.many2one('res.partner.address', 'Location Address'),
125         'icon': fields.selection(tools.icons, 'Icon', size=64),
126
127         'comment': fields.text('Additional Information'),
128         'posx': fields.integer('Corridor (X)'),
129         'posy': fields.integer('Shelves (Y)'),
130         'posz': fields.integer('Height (Z)'),
131
132         'parent_left': fields.integer('Left Parent', select=1),
133         'parent_right': fields.integer('Right Parent', select=1),
134     }
135     _defaults = {
136         'active': lambda *a: 1,
137         'usage': lambda *a: 'internal',
138         'allocation_method': lambda *a: 'fifo',
139         'chained_location_type': lambda *a: '',
140         'chained_auto_packing': lambda *a: 'manual',
141         'posx': lambda *a: 0,
142         'posy': lambda *a: 0,
143         'posz': lambda *a: 0,
144         'icon': lambda *a: False
145     }
146
147     def chained_location_get(self, cr, uid, location, partner=None, product=None, context={}):
148         result = None
149         if location.chained_location_type=='customer':
150             if partner:
151                 result = partner.property_stock_customer
152         elif location.chained_location_type=='fixed':
153             result = location.chained_location_id
154         if result:
155             return result, location.chained_auto_packing, location.chained_delay
156         return result
157
158     def picking_type_get(self, cr, uid, from_location, to_location, context={}):
159         result = 'internal'
160         if (from_location.usage=='internal') and (to_location and to_location.usage in ('customer','supplier')):
161             result = 'delivery'
162         elif (from_location.usage in ('supplier','customer')) and (to_location.usage=='internal'):
163             result = 'in'
164         return result
165
166     def _product_get_all_report(self, cr, uid, ids, product_ids=False,
167             context=None):
168         return self._product_get_report(cr, uid, ids, product_ids, context,
169                 recursive=True)
170
171     def _product_get_report(self, cr, uid, ids, product_ids=False,
172             context=None, recursive=False):
173         if context is None:
174             context = {}
175         product_obj = self.pool.get('product.product')
176         if not product_ids:
177             product_ids = product_obj.search(cr, uid, [])
178
179         products = product_obj.browse(cr, uid, product_ids, context=context)
180         products_by_uom = {}
181         products_by_id = {}
182         for product in products:
183             products_by_uom.setdefault(product.uom_id.id, [])
184             products_by_uom[product.uom_id.id].append(product)
185             products_by_id.setdefault(product.id, [])
186             products_by_id[product.id] = product
187
188         result = []
189         for id in ids:
190             for uom_id in products_by_uom.keys():
191                 fnc = self._product_get
192                 if recursive:
193                     fnc = self._product_all_get
194                 ctx = context.copy()
195                 ctx['uom'] = uom_id
196                 qty = fnc(cr, uid, id, [x.id for x in products_by_uom[uom_id]],
197                         context=ctx)
198                 for product_id in qty.keys():
199                     if not qty[product_id]:
200                         continue
201                     product = products_by_id[product_id]
202                     result.append({
203                         'price': product.standard_price,
204                         'name': product.name,
205                         'code': product.default_code, # used by lot_overview_all report!
206                         'variants': product.variants or '',
207                         'uom': product.uom_id.name,
208                         'amount': qty[product_id],
209                     })
210         return result
211
212     def _product_get_multi_location(self, cr, uid, ids, product_ids=False, context={}, states=['done'], what=('in', 'out')):                
213         product_obj = self.pool.get('product.product')        
214         context.update({
215             'states':states,
216             'what':what,
217             'location':ids
218         })                
219         return product_obj.get_product_available(cr,uid,product_ids,context=context)
220
221     def _product_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
222         ids = id and [id] or []
223         return self._product_get_multi_location(cr, uid, ids, product_ids, context, states)
224
225     def _product_all_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
226         # build the list of ids of children of the location given by id
227         ids = id and [id] or []
228         location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
229         return self._product_get_multi_location(cr, uid, location_ids, product_ids, context, states)
230
231     def _product_virtual_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
232         return self._product_all_get(cr, uid, id, product_ids, context, ['confirmed','waiting','assigned','done'])
233
234     #
235     # TODO:
236     #    Improve this function
237     #
238     # Returns:
239     #    [ (tracking_id, product_qty, location_id) ]
240     #
241     def _product_reserve(self, cr, uid, ids, product_id, product_qty, context={}):
242         result = []
243         amount = 0.0
244         for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
245             cr.execute("select product_uom,sum(product_qty) as product_qty from stock_move where location_dest_id=%d and product_id=%d and state='done' group by product_uom", (id,product_id))
246             results = cr.dictfetchall()
247             cr.execute("select product_uom,-sum(product_qty) as product_qty from stock_move where location_id=%d and product_id=%d and state in ('done', 'assigned') group by product_uom", (id,product_id))
248             results += cr.dictfetchall()
249
250             total = 0.0
251             results2 = 0.0
252             for r in results:
253                 amount = self.pool.get('product.uom')._compute_qty(cr, uid, r['product_uom'],r['product_qty'], context.get('uom',False))
254                 results2 += amount
255                 total += amount
256
257             if total<=0.0:
258                 continue
259
260             amount = results2
261             if amount>0:
262                 if amount>min(total,product_qty):
263                     amount = min(product_qty,total)
264                 result.append((amount,id))
265                 product_qty -= amount
266                 total -= amount
267                 if product_qty<=0.0:
268                     return result
269                 if total<=0.0:
270                     continue
271         return False
272 stock_location()
273
274 class stock_tracking(osv.osv):
275     _name = "stock.tracking"
276     _description = "Stock Tracking Lots"
277
278     def checksum(sscc):
279         salt = '31' * 8 + '3'
280         sum = 0
281         for sscc_part, salt_part in zip(sscc, salt):
282             sum += int(sscc_part) * int(salt_part)
283         return (10 - (sum % 10)) % 10
284     checksum = staticmethod(checksum)
285
286     def make_sscc(self, cr, uid, context={}):
287         sequence = self.pool.get('ir.sequence').get(cr, uid, 'stock.lot.tracking')
288         return sequence + str(self.checksum(sequence))
289
290     _columns = {
291         'name': fields.char('Tracking', size=64, required=True),
292         'active': fields.boolean('Active'),
293         'serial': fields.char('Reference', size=64),
294         'move_ids' : fields.one2many('stock.move', 'tracking_id', 'Moves tracked'),
295         'date': fields.datetime('Date create', required=True),
296     }
297     _defaults = {
298         'active': lambda *a: 1,
299         'name' : make_sscc,
300         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
301     }
302
303     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
304         if not args:
305             args=[]
306         if not context:
307             context={}
308         ids = self.search(cr, user, [('serial','=',name)]+ args, limit=limit, context=context)
309         ids += self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
310         return self.name_get(cr, user, ids, context)
311
312     def name_get(self, cr, uid, ids, context={}):
313         if not len(ids):
314             return []
315         res = [(r['id'], r['name']+' ['+(r['serial'] or '')+']') for r in self.read(cr, uid, ids, ['name','serial'], context)]
316         return res
317
318     def unlink(self, cr ,uid, ids):
319         raise Exception, _('You can not remove a lot line !')
320 stock_tracking()
321
322 #----------------------------------------------------------
323 # Stock Picking
324 #----------------------------------------------------------
325 class stock_picking(osv.osv):
326     _name = "stock.picking"
327     _description = "Packing list"
328     def _set_maximum_date(self, cr, uid, ids, name, value, arg, context):
329         if not value: return False
330         if isinstance(ids, (int, long)):
331             ids=[ids]
332         for pick in self.browse(cr, uid, ids, context):
333             sql_str="""update stock_move set
334                     date_planned='%s'
335                 where
336                     picking_id=%d """ % (value,pick.id)
337             
338             if pick.max_date:
339                 sql_str += " and (date_planned='"+pick.max_date+"' or date_planned>'"+value+"')"
340             cr.execute(sql_str)
341         return True
342
343     def _set_minimum_date(self, cr, uid, ids, name, value, arg, context):
344         if not value: return False
345         if isinstance(ids, (int, long)):
346             ids=[ids]
347         for pick in self.browse(cr, uid, ids, context):
348             sql_str="""update stock_move set
349                     date_planned='%s' 
350                 where
351                     picking_id=%d """ % (value,pick.id)
352             if pick.min_date:
353                 sql_str += " and (date_planned='"+pick.min_date+"' or date_planned<'"+value+"')"
354             cr.execute(sql_str)
355         return True
356
357     def get_min_max_date(self, cr, uid, ids, field_name, arg, context={}):
358         res = {}
359         for id in ids:
360             res[id] = {'min_date':False, 'max_date': False}
361         if not ids:
362             return res
363         cr.execute("""select
364                 picking_id,
365                 min(date_planned),
366                 max(date_planned)
367             from
368                 stock_move 
369             where
370                 picking_id in (""" + ','.join(map(str, ids)) + """)
371             group by
372                 picking_id""")
373         for pick, dt1,dt2 in cr.fetchall():
374             res[pick]['min_date'] = dt1
375             res[pick]['max_date'] = dt2
376         return res
377
378     _columns = {
379         'name': fields.char('Reference', size=64, required=True, select=True),
380         'origin': fields.char('Origin Reference', size=64),
381         'backorder_id': fields.many2one('stock.picking', 'Back Order'),
382         'type': fields.selection([('out','Sending Goods'),('in','Getting Goods'),('internal','Internal'),('delivery','Delivery')], 'Shipping Type', required=True, select=True),
383         'active': fields.boolean('Active'),
384         'note': fields.text('Notes'),
385
386         'location_id': fields.many2one('stock.location', 'Location'),
387         'location_dest_id': fields.many2one('stock.location', 'Dest. Location'),
388         'move_type': fields.selection([('direct','Direct Delivery'),('one','All at once')],'Delivery Method', required=True),
389         'state': fields.selection([
390             ('draft','Draft'),
391             ('auto','Waiting'),
392             ('confirmed','Confirmed'),
393             ('assigned','Assigned'),
394             ('done','Done'),
395             ('cancel','Cancel'),
396             ], 'Status', readonly=True, select=True),
397         'min_date': fields.function(get_min_max_date, fnct_inv=_set_minimum_date, multi="min_max_date",
398                  method=True,store=True, type='datetime', string='Planned Date', select=1),
399         'date':fields.datetime('Date Order'),
400         'date_done':fields.datetime('Date Done'),
401         'max_date': fields.function(get_min_max_date, fnct_inv=_set_maximum_date, multi="min_max_date",
402                  method=True,store=True, type='datetime', string='Max. Planned Date', select=2),
403         'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines'),
404
405         'auto_picking': fields.boolean('Auto-Packing'),
406         'address_id': fields.many2one('res.partner.address', 'Partner'),
407         'invoice_state':fields.selection([
408             ("invoiced","Invoiced"),
409             ("2binvoiced","To be invoiced"),
410             ("none","Not from Packing")], "Invoice Status", 
411             select=True, required=True, readonly=True, states={'draft':[('readonly',False)]}),
412     }
413     _defaults = {
414         'name': lambda self,cr,uid,context: self.pool.get('ir.sequence').get(cr, uid, 'stock.picking'),
415         'active': lambda *a: 1,
416         'state': lambda *a: 'draft',
417         'move_type': lambda *a: 'direct',
418         'type': lambda *a: 'in',
419         'invoice_state': lambda *a: 'none',
420         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
421     }
422     #def copy(self, cr, uid, id, data=None, context={}):
423     #    data = data or {}
424     #    return super(stock_picking, self).copy(cr, uid, id, data, context)
425
426     def onchange_partner_in(self, cr, uid, context, partner_id=None):
427         sid = self.pool.get('res.partner.address').browse(cr, uid, partner_id, context).partner_id.property_stock_supplier.id
428         return { }
429
430     def action_explode(self, cr, uid, moves, context={}):
431         return moves
432
433     def action_confirm(self, cr, uid, ids, context={}):
434         self.write(cr, uid, ids, {'state': 'confirmed'})
435         todo = []
436         for picking in self.browse(cr, uid, ids):
437             for r in picking.move_lines:
438                 if r.state=='draft':
439                     todo.append(r)
440         todo = self.action_explode(cr, uid, todo, context)
441         if len(todo):
442             self.pool.get('stock.move').action_confirm(cr, uid, todo, context)
443         return True
444
445     def test_auto_picking(self, cr, uid, ids):
446         # TODO: Check locations to see if in the same location ?
447         return True
448
449     def button_confirm(self, cr, uid, ids, *args):
450         for id in ids:
451             wf_service = netsvc.LocalService("workflow")
452             wf_service.trg_validate(uid, 'stock.picking', id, 'button_confirm', cr)
453         self.force_assign(cr, uid, ids, *args)
454         return True
455
456     def action_assign(self, cr, uid, ids, *args):
457         for pick in self.browse(cr, uid, ids):
458             move_ids = [x.id for x in pick.move_lines if x.state=='confirmed']
459             self.pool.get('stock.move').action_assign(cr, uid, move_ids)
460         return True
461
462     def force_assign(self, cr, uid, ids, *args):
463         wf_service = netsvc.LocalService("workflow")
464         for pick in self.browse(cr, uid, ids):
465 #           move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
466             move_ids = [x.id for x in pick.move_lines]
467             self.pool.get('stock.move').force_assign(cr, uid, move_ids)
468             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
469         return True
470     
471     def draft_force_assign(self, cr, uid, ids, *args):
472         wf_service = netsvc.LocalService("workflow")
473         for pick in self.browse(cr, uid, ids):
474             wf_service.trg_validate(uid, 'stock.picking', pick.id,
475                 'button_confirm', cr)
476             move_ids = [x.id for x in pick.move_lines]
477             self.pool.get('stock.move').force_assign(cr, uid, move_ids)
478             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
479         return True
480     
481     def draft_validate(self, cr, uid, ids, *args):
482         wf_service = netsvc.LocalService("workflow")
483         self.draft_force_assign(cr, uid, ids)
484         for pick in self.browse(cr, uid, ids):
485             self.action_move(cr, uid, [pick.id])
486             wf_service.trg_validate(uid, 'stock.picking', pick.id , 'button_done', cr)
487         return True
488     
489     def cancel_assign(self, cr, uid, ids, *args):
490         wf_service = netsvc.LocalService("workflow")
491         for pick in self.browse(cr, uid, ids):
492             move_ids = [x.id for x in pick.move_lines]
493             self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
494             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
495         return True
496
497     def action_assign_wkf(self, cr, uid, ids):
498         self.write(cr, uid, ids, {'state':'assigned'})
499         return True
500
501     def test_finnished(self, cr, uid, ids):
502         move_ids=self.pool.get('stock.move').search(cr,uid,[('picking_id','in',ids)])
503         
504         for move in self.pool.get('stock.move').browse(cr,uid,move_ids):
505             if move.state not in ('done','cancel') :
506                 if move.product_qty != 0.0:
507                     return False
508                 else:
509                     move.write(cr,uid,[move.id],{'state':'done'})
510         return True
511
512     def test_assigned(self, cr, uid, ids):
513         ok = True
514         for pick in self.browse(cr, uid, ids):
515             mt = pick.move_type
516             for move in pick.move_lines:
517                 if (move.state in ('confirmed','draft')) and (mt=='one'):
518                     return False
519                 if (mt=='direct') and (move.state=='assigned') and (move.product_qty):
520                     return True
521                 ok = ok and (move.state in ('cancel','done','assigned'))
522         return ok
523
524     def action_cancel(self, cr, uid, ids, context={}):
525         for pick in self.browse(cr, uid, ids):
526             ids2 = [move.id for move in pick.move_lines]
527             self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
528         self.write(cr,uid, ids, {'state':'cancel', 'invoice_state':'none'})
529         return True
530
531     #
532     # TODO: change and create a move if not parents
533     #
534     def action_done(self, cr, uid, ids, context=None):
535         self.write(cr,uid, ids, {'state':'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
536         return True
537
538     def action_move(self, cr, uid, ids, context={}):
539         for pick in self.browse(cr, uid, ids):
540             todo = []
541             for move in pick.move_lines:
542                 if move.state=='assigned':
543                     todo.append(move.id)
544
545             if len(todo):
546                 self.pool.get('stock.move').action_done(cr, uid, todo,
547                         context=context)
548         return True
549
550     def _get_address_invoice(self, cursor, user, picking):
551         '''Return {'contact': address, 'invoice': address} for invoice'''
552         partner_obj = self.pool.get('res.partner')
553         partner = picking.address_id.partner_id
554
555         return partner_obj.address_get(cursor, user, [partner.id],
556                 ['contact', 'invoice'])
557
558     def _get_comment_invoice(self, cursor, user, picking):
559         '''Return comment string for invoice'''
560         return picking.note or ''
561
562     def _get_price_unit_invoice(self, cursor, user, move_line, type):
563         '''Return the price unit for the move line'''
564         if type in ('in_invoice', 'in_refund'):
565             return move_line.product_id.standard_price
566         else:
567             return move_line.product_id.list_price
568     
569     def _get_discount_invoice(self, cursor, user, move_line):
570         '''Return the discount for the move line'''
571         return 0.0
572
573     def _get_taxes_invoice(self, cursor, user, move_line, type):
574         '''Return taxes ids for the move line'''
575         if type in ('in_invoice', 'in_refund'):
576             taxes = move_line.product_id.supplier_taxes_id
577         else:
578             taxes = move_line.product_id.taxes_id
579
580         if move_line.picking_id and move_line.picking_id.address_id and move_line.picking_id.address_id.partner_id:
581             return self.pool.get('account.fiscal.position').map_tax(
582                 cursor,
583                 user,
584                 move_line.picking_id.address_id.partner_id,
585                 taxes
586             )
587         else:
588             return map(lambda x: x.id, taxes)
589
590     def _get_account_analytic_invoice(self, cursor, user, picking, move_line):
591         return False
592
593     def _invoice_line_hook(self, cursor, user, move_line, invoice_line_id):
594         '''Call after the creation of the invoice line'''
595         return
596
597     def _invoice_hook(self, cursor, user, picking, invoice_id):
598         '''Call after the creation of the invoice'''
599         return
600
601     def action_invoice_create(self, cursor, user, ids, journal_id=False,
602             group=False, type='out_invoice', context=None):
603         print "WW"*12,context
604         '''Return ids of created invoices for the pickings'''
605         invoice_obj = self.pool.get('account.invoice')
606         invoice_line_obj = self.pool.get('account.invoice.line')
607         invoices_group = {}
608         res = {}
609         sale_line_obj = self.pool.get('sale.order.line')
610         
611         for picking in self.browse(cursor, user, ids, context=context):            
612             if picking.invoice_state != '2binvoiced':
613                 continue
614             payment_term_id = False
615             partner = picking.address_id and picking.address_id.partner_id
616             if not partner:
617                 raise osv.except_osv(_('Error, no partner !'),
618                     _('Please put a partner on the picking list if you want to generate invoice.'))
619
620             if type in ('out_invoice', 'out_refund'):
621                 account_id = partner.property_account_receivable.id
622                 if picking.sale_id and picking.sale_id.payment_term:
623                     payment_term_id= picking.sale_id.payment_term.id
624             else:
625                 account_id = partner.property_account_payable.id
626
627             address_contact_id, address_invoice_id = \
628                     self._get_address_invoice(cursor, user, picking).values()
629
630             comment = self._get_comment_invoice(cursor, user, picking)
631
632             if group and partner.id in invoices_group:
633                 invoice_id = invoices_group[partner.id]
634             else:
635                 invoice_vals = {
636                     'name': picking.name,
637                     'origin': picking.name + (picking.origin and (':' + picking.origin) or ''),
638                     'type': type,
639                     'account_id': account_id,
640                     'partner_id': partner.id,
641                     'address_invoice_id': address_invoice_id,
642                     'address_contact_id': address_contact_id,
643                     'comment': comment,
644                     'payment_term': payment_term_id,
645                     }
646                 if journal_id:
647                     invoice_vals['journal_id'] = journal_id
648                 invoice_id = invoice_obj.create(cursor, user, invoice_vals,
649                         context=context)
650                 invoices_group[partner.id] = invoice_id
651             res[picking.id] = invoice_id
652             sale_line_ids = sale_line_obj.search(cursor, user, [('order_id','=',picking.sale_id.id)])
653             sale_lines = sale_line_obj.browse(cursor, user, sale_line_ids, context=context)
654             for sale_line in sale_lines:
655                 if sale_line.product_id.type == 'service' and sale_line.invoiced == False:
656                     if group:
657                         name = picking.name + '-' + sale_line.name
658                     else:
659                         name = sale_line.name
660                     if type in ('out_invoice', 'out_refund'):
661                         account_id = sale_line.product_id.product_tmpl_id.\
662                                 property_account_income.id
663                         if not account_id:
664                             account_id = sale_line.product_id.categ_id.\
665                                     property_account_income_categ.id
666                     else:
667                         account_id = sale_line.product_id.product_tmpl_id.\
668                                 property_account_expense.id
669                         if not account_id:
670                             account_id = sale_line.product_id.categ_id.\
671                                     property_account_expense_categ.id
672                     price_unit = self._get_price_unit_invoice(cursor, user,
673                             sale_line, type)
674                     discount = self._get_discount_invoice(cursor, user, sale_line)
675                     tax_ids = self._get_taxes_invoice(cursor, user, sale_line, type)
676
677                     account_analytic_id = self._get_account_analytic_invoice(cursor,
678                             user, picking, sale_line)
679
680                     account_id = self.pool.get('account.fiscal.position').map_account(cursor, user, partner, account_id)
681                     invoice_line_id = invoice_line_obj.create(cursor, user, {
682                         'name': name,
683                         'invoice_id': invoice_id,
684                         'uos_id': sale_line.product_uos.id or sale_line.product_uom.id,
685                         'product_id': sale_line.product_id.id,
686                         'account_id': account_id,
687                         'price_unit': price_unit,
688                         'discount': discount,
689                         'quantity': sale_line.product_uos_qty,
690                         'invoice_line_tax_id': [(6, 0, tax_ids)],
691                         'account_analytic_id': account_analytic_id,
692                         }, context=context)
693                     sale_line_obj.write(cursor, user, [sale_line.id], {'invoiced':True,
694                         'invoice_lines': [(6, 0, [invoice_line_id])],
695                         })
696
697             for move_line in picking.move_lines:
698                 if group:
699                     name = picking.name + '-' + move_line.name
700                 else:
701                     name = move_line.name
702
703                 if type in ('out_invoice', 'out_refund'):
704                     account_id = move_line.product_id.product_tmpl_id.\
705                             property_account_income.id
706                     if not account_id:
707                         account_id = move_line.product_id.categ_id.\
708                                 property_account_income_categ.id
709                 else:
710                     account_id = move_line.product_id.product_tmpl_id.\
711                             property_account_expense.id
712                     if not account_id:
713                         account_id = move_line.product_id.categ_id.\
714                                 property_account_expense_categ.id
715
716                 price_unit = self._get_price_unit_invoice(cursor, user,
717                         move_line, type)
718                 discount = self._get_discount_invoice(cursor, user, move_line)
719                 tax_ids = self._get_taxes_invoice(cursor, user, move_line, type)
720                 account_analytic_id = self._get_account_analytic_invoice(cursor,
721                         user, picking, move_line)
722
723                 account_id = self.pool.get('account.fiscal.position').map_account(cursor, user, partner, account_id)
724                 invoice_line_id = invoice_line_obj.create(cursor, user, {
725                     'name': name,
726                     'invoice_id': invoice_id,
727                     'uos_id': move_line.product_uos.id,
728                     'product_id': move_line.product_id.id,
729                     'account_id': account_id,
730                     'price_unit': price_unit,
731                     'discount': discount,
732                     'quantity': move_line.product_uos_qty or move_line.product_qty,
733                     'invoice_line_tax_id': [(6, 0, tax_ids)],
734                     'account_analytic_id': account_analytic_id,
735                     }, context=context)
736                 self._invoice_line_hook(cursor, user, move_line, invoice_line_id)
737
738             invoice_obj.button_compute(cursor, user, [invoice_id], context=context,
739                     set_total=(type in ('in_invoice', 'in_refund')))
740             self.write(cursor, user, [picking.id], {
741                 'invoice_state': 'invoiced',
742                 }, context=context)
743             self._invoice_hook(cursor, user, picking, invoice_id)
744         self.write(cursor, user, res.keys(), {
745             'invoice_state': 'invoiced',
746             }, context=context)
747         return res
748
749 stock_picking()
750
751
752 class stock_production_lot(osv.osv):
753
754     def name_get(self, cr, uid, ids, context={}):
755         if not ids:
756             return []
757         reads = self.read(cr, uid, ids, ['name', 'ref'], context)
758         res=[]
759         for record in reads:
760             name=record['name']
761             if record['ref']:
762                 name=name+'/'+record['ref']
763             res.append((record['id'], name))
764         return res
765     
766
767     _name = 'stock.production.lot'
768     _description = 'Production lot'
769
770     def _get_stock(self, cr, uid, ids, field_name, arg, context={}):
771         if 'location_id' not in context:
772             locations = self.pool.get('stock.location').search(cr, uid, [('usage','=','internal')], context=context)
773         else:
774             locations = [context['location_id']]
775         res = {}.fromkeys(ids, 0.0)
776         cr.execute('''select
777                 prodlot_id,
778                 sum(name)
779             from
780                 stock_report_prodlots
781             where
782                 location_id in ('''+','.join(map(str, locations))+''')  and
783                 prodlot_id in  ('''+','.join(map(str, ids))+''')
784             group by
785                 prodlot_id
786         ''')
787         res.update(dict(cr.fetchall()))
788         return res
789
790     _columns = {
791         'name': fields.char('Serial', size=64, required=True),
792         'ref': fields.char('Internal Ref.', size=64),
793         'product_id': fields.many2one('product.product','Product',required=True),
794         'date': fields.datetime('Created Date', required=True),
795         'stock_available': fields.function(_get_stock, method=True, type="float", string="Available", select="2"),
796         'revisions': fields.one2many('stock.production.lot.revision','lot_id','Revisions'),
797     }
798     _defaults = {
799         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
800         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'stock.lot.serial'),
801         'product_id': lambda x,y,z,c: c.get('product_id',False),
802     }
803     _sql_constraints = [
804         ('name_ref_uniq', 'unique (name, ref)', 'The serial/ref must be unique !'),
805     ]
806
807 stock_production_lot()
808
809 class stock_production_lot_revision(osv.osv):
810     _name = 'stock.production.lot.revision'
811     _description = 'Production lot revisions'
812     _columns = {
813         'name': fields.char('Revision name', size=64, required=True),
814         'description': fields.text('Description'),
815         'date': fields.date('Revision date'),
816         'indice': fields.char('Revision', size=16),
817         'author_id': fields.many2one('res.users', 'Author'),
818         'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True, ondelete='cascade'),
819     }
820
821     _defaults = {
822         'author_id': lambda x,y,z,c: z,
823         'date': lambda *a: time.strftime('%Y-%m-%d'),
824     }
825 stock_production_lot_revision()
826
827 # ----------------------------------------------------
828 # Move
829 # ----------------------------------------------------
830
831 #
832 # Fields:
833 #   location_dest_id is only used for predicting futur stocks
834 #
835 class stock_move(osv.osv):
836     def _getSSCC(self, cr, uid, context={}):
837         cr.execute('select id from stock_tracking where create_uid=%d order by id desc limit 1', (uid,))
838         res = cr.fetchone()
839         return (res and res[0]) or False
840     _name = "stock.move"
841     _description = "Stock Move"
842     def name_get(self, cr, uid, ids, context={}):
843         res = []
844         for line in self.browse(cr, uid, ids, context):
845             res.append((line.id, (line.product_id.code or '/')+': '+line.location_id.name+' > '+line.location_dest_id.name))
846         return res
847
848     def _check_tracking(self, cr, uid, ids):
849          for move in self.browse(cr, uid, ids):             
850              if not move.prodlot_id and \
851                 (move.state == 'done' and \
852                 ( \
853                     (move.product_id.track_production and move.location_id.usage=='production') or \
854                     (move.product_id.track_production and move.location_dest_id.usage=='production') or \
855                     (move.product_id.track_incoming and move.location_id.usage=='supplier') or \
856                     (move.product_id.track_outgoing and move.location_dest_id.usage=='customer') \
857                 )):
858                     return False
859          return True
860
861     def _check_product_lot(self, cr, uid, ids):
862          for move in self.browse(cr, uid, ids):
863              if move.prodlot_id and (move.prodlot_id.product_id.id != move.product_id.id):
864                 return False                          
865          return True
866          
867     _columns = {
868         'name': fields.char('Name', size=64, required=True, select=True),
869         'priority': fields.selection([('0','Not urgent'),('1','Urgent')], 'Priority'),
870
871         'date': fields.datetime('Date Created'),
872         'date_planned': fields.datetime('Scheduled date', required=True),
873
874         'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
875
876         'product_qty': fields.float('Quantity', required=True),
877         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
878         'product_uos_qty': fields.float('Quantity (UOS)'),
879         'product_uos': fields.many2one('product.uom', 'Product UOS'),
880         'product_packaging' : fields.many2one('product.packaging', 'Packaging'),
881
882         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
883         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
884         'address_id' : fields.many2one('res.partner.address', 'Dest. Address'),
885
886         'prodlot_id' : fields.many2one('stock.production.lot', 'Production lot', help="Production lot is used to put a serial number on the production"),
887         'tracking_id': fields.many2one('stock.tracking', 'Tracking lot', select=True, help="Tracking lot is the code that will be put on the logistic unit/pallet"),
888 #       'lot_id': fields.many2one('stock.lot', 'Consumer lot', select=True, readonly=True),
889
890         'auto_validate': fields.boolean('Auto Validate'),
891
892         'move_dest_id': fields.many2one('stock.move', 'Dest. Move'),
893         'move_history_ids': fields.many2many('stock.move', 'stock_move_history_ids', 'parent_id', 'child_id', 'Move History'),
894         'move_history_ids2': fields.many2many('stock.move', 'stock_move_history_ids', 'child_id', 'parent_id', 'Move History'),
895         'picking_id': fields.many2one('stock.picking', 'Packing list', select=True),
896
897         'note': fields.text('Notes'),
898
899         'state': fields.selection([('draft','Draft'),('waiting','Waiting'),('confirmed','Confirmed'),('assigned','Assigned'),('done','Done'),('cancel','cancel')], 'Status', readonly=True, select=True),
900         'price_unit': fields.float('Unit Price',
901             digits=(16, int(config['price_accuracy']))),
902     }
903     
904     _constraints = [
905         (_check_tracking,
906             'You must assign a production lot for this product',
907             ['prodlot_id']),
908         (_check_product_lot,
909             'You try to assign a lot which is not from the same product',
910             ['prodlot_id'])]
911     
912     def _default_location_destination(self, cr, uid, context={}):
913         if context.get('move_line', []):
914             return context['move_line'][0][2]['location_dest_id']
915         if context.get('address_out_id', False):
916             return self.pool.get('res.partner.address').browse(cr, uid, context['address_out_id'], context).partner_id.property_stock_customer.id
917         return False
918
919     def _default_location_source(self, cr, uid, context={}):
920         if context.get('move_line', []):
921             try:
922                 return context['move_line'][0][2]['location_id']
923             except:
924                 pass
925         if context.get('address_in_id', False):
926             return self.pool.get('res.partner.address').browse(cr, uid, context['address_in_id'], context).partner_id.property_stock_supplier.id
927         return False
928
929     _defaults = {
930         'location_id': _default_location_source,
931         'location_dest_id': _default_location_destination,
932         'state': lambda *a: 'draft',
933         'priority': lambda *a: '1',
934         'product_qty': lambda *a: 1.0,
935         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
936         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
937     }
938
939     def _auto_init(self, cursor, context):
940         super(stock_move, self)._auto_init(cursor, context)
941         cursor.execute('SELECT indexname \
942                 FROM pg_indexes \
943                 WHERE indexname = \'stock_move_location_id_location_dest_id_product_id_state\'')
944         if not cursor.fetchone():
945             cursor.execute('CREATE INDEX stock_move_location_id_location_dest_id_product_id_state \
946                     ON stock_move (location_id, location_dest_id, product_id, state)')
947             cursor.commit()
948
949     def onchange_lot_id(self, cr, uid, context, prodlot_id=False,product_qty=False, loc_id=False):
950         if not prodlot_id or not loc_id:
951             return {}
952         prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id)
953         location=self.pool.get('stock.location').browse(cr,uid,loc_id)
954         warning={}
955         if (location.usage == 'internal') and (product_qty > (prodlot.stock_available or 0.0)):
956             warning={
957                 'title':'Bad Lot Assignation !',
958                 'message':'You are moving %.2f products but only %.2f available in this lot.' % (product_qty,prodlot.stock_available or 0.0)
959             }
960         return {'warning':warning}
961
962     def onchange_product_id(self, cr, uid, context, prod_id=False, loc_id=False, loc_dest_id=False):
963         if not prod_id:
964             return {}
965         product = self.pool.get('product.product').browse(cr, uid, [prod_id])[0]
966         result = {
967             'name': product.name,
968             'product_uom': product.uom_id.id,
969         }
970         if loc_id:
971             result['location_id'] = loc_id
972         if loc_dest_id:
973             result['location_dest_id'] = loc_dest_id
974         return {'value':result}
975
976     def _chain_compute(self, cr, uid, moves, context={}):
977         result = {}
978         for m in moves:
979             dest = self.pool.get('stock.location').chained_location_get(
980                 cr, 
981                 uid, 
982                 m.location_dest_id, 
983                 m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id, 
984                 m.product_id, 
985                 context
986             )
987             if dest:
988                 if dest[1]=='transparent':
989                     self.write(cr, uid, [m.id], {
990                         'date_planned': (DateTime.strptime(m.date_planned, '%Y-%m-%d %H:%M:%S') + \
991                             DateTime.RelativeDateTime(days=dest[2] or 0)).strftime('%Y-%m-%d'),
992                         'location_dest_id': dest[0].id})
993                 else:
994                     result.setdefault(m.picking_id, [])
995                     result[m.picking_id].append( (m, dest) )
996         return result
997
998     def action_confirm(self, cr, uid, moves, context={}):
999         ids = map(lambda m: m.id, moves)
1000         self.write(cr, uid, ids, {'state':'confirmed'})
1001         for picking, todo in self._chain_compute(cr, uid, moves, context).items():
1002             ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
1003             pickid = self.pool.get('stock.picking').create(cr, uid, {
1004                 'name': picking.name,
1005                 'origin': str(picking.origin or ''),
1006                 'type': ptype,
1007                 'note': picking.note,
1008                 'move_type': picking.move_type,
1009                 'auto_picking': todo[0][1][1]=='auto',
1010                 'address_id': picking.address_id.id,
1011                 'invoice_state': 'none'
1012             })
1013             for move,(loc,auto,delay) in todo:
1014                 # Is it smart to copy ? May be it's better to recreate ?
1015                 new_id = self.pool.get('stock.move').copy(cr, uid, move.id, {
1016                     'location_id': move.location_dest_id.id,
1017                     'location_dest_id': loc.id,
1018                     'date_moved': time.strftime('%Y-%m-%d'),
1019                     'picking_id': pickid,
1020                     'state':'waiting',
1021                     'move_history_ids':[],
1022                     'date_planned': (DateTime.strptime(move.date_planned, '%Y-%m-%d %H:%M:%S') + DateTime.RelativeDateTime(days=delay or 0)).strftime('%Y-%m-%d'),
1023                     'move_history_ids2':[]}
1024                 )
1025                 self.pool.get('stock.move').write(cr, uid, [move.id], {
1026                     'move_dest_id': new_id,
1027                     'move_history_ids': [(4, new_id)]
1028                 })
1029             wf_service = netsvc.LocalService("workflow")
1030             wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
1031         return []
1032
1033     def action_assign(self, cr, uid, ids, *args):
1034         todo = []
1035         for move in self.browse(cr, uid, ids):
1036             if move.state in ('confirmed','waiting'):
1037                 todo.append(move.id)
1038         res = self.check_assign(cr, uid, todo)
1039         return res
1040
1041     def force_assign(self, cr, uid, ids, context={}):
1042         self.write(cr, uid, ids, {'state' : 'assigned'})
1043         return True
1044
1045     def cancel_assign(self, cr, uid, ids, context={}):
1046         self.write(cr, uid, ids, {'state': 'confirmed'})
1047         return True
1048
1049     #
1050     # Duplicate stock.move
1051     #
1052     def check_assign(self, cr, uid, ids, context={}):
1053         done = []
1054         count=0
1055         pickings = {}
1056         for move in self.browse(cr, uid, ids):
1057             if move.product_id.type == 'consu':
1058                 if move.state in ('confirmed', 'waiting'):
1059                     done.append(move.id)
1060                 pickings[move.picking_id.id] = 1
1061                 continue
1062             if move.state in ('confirmed','waiting'):
1063                 res = self.pool.get('stock.location')._product_reserve(cr, uid, [move.location_id.id], move.product_id.id, move.product_qty, {'uom': move.product_uom.id})
1064                 if res:
1065                     done.append(move.id)
1066                     pickings[move.picking_id.id] = 1
1067                     r = res.pop(0)
1068                     cr.execute('update stock_move set location_id=%d, product_qty=%f where id=%d', (r[1],r[0], move.id))
1069
1070                     while res:
1071                         r = res.pop(0)
1072                         move_id = self.copy(cr, uid, move.id, {'product_qty':r[0], 'location_id':r[1]})
1073                         done.append(move_id)
1074                         #cr.execute('insert into stock_move_history_ids values (%d,%d)', (move.id,move_id))
1075         if done:
1076             count += len(done)
1077             self.write(cr, uid, done, {'state':'assigned'})
1078
1079         if count:
1080             for pick_id in pickings:
1081                 wf_service = netsvc.LocalService("workflow")
1082                 wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
1083         return count
1084
1085     #
1086     # Cancel move => cancel others move and pickings
1087     #
1088     def action_cancel(self, cr, uid, ids, context={}):
1089         if not len(ids):
1090             return True
1091         pickings = {}
1092         for move in self.browse(cr, uid, ids):
1093             if move.state in ('confirmed','waiting','assigned','draft'):
1094                 if move.picking_id:
1095                     pickings[move.picking_id.id] = True
1096         self.write(cr, uid, ids, {'state':'cancel'})
1097
1098         for pick_id in pickings:
1099             wf_service = netsvc.LocalService("workflow")
1100             wf_service.trg_validate(uid, 'stock.picking', pick_id, 'button_cancel', cr)
1101         ids2 = []
1102         for res in self.read(cr, uid, ids, ['move_dest_id']):
1103             if res['move_dest_id']:
1104                 ids2.append(res['move_dest_id'][0])
1105
1106         wf_service = netsvc.LocalService("workflow")
1107         for id in ids:
1108             wf_service.trg_trigger(uid, 'stock.move', id, cr)
1109         self.action_cancel(cr,uid, ids2, context)
1110         return True
1111
1112     def action_done(self, cr, uid, ids, context=None):
1113         track_flag=False
1114         for move in self.browse(cr, uid, ids):
1115             if move.move_dest_id.id and (move.state != 'done'):
1116                 mid = move.move_dest_id.id
1117                 cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%d,%d)', (move.id, move.move_dest_id.id))
1118                 if move.move_dest_id.state in ('waiting','confirmed'):
1119                     self.write(cr, uid, [move.move_dest_id.id], {'state':'assigned'})
1120                     if move.move_dest_id.picking_id:
1121                         wf_service = netsvc.LocalService("workflow")
1122                         wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1123                     else:
1124                         pass
1125                         # self.action_done(cr, uid, [move.move_dest_id.id])
1126                     if move.move_dest_id.auto_validate:
1127                         self.action_done(cr, uid, [move.move_dest_id.id], context=context)
1128
1129             #
1130             # Accounting Entries
1131             #
1132             acc_src = None
1133             acc_dest = None
1134             if move.location_id.account_id:
1135                 acc_src =  move.location_id.account_id.id
1136             if move.location_dest_id.account_id:
1137                 acc_dest =  move.location_dest_id.account_id.id
1138             if acc_src or acc_dest:
1139                 test = [('product.product', move.product_id.id)]
1140                 if move.product_id.categ_id:
1141                     test.append( ('product.category', move.product_id.categ_id.id) )
1142                 if not acc_src:
1143                     acc_src = move.product_id.product_tmpl_id.\
1144                             property_stock_account_input.id
1145                     if not acc_src:
1146                         acc_src = move.product_id.categ_id.\
1147                                 property_stock_account_input_categ.id
1148                     if not acc_src:
1149                         raise osv.except_osv(_('Error!'),
1150                                 _('There is no stock input account defined ' \
1151                                         'for this product: "%s" (id: %d)') % \
1152                                         (move.product_id.name,
1153                                             move.product_id.id,))
1154                 if not acc_dest:
1155                     acc_dest = move.product_id.product_tmpl_id.\
1156                             property_stock_account_output.id
1157                     if not acc_dest:
1158                         acc_dest = move.product_id.categ_id.\
1159                                 property_stock_account_output_categ.id
1160                     if not acc_dest:
1161                         raise osv.except_osv(_('Error!'),
1162                                 _('There is no stock output account defined ' \
1163                                         'for this product: "%s" (id: %d)') % \
1164                                         (move.product_id.name,
1165                                             move.product_id.id,))
1166                 if not move.product_id.categ_id.property_stock_journal.id:
1167                     raise osv.except_osv(_('Error!'),
1168                         _('There is no journal defined '\
1169                             'on the product category: "%s" (id: %d)') % \
1170                             (move.product_id.categ_id.name,
1171                                 move.product_id.categ_id.id,))
1172                 journal_id = move.product_id.categ_id.property_stock_journal.id
1173                 if acc_src != acc_dest:
1174                     ref = move.picking_id and move.picking_id.name or False
1175
1176                     if move.product_id.cost_method == 'average' and move.price_unit:
1177                         amount = move.product_qty * move.price_unit
1178                     else:
1179                         amount = move.product_qty * move.product_id.standard_price
1180
1181                     date = time.strftime('%Y-%m-%d')
1182                     lines = [
1183                             (0, 0, {
1184                                 'name': move.name,
1185                                 'quantity': move.product_qty,
1186                                 'credit': amount,
1187                                 'account_id': acc_src,
1188                                 'ref': ref,
1189                                 'date': date}),
1190                             (0, 0, {
1191                                 'name': move.name,
1192                                 'quantity': move.product_qty,
1193                                 'debit': amount,
1194                                 'account_id': acc_dest,
1195                                 'ref': ref,
1196                                 'date': date})
1197                     ]
1198                     self.pool.get('account.move').create(cr, uid,
1199                             {
1200                                 'name': move.name,
1201                                 'journal_id': journal_id,
1202                                 'line_id': lines,
1203                                 'ref': ref,
1204                             })
1205             
1206         self.write(cr, uid, ids, {'state':'done'})
1207
1208         wf_service = netsvc.LocalService("workflow")
1209         for id in ids:
1210             wf_service.trg_trigger(uid, 'stock.move', id, cr)
1211         return True
1212
1213     def unlink(self, cr, uid, ids, context=None):
1214         for move in self.browse(cr, uid, ids, context=context):
1215             if move.state != 'draft':
1216                 raise osv.except_osv(_('UserError'),
1217                         _('You can only delete draft moves.'))
1218         return super(stock_move, self).unlink(
1219             cr, uid, ids, context=context)
1220
1221 stock_move()
1222
1223 class stock_inventory(osv.osv):
1224     _name = "stock.inventory"
1225     _description = "Inventory"
1226     _columns = {
1227         'name': fields.char('Inventory', size=64, required=True, readonly=True, states={'draft':[('readonly',False)]}),
1228         'date': fields.datetime('Date create', required=True, readonly=True, states={'draft':[('readonly',False)]}),
1229         'date_done': fields.datetime('Date done'),
1230         'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=True, states={'draft':[('readonly',False)]}),
1231         'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
1232         'state': fields.selection( (('draft','Draft'),('done','Done')), 'Status', readonly=True),
1233     }
1234     _defaults = {
1235         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1236         'state': lambda *a: 'draft',
1237     }
1238     #
1239     # Update to support tracking
1240     #
1241     def action_done(self, cr, uid, ids, context=None):
1242         for inv in self.browse(cr,uid,ids):
1243             move_ids = []
1244             move_line=[]
1245             for line in inv.inventory_line_id:
1246                 pid=line.product_id.id
1247                 price=line.product_id.standard_price or 0.0
1248                 amount=self.pool.get('stock.location')._product_get(cr, uid, line.location_id.id, [pid], {'uom': line.product_uom.id})[pid]
1249                 change=line.product_qty-amount
1250                 if change:
1251                     location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
1252                     value = {
1253                         'name': 'INV:'+str(line.inventory_id.id)+':'+line.inventory_id.name,
1254                         'product_id': line.product_id.id,
1255                         'product_uom': line.product_uom.id,
1256                         'date': inv.date,
1257                         'date_planned': inv.date,
1258                         'state': 'assigned'
1259                     }
1260                     if change>0:
1261                         value.update( {
1262                             'product_qty': change,
1263                             'location_id': location_id,
1264                             'location_dest_id': line.location_id.id,
1265                         })
1266                     else:
1267                         value.update( {
1268                             'product_qty': -change,
1269                             'location_id': line.location_id.id,
1270                             'location_dest_id': location_id,
1271                         })
1272                     move_ids.append(self.pool.get('stock.move').create(cr, uid, value))
1273             if len(move_ids):
1274                 self.pool.get('stock.move').action_done(cr, uid, move_ids,
1275                         context=context)
1276             self.write(cr, uid, [inv.id], {'state':'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S'), 'move_ids': [(6,0,move_ids)]})
1277         return True
1278
1279     def action_cancel(self, cr, uid, ids, context={}):
1280         for inv in self.browse(cr,uid,ids):
1281             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
1282             self.write(cr, uid, [inv.id], {'state':'draft'})
1283         return True
1284 stock_inventory()
1285
1286
1287 class stock_inventory_line(osv.osv):
1288     _name = "stock.inventory.line"
1289     _description = "Inventory line"
1290     _columns = {
1291         'inventory_id': fields.many2one('stock.inventory','Inventory', ondelete='cascade', select=True),
1292         'location_id': fields.many2one('stock.location','Location', required=True),
1293         'product_id': fields.many2one('product.product', 'Product', required=True ),
1294         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True ),
1295         'product_qty': fields.float('Quantity')
1296     }
1297     def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False):
1298         if not product:
1299             return {}
1300         if not uom:
1301             prod = self.pool.get('product.product').browse(cr, uid, [product], {'uom': uom})[0]
1302             uom = prod.uom_id.id
1303         amount=self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], {'uom': uom})[product]
1304         result = {'product_qty':amount, 'product_uom':uom}
1305         return {'value':result}
1306 stock_inventory_line()
1307
1308
1309 #----------------------------------------------------------
1310 # Stock Warehouse
1311 #----------------------------------------------------------
1312 class stock_warehouse(osv.osv):
1313     _name = "stock.warehouse"
1314     _description = "Warehouse"
1315     _columns = {
1316         'name': fields.char('Name', size=60, required=True),
1317 #       'partner_id': fields.many2one('res.partner', 'Owner'),
1318         'partner_address_id': fields.many2one('res.partner.address', 'Owner Address'),
1319         'lot_input_id': fields.many2one('stock.location', 'Location Input', required=True ),
1320         'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True ),
1321         'lot_output_id': fields.many2one('stock.location', 'Location Output', required=True ),
1322     }
1323 stock_warehouse()
1324
1325
1326 # Move wizard : 
1327 #    get confirm or assign stock move lines of partner and put in current picking.
1328 class stock_picking_move_wizard(osv.osv_memory):
1329     _name='stock.picking.move.wizard'
1330     def _get_picking(self,cr, uid, ctx):        
1331         if ctx.get('action_id',False):
1332             return ctx['action_id']
1333         return False     
1334     def _get_picking_address(self,cr,uid,ctx):        
1335         picking_obj=self.pool.get('stock.picking')        
1336         if ctx.get('action_id',False):
1337             picking=picking_obj.browse(cr,uid,[ctx['action_id']])[0]            
1338             return picking.address_id and picking.address_id.id or False        
1339         return False
1340             
1341             
1342     _columns={
1343         'name':fields.char('Name',size=64,invisible=True),
1344         #'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines',readonly=True),
1345         'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Move lines',required=True),
1346         'address_id' : fields.many2one('res.partner.address', 'Dest. Address',invisible=True),
1347         'picking_id': fields.many2one('stock.picking', 'Packing list', select=True,invisible=True),
1348     }
1349     _defaults={
1350         'picking_id':_get_picking,
1351         'address_id':_get_picking_address,
1352     }
1353     def action_move(self,cr,uid,ids,context=None):
1354         move_obj=self.pool.get('stock.move')
1355         picking_obj=self.pool.get('stock.picking')
1356         for act in self.read(cr,uid,ids):
1357             move_lines=move_obj.browse(cr,uid,act['move_ids'])
1358             for line in move_lines:
1359                  picking_obj.write(cr,uid,[line.picking_id.id],{'move_lines':[(1,line.id,{'picking_id':act['picking_id']})]})                 
1360                  picking_obj.write(cr,uid,[act['picking_id']],{'move_lines':[(1,line.id,{'picking_id':act['picking_id']})]})
1361                  cr.commit()
1362                  old_picking=picking_obj.read(cr,uid,[line.picking_id.id])[0]
1363                  if not len(old_picking['move_lines']):
1364                     picking_obj.write(cr,uid,[old_picking['id']],{'state':'done'})
1365         return {'type':'ir.actions.act_window_close' }
1366             
1367 stock_picking_move_wizard()        
1368 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: