improvement
[odoo/odoo.git] / addons / stock / stock.py
index 81d152c..412a4b8 100644 (file)
@@ -35,7 +35,8 @@ from osv import fields,osv
 import ir
 from tools import config
 from tools.translate import _
-
+import tools
+from xml.dom import minidom
 
 #----------------------------------------------------------
 # Incoterms
@@ -53,21 +54,6 @@ class stock_incoterms(osv.osv):
     }
 stock_incoterms()
 
-#class stock_lot(osv.osv):
-#   _name = "stock.lot"
-#   _description = "Lot"
-#   _columns = {
-#       'name': fields.char('Lot Name', size=64, required=True),
-#       'active': fields.boolean('Active'),
-#       'tracking': fields.char('Tracking', size=64),
-#       'move_ids': fields.one2many('stock.move', 'lot_id', 'Move lines'),
-#   }
-#   _defaults = {
-#       'active': lambda *a: True,
-#   }
-#stock_lot()
-
-
 #----------------------------------------------------------
 # Stock Location
 #----------------------------------------------------------
@@ -75,47 +61,100 @@ class stock_location(osv.osv):
     _name = "stock.location"
     _description = "Location"
     _parent_name = "location_id"
+    _parent_store = True
+    _parent_order = 'name'
+    _order = 'parent_left'
+
+    def _complete_name(self, cr, uid, ids, name, args, context):
+        def _get_one_full_name(location, level=4):
+            if location.location_id:
+                parent_path = _get_one_full_name(location.location_id, level-1) + "/"
+            else:
+                parent_path = ''
+            return parent_path + location.name
+        res = {}
+        for m in self.browse(cr, uid, ids, context=context):
+            res[m.id] = _get_one_full_name(m)
+        return res
+
+    def _product_qty_available(self, cr, uid, ids, field_names, arg, context={}):
+        res = {}
+        for id in ids:
+            res[id] = {}.fromkeys(field_names, 0.0)
+        if ('product_id' not in context) or not ids:
+            return res
+        #location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
+        for loc in ids:
+            context['location'] = [loc]
+            prod = self.pool.get('product.product').browse(cr, uid, context['product_id'], context)
+            if 'stock_real' in field_names:
+                res[loc]['stock_real'] = prod.qty_available
+            if 'stock_virtual' in field_names:
+                res[loc]['stock_virtual'] = prod.virtual_available
+        return res
+
     _columns = {
-        'name': fields.char('Location Name', size=64, required=True),
+        'name': fields.char('Location Name', size=64, required=True, translate=True),
         'active': fields.boolean('Active'),
-        'usage': fields.selection([('supplier','Supplier Location'),('view','View'),('internal','Internal Location'),('customer','Customer Location'),('inventory','Inventory'),('procurement','Procurement'),('production','Production')], 'Location type'),
+        'usage': fields.selection([('supplier','Supplier Location'),('view','View'),('internal','Internal Location'),('customer','Customer Location'),('inventory','Inventory'),('procurement','Procurement'),('production','Production')], 'Location type', required=True),
         'allocation_method': fields.selection([('fifo','FIFO'),('lifo','LIFO'),('nearest','Nearest')], 'Allocation Method', required=True),
 
+        'complete_name': fields.function(_complete_name, method=True, type='char', size=100, string="Location Name"),
+
+        'stock_real': fields.function(_product_qty_available, method=True, type='float', string='Real Stock', multi="stock"),
+        'stock_virtual': fields.function(_product_qty_available, method=True, type='float', string='Virtual Stock', multi="stock"),
+
         'account_id': fields.many2one('account.account', string='Inventory Account', domain=[('type','!=','view')]),
-        'location_id': fields.many2one('stock.location', 'Parent Location', select=True),
+        'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
         'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
 
         'chained_location_id': fields.many2one('stock.location', 'Chained Location If Fixed'),
-        'chained_location_type': fields.selection([('','None'),('customer', 'Customer'),('fixed','Fixed Location')], 'Chained Location Type'),
-        'chained_auto_packing': fields.boolean('Chained Auto-Packing'),
+        'chained_location_type': fields.selection([('','None'),('customer', 'Customer'),('fixed','Fixed Location')],
+            'Chained Location Type', required=True),
+        'chained_auto_packing': fields.selection(
+            [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')], 
+            'Automatic Move', 
+            required=True,
+            help="This is used only if you selected a chained location type.\n" \
+                "The 'Automatic Move' value will create a stock move after the current one that will be "\
+                "validated automatically. With 'Manual Operation', the stock move has to be validated "\
+                "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
+            ),
         'chained_delay': fields.integer('Chained Delay (days)'),
-
         'address_id': fields.many2one('res.partner.address', 'Location Address'),
+        'icon': fields.selection(tools.icons, 'Icon', size=64),
 
         'comment': fields.text('Additional Information'),
-        'posx': fields.integer('Corridor (X)', required=True),
-        'posy': fields.integer('Shelves (Y)', required=True),
-        'posz': fields.integer('Height (Z)', required=True),
+        'posx': fields.integer('Corridor (X)'),
+        'posy': fields.integer('Shelves (Y)'),
+        'posz': fields.integer('Height (Z)'),
+
+        'parent_left': fields.integer('Left Parent', select=1),
+        'parent_right': fields.integer('Right Parent', select=1),
     }
     _defaults = {
         'active': lambda *a: 1,
         'usage': lambda *a: 'internal',
         'allocation_method': lambda *a: 'fifo',
         'chained_location_type': lambda *a: '',
+        'chained_auto_packing': lambda *a: 'manual',
         'posx': lambda *a: 0,
         'posy': lambda *a: 0,
         'posz': lambda *a: 0,
+        'icon': lambda *a: False
     }
 
     def chained_location_get(self, cr, uid, location, partner=None, product=None, context={}):
         result = None
         if location.chained_location_type=='customer':
-            result = partner.property_stock_customer
+            if partner:
+                result = partner.property_stock_customer
         elif location.chained_location_type=='fixed':
             result = location.chained_location_id
+        if result:
+            return result, location.chained_auto_packing, location.chained_delay
         return result
 
-
     def picking_type_get(self, cr, uid, from_location, to_location, context={}):
         result = 'internal'
         if (from_location.usage=='internal') and (to_location and to_location.usage in ('customer','supplier')):
@@ -170,59 +209,14 @@ class stock_location(osv.osv):
                     })
         return result
 
-    def _product_get_multi_location(self, cr, uid, ids, product_ids=False, context={}, states=['done'], what=('in', 'out')):
-        product_obj = self.pool.get('product.product')
-        states_str = ','.join(map(lambda s: "'%s'" % s, states))
-        if not product_ids:
-            product_ids = product_obj.search(cr, uid, [])
-        res = {}
-        for id in product_ids:
-            res[id] = 0.0
-        if not ids:
-            return res
-
-        product2uom = {}
-        for product in product_obj.browse(cr, uid, product_ids, context=context):
-            product2uom[product.id] = product.uom_id.id
-
-        prod_ids_str = ','.join(map(str, product_ids))
-        location_ids_str = ','.join(map(str, ids))
-        results = []
-        results2 = []
-        if 'in' in what:
-            # all moves from a location out of the set to a location in the set
-            cr.execute(
-                'select sum(product_qty), product_id, product_uom '\
-                'from stock_move '\
-                'where location_id not in ('+location_ids_str+') '\
-                'and location_dest_id in ('+location_ids_str+') '\
-                'and product_id in ('+prod_ids_str+') '\
-                'and state in ('+states_str+') '\
-                'group by product_id,product_uom'
-            )
-            results = cr.fetchall()
-        if 'out' in what:
-            # all moves from a location in the set to a location out of the set
-            cr.execute(
-                'select sum(product_qty), product_id, product_uom '\
-                'from stock_move '\
-                'where location_id in ('+location_ids_str+') '\
-                'and location_dest_id not in ('+location_ids_str+') '\
-                'and product_id in ('+prod_ids_str+') '\
-                'and state in ('+states_str+') '\
-                'group by product_id,product_uom'
-            )
-            results2 = cr.fetchall()
-        uom_obj = self.pool.get('product.uom')
-        for amount, prod_id, prod_uom in results:
-            amount = uom_obj._compute_qty(cr, uid, prod_uom, amount,
-                    context.get('uom', False) or product2uom[prod_id])
-            res[prod_id] += amount
-        for amount, prod_id, prod_uom in results2:
-            amount = uom_obj._compute_qty(cr, uid, prod_uom, amount,
-                    context.get('uom', False) or product2uom[prod_id])
-            res[prod_id] -= amount
-        return res
+    def _product_get_multi_location(self, cr, uid, ids, product_ids=False, context={}, states=['done'], what=('in', 'out')):                
+        product_obj = self.pool.get('product.product')        
+        context.update({
+            'states':states,
+            'what':what,
+            'location':ids
+        })                
+        return product_obj.get_product_available(cr,uid,product_ids,context=context)
 
     def _product_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
         ids = id and [id] or []
@@ -277,49 +271,6 @@ class stock_location(osv.osv):
         return False
 stock_location()
 
-
-#----------------------------------------------------------
-# Stock Move
-#----------------------------------------------------------
-
-#class stock_move_lot(osv.osv):
-#   _name = "stock.move.lot"
-#   _description = "Move Lot"
-#   _columns = {
-#       'name': fields.char('Move Description', size=64, required=True),
-#       'active': fields.boolean('Active'),
-#       'state': fields.selection( (('draft','Draft'),('done','Moved')), 'State', readonly=True),
-#       'serial': fields.char('Tracking Number', size=32),
-#       'date_planned': fields.date('Scheduled date'),
-#       'date_moved': fields.date('Actual date'),
-#       'lot_id': fields.many2one('stock.lot','Lot', required=True),
-#       'loc_dest_id': fields.many2one('stock.location', 'Destination Location', required=True),
-#       'address_id': fields.many2one('res.partner.address', 'Destination Address'),
-#       'origin': fields.char('Origin', size=64),
-#   }
-#   _defaults = {
-#       'active': lambda *a: 1,
-#       'state': lambda *a: 'draft',
-#       'date_planned': lambda *a: time.strftime('%Y-%m-%d'),
-#   }
-#   #
-#   # TODO: test if valid
-#   # ERROR: does this function should call action_done instead of doing him self on
-#   # stock.move
-#   #
-#   def action_move(self, cr, uid, ids, context={}):
-#       for move in self.browse(cr, uid, ids, context):
-#           lot_remove = []
-#           for m in move.lot_id.move_ids:
-#               new_id = self.pool.get('stock.move').copy(cr, uid, m.id, {'location_id': m.location_dest_id.id, 'location_dest_id': move.loc_dest_id.id, 'date_moved': time.strftime('%Y-%m-%d'), 'picking_id': False, 'state':'draft','prodlot_id':False, 'tracking_id':False, 'lot_id': False, 'move_history_ids':[], 'move_history_ids2':[]})
-#               self.pool.get('stock.move').action_done(cr, uid, [new_id], context)
-#               cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%d,%d)', (m.id, new_id))
-#               lot_remove.append(m.id)
-#           self.pool.get('stock.move').write(cr, uid, lot_remove, {'lot_id':False})
-#       self.write(cr,uid, ids, {'state':'done','date_moved':time.strftime('%Y-%m-%d')})
-#       return True
-#stock_move_lot()
-
 class stock_tracking(osv.osv):
     _name = "stock.tracking"
     _description = "Stock Tracking Lots"
@@ -374,16 +325,66 @@ stock_tracking()
 class stock_picking(osv.osv):
     _name = "stock.picking"
     _description = "Packing list"
+    def _set_maximum_date(self, cr, uid, ids, name, value, arg, context):
+        if not value: return False
+        if isinstance(ids, (int, long)):
+            ids=[ids]
+        for pick in self.browse(cr, uid, ids, context):
+            sql_str="""update stock_move set
+                    date_planned='%s'
+                where
+                    picking_id=%d """ % (value,pick.id)
+            
+            if pick.max_date:
+                sql_str += " and (date_planned='"+pick.max_date+"' or date_planned>'"+value+"')"
+            cr.execute(sql_str)
+        return True
+
+    def _set_minimum_date(self, cr, uid, ids, name, value, arg, context):
+        if not value: return False
+        if isinstance(ids, (int, long)):
+            ids=[ids]
+        for pick in self.browse(cr, uid, ids, context):
+            sql_str="""update stock_move set
+                    date_planned='%s' 
+                where
+                    picking_id=%d """ % (value,pick.id)
+            if pick.min_date:
+                sql_str += " and (date_planned='"+pick.min_date+"' or date_planned<'"+value+"')"
+            cr.execute(sql_str)
+        return True
+
+    def get_min_max_date(self, cr, uid, ids, field_name, arg, context={}):
+        res = {}
+        for id in ids:
+            res[id] = {'min_date':False, 'max_date': False}
+        if not ids:
+            return res
+        cr.execute("""select
+                picking_id,
+                min(date_planned),
+                max(date_planned)
+            from
+                stock_move 
+            where
+                picking_id in (""" + ','.join(map(str, ids)) + """)
+            group by
+                picking_id""")
+        for pick, dt1,dt2 in cr.fetchall():
+            res[pick]['min_date'] = dt1
+            res[pick]['max_date'] = dt2
+        return res
+
     _columns = {
-        'name': fields.char('Packing name', size=64, required=True, select=True),
-        'origin': fields.char('Origin', size=64),
+        'name': fields.char('Reference', size=64, required=True, select=True),
+        'origin': fields.char('Origin Reference', size=64),
+        'backorder_id': fields.many2one('stock.picking', 'Back Order'),
         'type': fields.selection([('out','Sending Goods'),('in','Getting Goods'),('internal','Internal'),('delivery','Delivery')], 'Shipping Type', required=True, select=True),
         'active': fields.boolean('Active'),
         'note': fields.text('Notes'),
 
         'location_id': fields.many2one('stock.location', 'Location'),
         'location_dest_id': fields.many2one('stock.location', 'Dest. Location'),
-
         'move_type': fields.selection([('direct','Direct Delivery'),('one','All at once')],'Delivery Method', required=True),
         'state': fields.selection([
             ('draft','Draft'),
@@ -392,9 +393,13 @@ class stock_picking(osv.osv):
             ('assigned','Assigned'),
             ('done','Done'),
             ('cancel','Cancel'),
-            ], 'State', readonly=True),
-        'date':fields.datetime('Date create'),
-
+            ], 'Status', readonly=True, select=True),
+        'min_date': fields.function(get_min_max_date, fnct_inv=_set_minimum_date, multi="min_max_date",
+                 method=True,store=True, type='datetime', string='Planned Date', select=1),
+        'date':fields.datetime('Date Order'),
+        'date_done':fields.datetime('Date Done'),
+        'max_date': fields.function(get_min_max_date, fnct_inv=_set_maximum_date, multi="min_max_date",
+                 method=True,store=True, type='datetime', string='Max. Planned Date', select=2),
         'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines'),
 
         'auto_picking': fields.boolean('Auto-Packing'),
@@ -402,10 +407,11 @@ class stock_picking(osv.osv):
         'invoice_state':fields.selection([
             ("invoiced","Invoiced"),
             ("2binvoiced","To be invoiced"),
-            ("none","Not from Packing")], "Invoice state"),
+            ("none","Not from Packing")], "Invoice Status", 
+            select=True, required=True, readonly=True, states={'draft':[('readonly',False)]}),
     }
     _defaults = {
-        'name': lambda *a: '/',
+        'name': lambda self,cr,uid,context: self.pool.get('ir.sequence').get(cr, uid, 'stock.picking'),
         'active': lambda *a: 1,
         'state': lambda *a: 'draft',
         'move_type': lambda *a: 'direct',
@@ -413,72 +419,40 @@ class stock_picking(osv.osv):
         'invoice_state': lambda *a: 'none',
         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
     }
+    #def copy(self, cr, uid, id, data=None, context={}):
+    #    data = data or {}
+    #    return super(stock_picking, self).copy(cr, uid, id, data, context)
+
     def onchange_partner_in(self, cr, uid, context, partner_id=None):
         sid = self.pool.get('res.partner.address').browse(cr, uid, partner_id, context).partner_id.property_stock_supplier.id
-        return {
-            'value': {'location_id':sid}
-        }
+        return { }
 
-    def action_explode(self, cr, uid, ids, *args):
-        return True
+    def action_explode(self, cr, uid, moves, context={}):
+        return moves
 
-    def action_confirm(self, cr, uid, ids, *args):
+    def action_confirm(self, cr, uid, ids, context={}):
         self.write(cr, uid, ids, {'state': 'confirmed'})
         todo = []
         for picking in self.browse(cr, uid, ids):
-            if picking.name == self._defaults['name']():
-                number = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.%s' % picking.type)
-                self.write(cr, uid, [picking.id], {'name': number})
             for r in picking.move_lines:
                 if r.state=='draft':
-                    todo.append(r.id)
-        if len(todo):
-            self.pool.get('stock.move').action_confirm(cr,uid, todo)
-        self.action_explode(cr, uid, ids)
-        for picking in self.browse(cr, uid, ids):
-            todo = []
-            for r in picking.move_lines:
-                if r.location_dest_id.chained_location_type:
                     todo.append(r)
-            if todo:
-                loc = self.pool.get('stock.location').chained_location_get(cr, uid, todo[0].location_dest_id, todo[0].picking_id and todo[0].picking_id.address_id and todo[0].picking_id.address_id.partner_id, todo[0].product_id)
-                ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0].location_dest_id, loc)
-                pickid = self.pool.get('stock.picking').create(cr, uid, {
-                    'name': picking.name,
-                    'origin': str(picking.origin or ''),
-                    'type': ptype,
-                    'note': picking.note,
-                    'move_type': picking.move_type,
-                    'auto_picking': todo[0].location_dest_id.chained_auto_packing,
-                    'address_id': picking.address_id.id,
-                    'invoice_state': 'none'
-                })
-                for move in todo:
-                    loc = self.pool.get('stock.location').chained_location_get(cr, uid, move.location_dest_id, picking.address_id.partner_id, move.product_id).id
-                    new_id = self.pool.get('stock.move').copy(cr, uid, move.id, {
-                        'location_id': move.location_dest_id.id,
-                        'location_dest_id': loc,
-                        'date_moved': time.strftime('%Y-%m-%d'),
-                        'picking_id': pickid,
-                        'state':'waiting',
-                        'prodlot_id':False,
-                        'tracking_id':False,
-                        'move_history_ids':[],
-                        'date_planned': DateTime.strptime(move.date_planned, '%Y-%m-%d') + DateTime.RelativeDateTime(days=move.location_dest_id.chained_delay or 0),
-                        'move_history_ids2':[]}
-                    )
-                    self.pool.get('stock.move').write(cr, uid, [move.id], {
-                        'move_dest_id': new_id,
-                        'move_history_ids': [(4, new_id)]
-                    })
-                wf_service = netsvc.LocalService("workflow")
-                wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
+        todo = self.action_explode(cr, uid, todo, context)
+        if len(todo):
+            self.pool.get('stock.move').action_confirm(cr, uid, todo, context)
         return True
 
     def test_auto_picking(self, cr, uid, ids):
         # TODO: Check locations to see if in the same location ?
         return True
 
+    def button_confirm(self, cr, uid, ids, *args):
+        for id in ids:
+            wf_service = netsvc.LocalService("workflow")
+            wf_service.trg_validate(uid, 'stock.picking', id, 'button_confirm', cr)
+        self.force_assign(cr, uid, ids, *args)
+        return True
+
     def action_assign(self, cr, uid, ids, *args):
         for pick in self.browse(cr, uid, ids):
             move_ids = [x.id for x in pick.move_lines if x.state=='confirmed']
@@ -493,7 +467,25 @@ class stock_picking(osv.osv):
             self.pool.get('stock.move').force_assign(cr, uid, move_ids)
             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
         return True
-
+    
+    def draft_force_assign(self, cr, uid, ids, *args):
+        wf_service = netsvc.LocalService("workflow")
+        for pick in self.browse(cr, uid, ids):
+            wf_service.trg_validate(uid, 'stock.picking', pick.id,
+                'button_confirm', cr)
+            move_ids = [x.id for x in pick.move_lines]
+            self.pool.get('stock.move').force_assign(cr, uid, move_ids)
+            wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
+        return True
+    
+    def draft_validate(self, cr, uid, ids, *args):
+        wf_service = netsvc.LocalService("workflow")
+        self.draft_force_assign(cr, uid, ids)
+        for pick in self.browse(cr, uid, ids):
+            self.action_move(cr, uid, [pick.id])
+            wf_service.trg_validate(uid, 'stock.picking', pick.id , 'button_done', cr)
+        return True
+    
     def cancel_assign(self, cr, uid, ids, *args):
         wf_service = netsvc.LocalService("workflow")
         for pick in self.browse(cr, uid, ids):
@@ -540,7 +532,7 @@ class stock_picking(osv.osv):
     # TODO: change and create a move if not parents
     #
     def action_done(self, cr, uid, ids, context=None):
-        self.write(cr,uid, ids, {'state':'done'})
+        self.write(cr,uid, ids, {'state':'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
         return True
 
     def action_move(self, cr, uid, ids, context={}):
@@ -581,9 +573,19 @@ class stock_picking(osv.osv):
     def _get_taxes_invoice(self, cursor, user, move_line, type):
         '''Return taxes ids for the move line'''
         if type in ('in_invoice', 'in_refund'):
-            return [x.id for x in move_line.product_id.supplier_taxes_id]
+            taxes = move_line.product_id.supplier_taxes_id
         else:
-            return [x.id for x in move_line.product_id.taxes_id]
+            taxes = move_line.product_id.taxes_id
+
+        if move_line.picking_id and move_line.picking_id.address_id and move_line.picking_id.address_id.partner_id:
+            return self.pool.get('account.fiscal.position').map_tax(
+                cursor,
+                user,
+                move_line.picking_id.address_id.partner_id,
+                taxes
+            )
+        else:
+            return map(lambda x: x.id, taxes)
 
     def _get_account_analytic_invoice(self, cursor, user, picking, move_line):
         return False
@@ -598,24 +600,29 @@ class stock_picking(osv.osv):
 
     def action_invoice_create(self, cursor, user, ids, journal_id=False,
             group=False, type='out_invoice', context=None):
+        print "WW"*12,context
         '''Return ids of created invoices for the pickings'''
         invoice_obj = self.pool.get('account.invoice')
         invoice_line_obj = self.pool.get('account.invoice.line')
         invoices_group = {}
         res = {}
-
-        for picking in self.browse(cursor, user, ids, context=context):
+        sale_line_obj = self.pool.get('sale.order.line')
+        
+        for picking in self.browse(cursor, user, ids, context=context):            
             if picking.invoice_state != '2binvoiced':
                 continue
+            payment_term_id = False
+            partner = picking.address_id and picking.address_id.partner_id
+            if not partner:
+                raise osv.except_osv(_('Error, no partner !'),
+                    _('Please put a partner on the picking list if you want to generate invoice.'))
 
-            partner = picking.address_id.partner_id
             if type in ('out_invoice', 'out_refund'):
                 account_id = partner.property_account_receivable.id
+                if picking.sale_id and picking.sale_id.payment_term:
+                    payment_term_id= picking.sale_id.payment_term.id
             else:
                 account_id = partner.property_account_payable.id
-            payment_term_id = False
-            if partner.property_payment_term:
-                payment_term_id = partner.property_payment_term.id
 
             address_contact_id, address_invoice_id = \
                     self._get_address_invoice(cursor, user, picking).values()
@@ -627,7 +634,7 @@ class stock_picking(osv.osv):
             else:
                 invoice_vals = {
                     'name': picking.name,
-                    'origin': picking.name + ':' + picking.origin,
+                    'origin': picking.name + (picking.origin and (':' + picking.origin) or ''),
                     'type': type,
                     'account_id': account_id,
                     'partner_id': partner.id,
@@ -642,6 +649,50 @@ class stock_picking(osv.osv):
                         context=context)
                 invoices_group[partner.id] = invoice_id
             res[picking.id] = invoice_id
+            sale_line_ids = sale_line_obj.search(cursor, user, [('order_id','=',picking.sale_id.id)])
+            sale_lines = sale_line_obj.browse(cursor, user, sale_line_ids, context=context)
+            for sale_line in sale_lines:
+                if sale_line.product_id.type == 'service' and sale_line.invoiced == False:
+                    if group:
+                        name = picking.name + '-' + sale_line.name
+                    else:
+                        name = sale_line.name
+                    if type in ('out_invoice', 'out_refund'):
+                        account_id = sale_line.product_id.product_tmpl_id.\
+                                property_account_income.id
+                        if not account_id:
+                            account_id = sale_line.product_id.categ_id.\
+                                    property_account_income_categ.id
+                    else:
+                        account_id = sale_line.product_id.product_tmpl_id.\
+                                property_account_expense.id
+                        if not account_id:
+                            account_id = sale_line.product_id.categ_id.\
+                                    property_account_expense_categ.id
+                    price_unit = self._get_price_unit_invoice(cursor, user,
+                            sale_line, type)
+                    discount = self._get_discount_invoice(cursor, user, sale_line)
+                    tax_ids = self._get_taxes_invoice(cursor, user, sale_line, type)
+
+                    account_analytic_id = self._get_account_analytic_invoice(cursor,
+                            user, picking, sale_line)
+
+                    account_id = self.pool.get('account.fiscal.position').map_account(cursor, user, partner, account_id)
+                    invoice_line_id = invoice_line_obj.create(cursor, user, {
+                        'name': name,
+                        'invoice_id': invoice_id,
+                        'uos_id': sale_line.product_uos.id or sale_line.product_uom.id,
+                        'product_id': sale_line.product_id.id,
+                        'account_id': account_id,
+                        'price_unit': price_unit,
+                        'discount': discount,
+                        'quantity': sale_line.product_uos_qty,
+                        'invoice_line_tax_id': [(6, 0, tax_ids)],
+                        'account_analytic_id': account_analytic_id,
+                        }, context=context)
+                    sale_line_obj.write(cursor, user, [sale_line.id], {'invoiced':True,
+                        'invoice_lines': [(6, 0, [invoice_line_id])],
+                        })
 
             for move_line in picking.move_lines:
                 if group:
@@ -669,6 +720,7 @@ class stock_picking(osv.osv):
                 account_analytic_id = self._get_account_analytic_invoice(cursor,
                         user, picking, move_line)
 
+                account_id = self.pool.get('account.fiscal.position').map_account(cursor, user, partner, account_id)
                 invoice_line_id = invoice_line_obj.create(cursor, user, {
                     'name': name,
                     'invoice_id': invoice_id,
@@ -677,7 +729,7 @@ class stock_picking(osv.osv):
                     'account_id': account_id,
                     'price_unit': price_unit,
                     'discount': discount,
-                    'quantity': move_line.product_uos_qty,
+                    'quantity': move_line.product_uos_qty or move_line.product_qty,
                     'invoice_line_tax_id': [(6, 0, tax_ids)],
                     'account_analytic_id': account_analytic_id,
                     }, context=context)
@@ -710,19 +762,43 @@ class stock_production_lot(osv.osv):
                 name=name+'/'+record['ref']
             res.append((record['id'], name))
         return res
+    
 
     _name = 'stock.production.lot'
     _description = 'Production lot'
 
+    def _get_stock(self, cr, uid, ids, field_name, arg, context={}):
+        if 'location_id' not in context:
+            locations = self.pool.get('stock.location').search(cr, uid, [('usage','=','internal')], context=context)
+        else:
+            locations = [context['location_id']]
+        res = {}.fromkeys(ids, 0.0)
+        cr.execute('''select
+                prodlot_id,
+                sum(name)
+            from
+                stock_report_prodlots
+            where
+                location_id in ('''+','.join(map(str, locations))+''')  and
+                prodlot_id in  ('''+','.join(map(str, ids))+''')
+            group by
+                prodlot_id
+        ''')
+        res.update(dict(cr.fetchall()))
+        return res
+
     _columns = {
         'name': fields.char('Serial', size=64, required=True),
-        'ref': fields.char('Reference', size=64),
-        'date': fields.datetime('Date create', required=True),
+        'ref': fields.char('Internal Ref.', size=64),
+        'product_id': fields.many2one('product.product','Product',required=True),
+        'date': fields.datetime('Created Date', required=True),
+        'stock_available': fields.function(_get_stock, method=True, type="float", string="Available", select="2"),
         'revisions': fields.one2many('stock.production.lot.revision','lot_id','Revisions'),
     }
     _defaults = {
         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
         'name': lambda x,y,z,c: x.pool.get('ir.sequence').get(y,z,'stock.lot.serial'),
+        'product_id': lambda x,y,z,c: c.get('product_id',False),
     }
     _sql_constraints = [
         ('name_ref_uniq', 'unique (name, ref)', 'The serial/ref must be unique !'),
@@ -739,7 +815,7 @@ class stock_production_lot_revision(osv.osv):
         'date': fields.date('Revision date'),
         'indice': fields.char('Revision', size=16),
         'author_id': fields.many2one('res.users', 'Author'),
-        'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True),
+        'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True, ondelete='cascade'),
     }
 
     _defaults = {
@@ -763,15 +839,39 @@ class stock_move(osv.osv):
         return (res and res[0]) or False
     _name = "stock.move"
     _description = "Stock Move"
+    def name_get(self, cr, uid, ids, context={}):
+        res = []
+        for line in self.browse(cr, uid, ids, context):
+            res.append((line.id, (line.product_id.code or '/')+': '+line.location_id.name+' > '+line.location_dest_id.name))
+        return res
 
+    def _check_tracking(self, cr, uid, ids):
+         for move in self.browse(cr, uid, ids):             
+             if not move.prodlot_id and \
+                (move.state == 'done' and \
+                ( \
+                    (move.product_id.track_production and move.location_id.usage=='production') or \
+                    (move.product_id.track_production and move.location_dest_id.usage=='production') or \
+                    (move.product_id.track_incoming and move.location_id.usage=='supplier') or \
+                    (move.product_id.track_outgoing and move.location_dest_id.usage=='customer') \
+                )):
+                    return False
+         return True
+
+    def _check_product_lot(self, cr, uid, ids):
+         for move in self.browse(cr, uid, ids):
+             if move.prodlot_id and (move.prodlot_id.product_id.id != move.product_id.id):
+                return False                          
+         return True
+         
     _columns = {
         'name': fields.char('Name', size=64, required=True, select=True),
         'priority': fields.selection([('0','Not urgent'),('1','Urgent')], 'Priority'),
 
         'date': fields.datetime('Date Created'),
-        'date_planned': fields.date('Scheduled date', required=True),
+        'date_planned': fields.datetime('Scheduled date', required=True),
 
-        'product_id': fields.many2one('product.product', 'Product', required=True),
+        'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
 
         'product_qty': fields.float('Quantity', required=True),
         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
@@ -779,8 +879,8 @@ class stock_move(osv.osv):
         'product_uos': fields.many2one('product.uom', 'Product UOS'),
         'product_packaging' : fields.many2one('product.packaging', 'Packaging'),
 
-        'location_id': fields.many2one('stock.location', 'Source Location', required=True),
-        'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True),
+        'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
+        'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
         'address_id' : fields.many2one('res.partner.address', 'Dest. Address'),
 
         'prodlot_id' : fields.many2one('stock.production.lot', 'Production lot', help="Production lot is used to put a serial number on the production"),
@@ -796,15 +896,41 @@ class stock_move(osv.osv):
 
         'note': fields.text('Notes'),
 
-        'state': fields.selection([('draft','Draft'),('waiting','Waiting'),('confirmed','Confirmed'),('assigned','Assigned'),('done','Done'),('cancel','cancel')], 'State', readonly=True, select=True),
+        'state': fields.selection([('draft','Draft'),('waiting','Waiting'),('confirmed','Confirmed'),('assigned','Assigned'),('done','Done'),('cancel','cancel')], 'Status', readonly=True, select=True),
         'price_unit': fields.float('Unit Price',
             digits=(16, int(config['price_accuracy']))),
     }
+    _constraints = [
+        (_check_tracking,
+            'You must assign a production lot for this product',
+            ['prodlot_id']),
+        (_check_product_lot,
+            'You try to assign a lot which is not from the same product',
+            ['prodlot_id'])]
+    def _default_location_destination(self, cr, uid, context={}):
+        if context.get('move_line', []):
+            return context['move_line'][0][2]['location_dest_id']
+        if context.get('address_out_id', False):
+            return self.pool.get('res.partner.address').browse(cr, uid, context['address_out_id'], context).partner_id.property_stock_customer.id
+        return False
+
+    def _default_location_source(self, cr, uid, context={}):
+        if context.get('move_line', []):
+            try:
+                return context['move_line'][0][2]['location_id']
+            except:
+                pass
+        if context.get('address_in_id', False):
+            return self.pool.get('res.partner.address').browse(cr, uid, context['address_in_id'], context).partner_id.property_stock_supplier.id
+        return False
+
     _defaults = {
+        'location_id': _default_location_source,
+        'location_dest_id': _default_location_destination,
         'state': lambda *a: 'draft',
         'priority': lambda *a: '1',
         'product_qty': lambda *a: 1.0,
-        'date_planned': lambda *a: time.strftime('%Y-%m-%d'),
+        'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
     }
 
@@ -818,6 +944,18 @@ class stock_move(osv.osv):
                     ON stock_move (location_id, location_dest_id, product_id, state)')
             cursor.commit()
 
+    def onchange_lot_id(self, cr, uid, context, prodlot_id=False,product_qty=False, loc_id=False):
+        if not prodlot_id or not loc_id:
+            return {}
+        prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id)
+        location=self.pool.get('stock.location').browse(cr,uid,loc_id)
+        warning={}
+        if (location.usage == 'internal') and (product_qty > (prodlot.stock_available or 0.0)):
+            warning={
+                'title':'Bad Lot Assignation !',
+                'message':'You are moving %.2f products but only %.2f available in this lot.' % (product_qty,prodlot.stock_available or 0.0)
+            }
+        return {'warning':warning}
 
     def onchange_product_id(self, cr, uid, context, prod_id=False, loc_id=False, loc_dest_id=False):
         if not prod_id:
@@ -833,9 +971,62 @@ class stock_move(osv.osv):
             result['location_dest_id'] = loc_dest_id
         return {'value':result}
 
-    def action_confirm(self, cr, uid, ids, context={}):
+    def _chain_compute(self, cr, uid, moves, context={}):
+        result = {}
+        for m in moves:
+            dest = self.pool.get('stock.location').chained_location_get(
+                cr, 
+                uid, 
+                m.location_dest_id, 
+                m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id, 
+                m.product_id, 
+                context
+            )
+            if dest:
+                if dest[1]=='transparent':
+                    self.write(cr, uid, [m.id], {
+                        'date_planned': (DateTime.strptime(m.date_planned, '%Y-%m-%d %H:%M:%S') + \
+                            DateTime.RelativeDateTime(days=dest[2] or 0)).strftime('%Y-%m-%d'),
+                        'location_dest_id': dest[0].id})
+                else:
+                    result.setdefault(m.picking_id, [])
+                    result[m.picking_id].append( (m, dest) )
+        return result
+
+    def action_confirm(self, cr, uid, moves, context={}):
+        ids = map(lambda m: m.id, moves)
         self.write(cr, uid, ids, {'state':'confirmed'})
-        return True
+        for picking, todo in self._chain_compute(cr, uid, moves, context).items():
+            ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
+            pickid = self.pool.get('stock.picking').create(cr, uid, {
+                'name': picking.name,
+                'origin': str(picking.origin or ''),
+                'type': ptype,
+                'note': picking.note,
+                'move_type': picking.move_type,
+                'auto_picking': todo[0][1][1]=='auto',
+                'address_id': picking.address_id.id,
+                'invoice_state': 'none'
+            })
+            for move,(loc,auto,delay) in todo:
+                # Is it smart to copy ? May be it's better to recreate ?
+                new_id = self.pool.get('stock.move').copy(cr, uid, move.id, {
+                    'location_id': move.location_dest_id.id,
+                    'location_dest_id': loc.id,
+                    'date_moved': time.strftime('%Y-%m-%d'),
+                    'picking_id': pickid,
+                    'state':'waiting',
+                    'move_history_ids':[],
+                    'date_planned': (DateTime.strptime(move.date_planned, '%Y-%m-%d %H:%M:%S') + DateTime.RelativeDateTime(days=delay or 0)).strftime('%Y-%m-%d'),
+                    'move_history_ids2':[]}
+                )
+                self.pool.get('stock.move').write(cr, uid, [move.id], {
+                    'move_dest_id': new_id,
+                    'move_history_ids': [(4, new_id)]
+                })
+            wf_service = netsvc.LocalService("workflow")
+            wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
+        return []
 
     def action_assign(self, cr, uid, ids, *args):
         todo = []
@@ -917,11 +1108,11 @@ class stock_move(osv.osv):
         return True
 
     def action_done(self, cr, uid, ids, context=None):
+        track_flag=False
         for move in self.browse(cr, uid, ids):
             if move.move_dest_id.id and (move.state != 'done'):
                 mid = move.move_dest_id.id
-                if move.move_dest_id.id:
-                    cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%d,%d)', (move.id, move.move_dest_id.id))
+                cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%d,%d)', (move.id, move.move_dest_id.id))
                 if move.move_dest_id.state in ('waiting','confirmed'):
                     self.write(cr, uid, [move.move_dest_id.id], {'state':'assigned'})
                     if move.move_dest_id.picking_id:
@@ -1002,21 +1193,18 @@ class stock_move(osv.osv):
                                 'ref': ref,
                                 'date': date})
                     ]
-                    self.pool.get('account.move').create(cr, uid,
-                            {
-                                'name': move.name,
-                                'journal_id': journal_id,
-                                'line_id': lines,
-                                'ref': ref,
-                            })
-        self.write(cr, uid, ids, {'state':'done'})
-
+                    self.pool.get('account.move').create(cr, uid, {
+                        'name': move.name,
+                        'journal_id': journal_id,
+                        'line_id': lines,
+                        'ref': ref,
+                    })
+        self.write(cr, uid, ids, {'state':'done','date_planned':time.strftime('%Y-%m-%d %H:%M:%S')})
         wf_service = netsvc.LocalService("workflow")
         for id in ids:
             wf_service.trg_trigger(uid, 'stock.move', id, cr)
         return True
 
-
     def unlink(self, cr, uid, ids, context=None):
         for move in self.browse(cr, uid, ids, context=context):
             if move.state != 'draft':
@@ -1036,7 +1224,7 @@ class stock_inventory(osv.osv):
         'date_done': fields.datetime('Date done'),
         'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=True, states={'draft':[('readonly',False)]}),
         'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
-        'state': fields.selection( (('draft','Draft'),('done','Done')), 'State', readonly=True),
+        'state': fields.selection( (('draft','Draft'),('done','Done')), 'Status', readonly=True),
     }
     _defaults = {
         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
@@ -1130,94 +1318,26 @@ class stock_warehouse(osv.osv):
 stock_warehouse()
 
 
-# Product
-class product_product(osv.osv):
-    _inherit = "product.product"
-    #
-    # Utiliser browse pour limiter les queries !
-    #
-    def view_header_get(self, cr, user, view_id, view_type, context):
-        if (not context.get('location', False)):
-            return False
-        cr.execute('select name from stock_location where id=%d', (context['location'],))
-        j = cr.fetchone()[0]
-        if j:
-            return 'Products: '+j
-        return False
-
-    def _get_product_available_func(states, what):
-        def _product_available(self, cr, uid, ids, name, arg, context={}):
-            if context.get('shop', False):
-                cr.execute('select warehouse_id from sale_shop where id=%d', (int(context['shop']),))
-                res2 = cr.fetchone()
-                if res2:
-                    context['warehouse'] = res2[0]
-
-            if context.get('warehouse', False):
-                cr.execute('select lot_stock_id from stock_warehouse where id=%d', (int(context['warehouse']),))
-                res2 = cr.fetchone()
-                if res2:
-                    context['location'] = res2[0]
-
-            if context.get('location', False):
-                location_ids = [context['location']]
-            else:
-                # get the list of ids of the stock location of all warehouses
-                cr.execute("select lot_stock_id from stock_warehouse")
-                location_ids = [id for (id,) in cr.fetchall()]
-                
-            # build the list of ids of children of the location given by id
-            location_ids = self.pool.get('stock.location').search(cr, uid, [('location_id', 'child_of', location_ids)])
-            res = self.pool.get('stock.location')._product_get_multi_location(cr, uid, location_ids, ids, context, states, what)
-            for id in ids:
-                res.setdefault(id, 0.0)
-            return res
-        return _product_available
-    _product_qty_available = _get_product_available_func(('done',), ('in', 'out'))
-    _product_virtual_available = _get_product_available_func(('confirmed','waiting','assigned','done'), ('in', 'out'))
-    _product_outgoing_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('out',))
-    _product_incoming_qty = _get_product_available_func(('confirmed','waiting','assigned'), ('in',))
-    _columns = {
-        'qty_available': fields.function(_product_qty_available, method=True, type='float', string='Real Stock'),
-        'virtual_available': fields.function(_product_virtual_available, method=True, type='float', string='Virtual Stock'),
-        'incoming_qty': fields.function(_product_incoming_qty, method=True, type='float', string='Incoming'),
-        'outgoing_qty': fields.function(_product_outgoing_qty, method=True, type='float', string='Outgoing'),
-    }
-product_product()
-
 # Move wizard : 
 #    get confirm or assign stock move lines of partner and put in current picking.
 class stock_picking_move_wizard(osv.osv_memory):
     _name='stock.picking.move.wizard'
-    def _get_picking(self,cr, uid, ctx):
-        if 'action_id' in ctx:
+    def _get_picking(self,cr, uid, ctx):        
+        if ctx.get('action_id',False):
             return ctx['action_id']
-        return False 
-    def _get_move_lines(self,cr,uid,ctx):
-        move_obj=self.pool.get('stock.move')
-        picking_obj=self.pool.get('stock.picking')
-        if 'action_id' in ctx:
-            picking=picking_obj.browse(cr,uid,[ctx['action_id']])
-            if picking and len(picking):
-                move_line_ids=move_obj.search(cr,uid,[('state','in',['confirmed','assigned']),('address_id','=',picking[0].address_id.id)])                
-                move_lines=move_obj.read(cr,uid,move_line_ids)
-                #res=[]
-                #for move_line in move_lines:
-                #    res.append((0,0,move_line))
-                return [{'move_ids':(0,0,move_lines)}]
-        return []
+        return False     
     def _get_picking_address(self,cr,uid,ctx):        
-        picking_obj=self.pool.get('stock.picking')
-        if 'action_id' in ctx:
-            picking=picking_obj.browse(cr,uid,[ctx['action_id']])[0]
-            return picking.address_id and picking.address_id.id
+        picking_obj=self.pool.get('stock.picking')        
+        if ctx.get('action_id',False):
+            picking=picking_obj.browse(cr,uid,[ctx['action_id']])[0]            
+            return picking.address_id and picking.address_id.id or False        
         return False
             
             
     _columns={
         'name':fields.char('Name',size=64,invisible=True),
         #'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines',readonly=True),
-        'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Move lines'),
+        'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Move lines',required=True),
         'address_id' : fields.many2one('res.partner.address', 'Dest. Address',invisible=True),
         'picking_id': fields.many2one('stock.picking', 'Packing list', select=True,invisible=True),
     }
@@ -1240,6 +1360,4 @@ class stock_picking_move_wizard(osv.osv_memory):
         return {'type':'ir.actions.act_window_close' }
             
 stock_picking_move_wizard()        
-
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-