[MERGE] forward port of branch 8.0 up to e883193
[odoo/odoo.git] / addons / sale_stock / sale_stock.py
index da134a3..2375063 100644 (file)
@@ -21,7 +21,6 @@
 ##############################################################################
 from datetime import datetime, timedelta
 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
-from dateutil.relativedelta import relativedelta
 from openerp.osv import fields, osv
 from openerp.tools.safe_eval import safe_eval as eval
 from openerp.tools.translate import _
@@ -30,20 +29,12 @@ from openerp import SUPERUSER_ID
 
 class sale_order(osv.osv):
     _inherit = "sale.order"
-    def copy(self, cr, uid, id, default=None, context=None):
-        if not default:
-            default = {}
-        default.update({
-            'shipped': False,
-            'picking_ids': []
-        })
-        return super(sale_order, self).copy(cr, uid, id, default, context=context)
 
     def _get_default_warehouse(self, cr, uid, context=None):
         company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
         warehouse_ids = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)
         if not warehouse_ids:
-            raise osv.except_osv(_('Error!'), _('There is no warehouse defined for selected company.'))
+            return False
         return warehouse_ids[0]
 
     def _get_shipped(self, cr, uid, ids, name, args, context=None):
@@ -62,32 +53,31 @@ class sale_order(osv.osv):
             if move.procurement_id and move.procurement_id.sale_line_id:
                 res.add(move.procurement_id.sale_line_id.order_id.id)
         return list(res)
-    
+
     def _get_orders_procurements(self, cr, uid, ids, context=None):
         res = set()
         for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context):
-            if proc.sale_line_id:
+            if proc.state =='done' and proc.sale_line_id:
                 res.add(proc.sale_line_id.order_id.id)
         return list(res)
-    
+
     def _get_picking_ids(self, cr, uid, ids, name, args, context=None):
         res = {}
         for sale in self.browse(cr, uid, ids, context=context):
             if not sale.procurement_group_id:
                 res[sale.id] = []
                 continue
-            picking_ids = {}
-            for procurement in sale.procurement_group_id.procurement_ids:
-                for move in procurement.move_ids:
-                    if move.picking_id:
-                        picking_ids[move.picking_id.id] = True
-            res[sale.id] = picking_ids.keys()
+            res[sale.id] = self.pool.get('stock.picking').search(cr, uid, [('group_id', '=', sale.procurement_group_id.id)], context=context)
         return res
 
-    def _prepare_order_line_procurement(self, cr, uid, order, line, group_id = False, context=None):
+    def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
         vals = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
         location_id = order.partner_shipping_id.property_stock_customer.id
         vals['location_id'] = location_id
+        routes = line.route_id and [(4, line.route_id.id)] or []
+        vals['route_ids'] = routes
+        vals['warehouse_id'] = order.warehouse_id and order.warehouse_id.id or False
+        vals['partner_dest_id'] = order.partner_shipping_id.id
         return vals
 
     _columns = {
@@ -102,10 +92,9 @@ class sale_order(osv.osv):
             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
             help="""On demand: A draft invoice can be created from the sales order when needed. \nOn delivery order: A draft invoice can be created from the delivery order when the products have been delivered. \nBefore delivery: A draft invoice is created from the sales order and must be paid before the products can be delivered."""),
         'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={
-                'stock.move': (_get_orders, ['state'], 10),
                 'procurement.order': (_get_orders_procurements, ['state'], 10)
             }),
-        'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
+        'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
         'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking associated to this sale'),
     }
     _defaults = {
@@ -121,45 +110,34 @@ class sale_order(osv.osv):
                 val['company_id'] = warehouse.company_id.id
         return {'value': val}
 
-    # FP Note: to change, take the picking related to the moves related to the
-    # procurements related to SO lines
-
     def action_view_delivery(self, cr, uid, ids, context=None):
         '''
         This function returns an action that display existing delivery orders
         of given sales order ids. It can either be a in a list or in a form
         view, if there is only one delivery order to show.
         '''
+        
         mod_obj = self.pool.get('ir.model.data')
         act_obj = self.pool.get('ir.actions.act_window')
 
-        result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree')
+        result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree_all')
         id = result and result[1] or False
         result = act_obj.read(cr, uid, [id], context=context)[0]
 
         #compute the number of delivery orders to display
         pick_ids = []
-        ctx = {}
         for so in self.browse(cr, uid, ids, context=context):
             pick_ids += [picking.id for picking in so.picking_ids]
-        if len(pick_ids)>0:
-            pick_type = self.pool.get('stock.picking').browse(cr, uid, pick_ids[0], context=context)['picking_type_id'].id
-            ctx = eval(result['context'],{'active_id': pick_type}, nocopy=True)
+            
         #choose the view_mode accordingly
         if len(pick_ids) > 1:
-            result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
+            result['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
         else:
             res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form')
             result['views'] = [(res and res[1] or False, 'form')]
             result['res_id'] = pick_ids and pick_ids[0] or False
-        result.update({
-            'context': ctx,
-        })
         return result
 
-    # TODO: FP Note: I guess it's better to do:
-    # if order_policy<>picking: super()
-    # else: call invoice_on_picking_method()
     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
         move_obj = self.pool.get("stock.move")
         res = super(sale_order,self).action_invoice_create(cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context)
@@ -181,13 +159,7 @@ class sale_order(osv.osv):
                     raise osv.except_osv(
                         _('Cannot cancel sales order!'),
                         _('You must first cancel all delivery order(s) attached to this sales order.'))
-                 # FP Note: not sure we need this
-                 #if pick.state == 'cancel':
-                 #    for mov in pick.move_lines:
-                 #        proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
-                 #        if proc_ids:
-                 #            proc_obj.signal_button_check(cr, uid, proc_ids)
-            stock_obj.signal_button_cancel(cr, uid, [p.id for p in sale.picking_ids])
+            stock_obj.signal_workflow(cr, uid, [p.id for p in sale.picking_ids], 'button_cancel')
         return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
 
     def action_wait(self, cr, uid, ids, context=None):
@@ -198,8 +170,6 @@ class sale_order(osv.osv):
                 self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
         return res
 
-
-        
     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
         date_planned = super(sale_order, self)._get_date_planned(cr, uid, order, line, start_date, context=context)
         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
@@ -224,9 +194,6 @@ class sale_order(osv.osv):
             res = self.write(cr, uid, [order.id], val)
         return True
 
-
-
-
     def has_stockable_products(self, cr, uid, ids, *args):
         for order in self.browse(cr, uid, ids):
             for order_line in order.order_line:
@@ -235,10 +202,21 @@ class sale_order(osv.osv):
         return False
 
 
+class product_product(osv.osv):
+    _inherit = 'product.product'
+    
+    def need_procurement(self, cr, uid, ids, context=None):
+        #when sale/product is installed alone, there is no need to create procurements, but with sale_stock
+        #we must create a procurement for each product that is not a service.
+        for product in self.browse(cr, uid, ids, context=context):
+            if product.type != 'service':
+                return True
+        return super(product_product, self).need_procurement(cr, uid, ids, context=context)
 
 class sale_order_line(osv.osv):
     _inherit = 'sale.order.line'
-    
+
+
     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
         res = {}
         for line in self.browse(cr, uid, ids, context=context):
@@ -251,29 +229,14 @@ class sale_order_line(osv.osv):
     _columns = {
         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
+        'route_id': fields.many2one('stock.location.route', 'Route', domain=[('sale_selectable', '=', True)]),
+        'product_tmpl_id': fields.related('product_id', 'product_tmpl_id', type='many2one', relation='product.template', string='Product Template'),
     }
 
     _defaults = {
         'product_packaging': False,
     }
 
-
-    def button_cancel(self, cr, uid, ids, context=None):
-        res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
-        for line in self.browse(cr, uid, ids, context=context):
-            for move_line in line.move_ids:
-                if move_line.state != 'cancel':
-                    raise osv.except_osv(
-                            _('Cannot cancel sales order line!'),
-                            _('You must first cancel stock moves attached to this sales order line.'))
-        return res
-
-    def copy_data(self, cr, uid, id, default=None, context=None):
-        if not default:
-            default = {}
-        default.update({'move_ids': []})
-        return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
-
     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
                                    partner_id=False, packaging=False, flag=False, context=None):
         if not product:
@@ -288,14 +251,11 @@ class sale_order_line(osv.osv):
             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
                     product=product, qty=qty, uom=uom, partner_id=partner_id,
                     packaging=packaging, flag=False, context=context)
-            warning_msgs = res.get('warning') and res['warning']['message']
+            warning_msgs = res.get('warning') and res['warning'].get('message', '') or ''
 
         products = product_obj.browse(cr, uid, product, context=context)
-        if not products.packaging:
+        if not products.packaging_ids:
             packaging = result['product_packaging'] = False
-        elif not packaging and products.packaging and not flag:
-            packaging = products.packaging[0].id
-            result['product_packaging'] = packaging
 
         if packaging:
             default_uom = products.uom_id and products.uom_id.id
@@ -322,16 +282,17 @@ class sale_order_line(osv.osv):
         return {'value': result, 'warning': warning}
 
 
-    def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
+    def product_id_change_with_wh(self, cr, uid, ids, pricelist, product, qty=0,
             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
-            lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
+            lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, warehouse_id=False, context=None):
         context = context or {}
         product_uom_obj = self.pool.get('product.uom')
-        partner_obj = self.pool.get('res.partner')
         product_obj = self.pool.get('product.product')
+        warehouse_obj = self.pool['stock.warehouse']
         warning = {}
-        res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
-            uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
+        #UoM False due to hack which makes sure uom changes price, ... in product_id_change
+        res = self.product_id_change(cr, uid, ids, pricelist, product, qty=qty,
+            uom=False, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
             lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
 
         if not product:
@@ -340,29 +301,50 @@ class sale_order_line(osv.osv):
 
         #update of result obtained in super function
         product_obj = product_obj.browse(cr, uid, product, context=context)
-        res['value']['delay'] = (product_obj.sale_delay or 0.0)
-
-        #check if product is available, and if not: raise an error
-        uom2 = False
-        if uom:
-            uom2 = product_uom_obj.browse(cr, uid, uom)
-            if product_obj.uom_id.category_id.id != uom2.category_id.id:
-                uom = False
-        if not uom2:
-            uom2 = product_obj.uom_id
+        res['value'].update({'product_tmpl_id': product_obj.product_tmpl_id.id, 'delay': (product_obj.sale_delay or 0.0)})
 
         # Calling product_packaging_change function after updating UoM
         res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
         res['value'].update(res_packing.get('value', {}))
         warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or ''
-        compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
-        if (product_obj.type=='product') and int(compare_qty) == -1:
-          #and (product_obj.procure_method=='make_to_stock'): --> need to find alternative for procure_method
-            warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
-                    (qty, uom2 and uom2.name or product_obj.uom_id.name,
-                     max(0,product_obj.virtual_available), product_obj.uom_id.name,
-                     max(0,product_obj.qty_available), product_obj.uom_id.name)
-            warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
+
+        if product_obj.type == 'product':
+            #determine if the product is MTO or not (for a further check)
+            isMto = False
+            if warehouse_id:
+                warehouse = warehouse_obj.browse(cr, uid, warehouse_id, context=context)
+                for product_route in product_obj.route_ids:
+                    if warehouse.mto_pull_id and warehouse.mto_pull_id.route_id and warehouse.mto_pull_id.route_id.id == product_route.id:
+                        isMto = True
+                        break
+            else:
+                try:
+                    mto_route_id = warehouse_obj._get_mto_route(cr, uid, context=context)
+                except:
+                    # if route MTO not found in ir_model_data, we treat the product as in MTS
+                    mto_route_id = False
+                if mto_route_id:
+                    for product_route in product_obj.route_ids:
+                        if product_route.id == mto_route_id:
+                            isMto = True
+                            break
+
+            #check if product is available, and if not: raise a warning, but do this only for products that aren't processed in MTO
+            if not isMto:
+                uom_record = False
+                if uom:
+                    uom_record = product_uom_obj.browse(cr, uid, uom, context=context)
+                    if product_obj.uom_id.category_id.id != uom_record.category_id.id:
+                        uom_record = False
+                if not uom_record:
+                    uom_record = product_obj.uom_id
+                compare_qty = float_compare(product_obj.virtual_available, qty, precision_rounding=uom_record.rounding)
+                if compare_qty == -1:
+                    warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
+                        (qty, uom_record.name,
+                         max(0,product_obj.virtual_available), uom_record.name,
+                         max(0,product_obj.qty_available), uom_record.name)
+                    warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
 
         #update of warning messages
         if warning_msgs:
@@ -377,7 +359,7 @@ class stock_move(osv.osv):
     _inherit = 'stock.move'
 
     def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
-        invoice_line_id = self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context)
+        invoice_line_id = super(stock_move, self)._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context)
         if move.procurement_id and move.procurement_id.sale_line_id:
             sale_line = move.procurement_id.sale_line_id
             self.pool.get('sale.order.line').write(cr, uid, [sale_line.id], {
@@ -400,6 +382,63 @@ class stock_move(osv.osv):
             sale_line = move.procurement_id.sale_line_id
             res['invoice_line_tax_id'] = [(6, 0, [x.id for x in sale_line.tax_id])]
             res['account_analytic_id'] = sale_line.order_id.project_id and sale_line.order_id.project_id.id or False
-            res['price_unit'] = sale_line.price_unit
             res['discount'] = sale_line.discount
+            if move.product_id.id != sale_line.product_id.id:
+                res['price_unit'] = self.pool['product.pricelist'].price_get(
+                    cr, uid, [sale_line.order_id.pricelist_id.id],
+                    move.product_id.id, move.product_uom_qty or 1.0,
+                    sale_line.order_id.partner_id, context=context)[sale_line.order_id.pricelist_id.id]
+            else:
+                res['price_unit'] = sale_line.price_unit
+        return res
+
+
+class stock_location_route(osv.osv):
+    _inherit = "stock.location.route"
+    _columns = {
+        'sale_selectable': fields.boolean("Selectable on Sales Order Line")
+        }
+
+
+class stock_picking(osv.osv):
+    _inherit = "stock.picking"
+
+    def _get_partner_to_invoice(self, cr, uid, picking, context=None):
+        """ Inherit the original function of the 'stock' module
+            We select the partner of the sales order as the partner of the customer invoice
+        """
+        saleorder_ids = self.pool['sale.order'].search(cr, uid, [('procurement_group_id' ,'=', picking.group_id.id)], context=context)
+        saleorders = self.pool['sale.order'].browse(cr, uid, saleorder_ids, context=context)
+        if saleorders and saleorders[0]:
+            saleorder = saleorders[0]
+            return saleorder.partner_invoice_id.id
+        return super(stock_picking, self)._get_partner_to_invoice(cr, uid, picking, context=context)
+    
+    def _get_sale_id(self, cr, uid, ids, name, args, context=None):
+        sale_obj = self.pool.get("sale.order")
+        res = {}
+        for picking in self.browse(cr, uid, ids, context=context):
+            res[picking.id] = False
+            if picking.group_id:
+                sale_ids = sale_obj.search(cr, uid, [('procurement_group_id', '=', picking.group_id.id)], context=context)
+                if sale_ids:
+                    res[picking.id] = sale_ids[0]
         return res
+    
+    _columns = {
+        'sale_id': fields.function(_get_sale_id, type="many2one", relation="sale.order", string="Sale Order"),
+    }
+
+    def _create_invoice_from_picking(self, cr, uid, picking, vals, context=None):
+        sale_obj = self.pool.get('sale.order')
+        sale_line_obj = self.pool.get('sale.order.line')
+        invoice_line_obj = self.pool.get('account.invoice.line')
+        invoice_id = super(stock_picking, self)._create_invoice_from_picking(cr, uid, picking, vals, context=context)
+        if picking.group_id:
+            sale_ids = sale_obj.search(cr, uid, [('procurement_group_id', '=', picking.group_id.id)], context=context)
+            if sale_ids:
+                sale_line_ids = sale_line_obj.search(cr, uid, [('order_id', 'in', sale_ids), ('product_id.type', '=', 'service'), ('invoiced', '=', False)], context=context)
+                if sale_line_ids:
+                    created_lines = sale_line_obj.invoice_line_create(cr, uid, sale_line_ids, context=context)
+                    invoice_line_obj.write(cr, uid, created_lines, {'invoice_id': invoice_id}, context=context)
+        return invoice_id