[MERGE] Access inherited product fields on the product directly rather than template...
authorOlivier Dony <odo@openerp.com>
Tue, 18 Dec 2012 22:50:15 +0000 (23:50 +0100)
committerOlivier Dony <odo@openerp.com>
Tue, 18 Dec 2012 22:50:15 +0000 (23:50 +0100)
It makes the code a little more straightforward and does not hurt.

bzr revid: odo@openerp.com-20121218225015-6r9ydxjlh147m3z1

20 files changed:
1  2 
addons/account/account_analytic_line.py
addons/account/account_invoice.py
addons/account_anglo_saxon/stock.py
addons/analytic_user_function/analytic_user_function.py
addons/hr_expense/hr_expense.py
addons/hr_timesheet/hr_timesheet.py
addons/hr_timesheet_invoice/hr_timesheet_invoice.py
addons/mrp/mrp.py
addons/mrp/procurement.py
addons/mrp/test/order_process.yml
addons/mrp_byproduct/mrp_byproduct.py
addons/procurement/procurement.py
addons/project_timesheet/project_timesheet.py
addons/purchase/purchase.py
addons/purchase/wizard/purchase_line_invoice.py
addons/sale/sale.py
addons/sale_stock/sale_stock.py
addons/sale_stock/test/picking_order_policy.yml
addons/stock/product.py
addons/stock/stock.py

@@@ -86,12 -87,12 +86,12 @@@ class account_analytic_line(osv.osv)
              if not a:
                  a = prod.categ_id.property_account_expense_categ.id
              if not a:
 -                raise osv.except_osv(_('Error !'),
 +                raise osv.except_osv(_('Error!'),
                          _('There is no expense account defined ' \
 -                                'for this product: "%s" (id:%d)') % \
 +                                'for this product: "%s" (id:%d).') % \
                                  (prod.name, prod.id,))
          else:
-             a = prod.product_tmpl_id.property_account_income.id
+             a = prod.property_account_income.id
              if not a:
                  a = prod.categ_id.property_account_income_categ.id
              if not a:
Simple merge
Simple merge
@@@ -151,29 -158,16 +151,29 @@@ class hr_expense_expense(osv.osv)
          sequence_obj = self.pool.get('ir.sequence')
          analytic_journal_obj = self.pool.get('account.analytic.journal')
          account_journal = self.pool.get('account.journal')
 -        for exp in self.browse(cr, uid, ids):
 +        voucher_obj = self.pool.get('account.voucher')
 +        currency_obj = self.pool.get('res.currency')
 +        wkf_service = netsvc.LocalService("workflow")
 +        if context is None:
 +            context = {}
 +        for exp in self.browse(cr, uid, ids, context=context):
              company_id = exp.company_id.id
              lines = []
 -            for l in exp.line_ids:
 -                tax_id = []
 -                if l.product_id:
 -                    acc = l.product_id.property_account_expense
 +            total = 0.0
 +            ctx = context.copy()
 +            ctx.update({'date': exp.date})
 +            journal = False
 +            if exp.journal_id:
 +                journal = exp.journal_id
 +            else:
 +                journal_id = voucher_obj._get_journal(cr, uid, context={'type': 'purchase', 'company_id': company_id})
 +                if journal_id:
 +                    journal = account_journal.browse(cr, uid, journal_id, context=context)
 +            for line in exp.line_ids:
 +                if line.product_id:
-                     acc = line.product_id.product_tmpl_id.property_account_expense
++                    acc = line.product_id.property_account_expense
                      if not acc:
 -                        acc = l.product_id.categ_id.property_account_expense_categ
 -                    tax_id = [x.id for x in l.product_id.supplier_taxes_id]
 +                        acc = line.product_id.categ_id.property_account_expense_categ
                  else:
                      acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context={'force_company': company_id})
                      if not acc:
Simple merge
@@@ -166,143 -150,6 +166,143 @@@ class account_analytic_line(osv.osv)
          return super(account_analytic_line, self).copy(cursor, user, obj_id,
                  default, context=context)
  
 +    def _get_invoice_price(self, cr, uid, account, product_id, user_id, qty, context = {}):
 +        pro_price_obj = self.pool.get('product.pricelist')
 +        if account.pricelist_id:
 +            pl = account.pricelist_id.id
 +            price = pro_price_obj.price_get(cr,uid,[pl], product_id, qty or 1.0, account.partner_id.id, context=context)[pl]
 +        else:
 +            price = 0.0
 +        return price
 +
 +    def invoice_cost_create(self, cr, uid, ids, data=None, context=None):
 +        analytic_account_obj = self.pool.get('account.analytic.account')
 +        account_payment_term_obj = self.pool.get('account.payment.term')
 +        invoice_obj = self.pool.get('account.invoice')
 +        product_obj = self.pool.get('product.product')
 +        invoice_factor_obj = self.pool.get('hr_timesheet_invoice.factor')
 +        fiscal_pos_obj = self.pool.get('account.fiscal.position')
 +        product_uom_obj = self.pool.get('product.uom')
 +        invoice_line_obj = self.pool.get('account.invoice.line')
 +        invoices = []
 +        if context is None:
 +            context = {}
 +        if data is None:
 +            data = {}
 +
 +        journal_types = {}
 +        for line in self.pool.get('account.analytic.line').browse(cr, uid, ids, context=context):
 +            if line.journal_id.type not in journal_types:
 +                journal_types[line.journal_id.type] = set()
 +            journal_types[line.journal_id.type].add(line.account_id.id)
 +        for journal_type, account_ids in journal_types.items():
 +            for account in analytic_account_obj.browse(cr, uid, list(account_ids), context=context):
 +                partner = account.partner_id
 +                if (not partner) or not (account.pricelist_id):
 +                    raise osv.except_osv(_('Analytic Account incomplete !'),
 +                            _('Contract incomplete. Please fill in the Customer and Pricelist fields.'))
 +
 +                date_due = False
 +                if partner.property_payment_term:
 +                    pterm_list= account_payment_term_obj.compute(cr, uid,
 +                            partner.property_payment_term.id, value=1,
 +                            date_ref=time.strftime('%Y-%m-%d'))
 +                    if pterm_list:
 +                        pterm_list = [line[0] for line in pterm_list]
 +                        pterm_list.sort()
 +                        date_due = pterm_list[-1]
 +
 +                curr_invoice = {
 +                    'name': time.strftime('%d/%m/%Y') + ' - '+account.name,
 +                    'partner_id': account.partner_id.id,
 +                    'company_id': account.company_id.id,
 +                    'payment_term': partner.property_payment_term.id or False,
 +                    'account_id': partner.property_account_receivable.id,
 +                    'currency_id': account.pricelist_id.currency_id.id,
 +                    'date_due': date_due,
 +                    'fiscal_position': account.partner_id.property_account_position.id
 +                }
 +
 +                context2 = context.copy()
 +                context2['lang'] = partner.lang
 +                # set company_id in context, so the correct default journal will be selected
 +                context2['force_company'] = curr_invoice['company_id']
 +                # set force_company in context so the correct product properties are selected (eg. income account)
 +                context2['company_id'] = curr_invoice['company_id']
 +
 +                last_invoice = invoice_obj.create(cr, uid, curr_invoice, context=context2)
 +                invoices.append(last_invoice)
 +
 +                cr.execute("""SELECT product_id, user_id, to_invoice, sum(unit_amount), product_uom_id
 +                        FROM account_analytic_line as line LEFT JOIN account_analytic_journal journal ON (line.journal_id = journal.id)
 +                        WHERE account_id = %s
 +                            AND line.id IN %s AND journal.type = %s AND to_invoice IS NOT NULL
 +                        GROUP BY product_id, user_id, to_invoice, product_uom_id""", (account.id, tuple(ids), journal_type))
 +
 +                for product_id, user_id, factor_id, qty, uom in cr.fetchall():
 +                    if data.get('product'):
 +                        product_id = data['product'][0]
 +                    product = product_obj.browse(cr, uid, product_id, context=context2)
 +                    if not product:
 +                        raise osv.except_osv(_('Error!'), _('There is no product defined. Please select one or force the product through the wizard.'))
 +                    factor = invoice_factor_obj.browse(cr, uid, factor_id, context=context2)
 +                    factor_name = product_obj.name_get(cr, uid, [product_id], context=context2)[0][1]
 +                    if factor.customer_name:
 +                        factor_name += ' - ' + factor.customer_name
 +
 +                    ctx =  context.copy()
 +                    ctx.update({'uom':uom})
 +
 +                    price = self._get_invoice_price(cr, uid, account, product_id, user_id, qty, ctx)
 +
-                     general_account = product.product_tmpl_id.property_account_income or product.categ_id.property_account_income_categ
++                    general_account = product.property_account_income or product.categ_id.property_account_income_categ
 +                    if not general_account:
 +                        raise osv.except_osv(_("Configuration Error!"), _("Please define income account for product '%s'.") % product.name)
 +                    taxes = product.taxes_id or general_account.tax_ids
 +                    tax = fiscal_pos_obj.map_tax(cr, uid, account.partner_id.property_account_position, taxes)
 +                    curr_line = {
 +                        'price_unit': price,
 +                        'quantity': qty,
 +                        'discount':factor.factor,
 +                        'invoice_line_tax_id': [(6,0,tax )],
 +                        'invoice_id': last_invoice,
 +                        'name': factor_name,
 +                        'product_id': product_id,
 +                        'invoice_line_tax_id': [(6,0,tax)],
 +                        'uos_id': uom,
 +                        'account_id': general_account.id,
 +                        'account_analytic_id': account.id,
 +                    }
 +
 +                    #
 +                    # Compute for lines
 +                    #
 +                    cr.execute("SELECT * FROM account_analytic_line WHERE account_id = %s and id IN %s AND product_id=%s and to_invoice=%s ORDER BY account_analytic_line.date", (account.id, tuple(ids), product_id, factor_id))
 +
 +                    line_ids = cr.dictfetchall()
 +                    note = []
 +                    for line in line_ids:
 +                        # set invoice_line_note
 +                        details = []
 +                        if data.get('date', False):
 +                            details.append(line['date'])
 +                        if data.get('time', False):
 +                            if line['product_uom_id']:
 +                                details.append("%s %s" % (line['unit_amount'], product_uom_obj.browse(cr, uid, [line['product_uom_id']],context2)[0].name))
 +                            else:
 +                                details.append("%s" % (line['unit_amount'], ))
 +                        if data.get('name', False):
 +                            details.append(line['name'])
 +                        note.append(u' - '.join(map(lambda x: unicode(x) or '',details)))
 +
 +                    if note:
 +                        curr_line['name'] += "\n" + ("\n".join(map(lambda x: unicode(x) or '',note)))
 +                    invoice_line_obj.create(cr, uid, curr_line, context=context)
 +                    cr.execute("update account_analytic_line set invoice_id=%s WHERE account_id = %s and id IN %s", (last_invoice, account.id, tuple(ids)))
 +
 +                invoice_obj.button_reset_taxes(cr, uid, [last_invoice], context)
 +        return invoices
 +
  account_analytic_line()
  
  
@@@ -948,10 -957,11 +948,10 @@@ class mrp_production(osv.osv)
  
      def _make_production_produce_line(self, cr, uid, production, context=None):
          stock_move = self.pool.get('stock.move')
-         source_location_id = production.product_id.product_tmpl_id.property_stock_production.id
+         source_location_id = production.product_id.property_stock_production.id
          destination_location_id = production.location_dest_id.id
 -        move_name = _('PROD: %s') + production.name
          data = {
 -            'name': move_name,
 +            'name': production.name,
              'date': production.date_planned,
              'product_id': production.product_id.id,
              'product_qty': production.product_qty,
          # Internal shipment is created for Stockable and Consumer Products
          if production_line.product_id.type not in ('product', 'consu'):
              return False
-         destination_location_id = production.product_id.product_tmpl_id.property_stock_production.id
 -        move_name = _('PROD: %s') % production.name
+         destination_location_id = production.product_id.property_stock_production.id
          if not source_location_id:
              source_location_id = production.location_src_id.id
          move_id = stock_move.create(cr, uid, {
Simple merge
Simple merge
index 625f039,0000000..22bbfac
mode 100644,000000..100644
--- /dev/null
@@@ -1,165 -1,0 +1,165 @@@
 +# -*- coding: utf-8 -*-
 +##############################################################################
 +#
 +#    OpenERP, Open Source Management Solution
 +#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
 +#
 +#    This program is free software: you can redistribute it and/or modify
 +#    it under the terms of the GNU Affero General Public License as
 +#    published by the Free Software Foundation, either version 3 of the
 +#    License, or (at your option) any later version.
 +#
 +#    This program is distributed in the hope that it will be useful,
 +#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +#    GNU Affero General Public License for more details.
 +#
 +#    You should have received a copy of the GNU Affero General Public License
 +#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 +#
 +##############################################################################
 +
 +from openerp.osv import fields
 +from openerp.osv import osv
 +import openerp.addons.decimal_precision as dp
 +from openerp.tools.translate import _
 +
 +class mrp_subproduct(osv.osv):
 +    _name = 'mrp.subproduct'
 +    _description = 'Byproduct'
 +    _columns={
 +        'product_id': fields.many2one('product.product', 'Product', required=True),
 +        'product_qty': fields.float('Product Qty', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
 +        'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
 +        'subproduct_type': fields.selection([('fixed','Fixed'),('variable','Variable')], 'Quantity Type', required=True, help="Define how the quantity of byproducts will be set on the production orders using this BoM.\
 +  'Fixed' depicts a situation where the quantity of created byproduct is always equal to the quantity set on the BoM, regardless of how many are created in the production order.\
 +  By opposition, 'Variable' means that the quantity will be computed as\
 +    '(quantity of byproduct set on the BoM / quantity of manufactured product set on the BoM * quantity of manufactured product in the production order.)'"),
 +        'bom_id': fields.many2one('mrp.bom', 'BoM'),
 +    }
 +    _defaults={
 +        'subproduct_type': 'variable',
 +        'product_qty': lambda *a: 1.0,
 +    }
 +
 +    def onchange_product_id(self, cr, uid, ids, product_id, context=None):
 +        """ Changes UoM if product_id changes.
 +        @param product_id: Changed product_id
 +        @return: Dictionary of changed values
 +        """
 +        if product_id:
 +            prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
 +            v = {'product_uom': prod.uom_id.id}
 +            return {'value': v}
 +        return {}
 +
 +    def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
 +        res = {'value':{}}
 +        if not product_uom or not product_id:
 +            return res
 +        product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
 +        uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
 +        if uom.category_id.id != product.uom_id.category_id.id:
 +            res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
 +            res['value'].update({'product_uom': product.uom_id.id})
 +        return res
 +
 +mrp_subproduct()
 +
 +class mrp_bom(osv.osv):
 +    _name = 'mrp.bom'
 +    _description = 'Bill of Material'
 +    _inherit='mrp.bom'
 +
 +    _columns={
 +        'sub_products':fields.one2many('mrp.subproduct', 'bom_id', 'Byproducts'),
 +    }
 +
 +mrp_bom()
 +
 +class mrp_production(osv.osv):
 +    _description = 'Production'
 +    _inherit= 'mrp.production'
 +
 +
 +    def action_confirm(self, cr, uid, ids):
 +        """ Confirms production order and calculates quantity based on subproduct_type.
 +        @return: Newly generated picking Id.
 +        """
 +        picking_id = super(mrp_production,self).action_confirm(cr, uid, ids)
 +        product_uom_obj = self.pool.get('product.uom')
 +        for production in self.browse(cr, uid, ids):
-             source = production.product_id.product_tmpl_id.property_stock_production.id
++            source = production.product_id.property_stock_production.id
 +            if not production.bom_id:
 +                continue
 +            for sub_product in production.bom_id.sub_products:
 +                product_uom_factor = product_uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.bom_id.product_uom.id)
 +                qty1 = sub_product.product_qty
 +                qty2 = production.product_uos and production.product_uos_qty or False
 +                product_uos_factor = 0.0
 +                if qty2 and production.bom_id.product_uos.id:
 +                    product_uos_factor = product_uom_obj._compute_qty(cr, uid, production.product_uos.id, production.product_uos_qty, production.bom_id.product_uos.id)
 +                if sub_product.subproduct_type == 'variable':
 +                    if production.product_qty:
 +                        qty1 *= product_uom_factor / (production.bom_id.product_qty or 1.0)
 +                    if production.product_uos_qty:
 +                        qty2 *= product_uos_factor / (production.bom_id.product_uos_qty or 1.0)
 +                data = {
 +                    'name': 'PROD:'+production.name,
 +                    'date': production.date_planned,
 +                    'product_id': sub_product.product_id.id,
 +                    'product_qty': qty1,
 +                    'product_uom': sub_product.product_uom.id,
 +                    'product_uos_qty': qty2,
 +                    'product_uos': production.product_uos and production.product_uos.id or False,
 +                    'location_id': source,
 +                    'location_dest_id': production.location_dest_id.id,
 +                    'move_dest_id': production.move_prod_id.id,
 +                    'state': 'waiting',
 +                    'production_id': production.id
 +                }
 +                self.pool.get('stock.move').create(cr, uid, data)
 +        return picking_id
 +
 +    def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
 +        """Compute the factor to compute the qty of procucts to produce for the given production_id. By default, 
 +            it's always equal to the quantity encoded in the production order or the production wizard, but with 
 +            the module mrp_byproduct installed it can differ for byproducts having type 'variable'.
 +        :param production_id: ID of the mrp.order
 +        :param move_id: ID of the stock move that needs to be produced. Identify the product to produce.
 +        :return: The factor to apply to the quantity that we should produce for the given production order and stock move.
 +        """
 +        sub_obj = self.pool.get('mrp.subproduct')
 +        move_obj = self.pool.get('stock.move')
 +        production_obj = self.pool.get('mrp.production')
 +        production_browse = production_obj.browse(cr, uid, production_id, context=context)
 +        move_browse = move_obj.browse(cr, uid, move_id, context=context)
 +        subproduct_factor = 1
 +        sub_id = sub_obj.search(cr, uid,[('product_id', '=', move_browse.product_id.id),('bom_id', '=', production_browse.bom_id.id), ('subproduct_type', '=', 'variable')], context=context)
 +        if sub_id:
 +            subproduct_record = sub_obj.browse(cr ,uid, sub_id[0], context=context)
 +            if subproduct_record.bom_id.product_qty:
 +                subproduct_factor = subproduct_record.product_qty / subproduct_record.bom_id.product_qty
 +                return subproduct_factor
 +        return super(mrp_production, self)._get_subproduct_factor(cr, uid, production_id, move_id, context=context)
 +
 +mrp_production()
 +
 +class change_production_qty(osv.osv_memory):
 +    _inherit = 'change.production.qty'
 +
 +    def _update_product_to_produce(self, cr, uid, prod, qty, context=None):
 +        bom_obj = self.pool.get('mrp.bom')
 +        move_lines_obj = self.pool.get('stock.move')
 +        prod_obj = self.pool.get('mrp.production')
 +        for m in prod.move_created_ids:
 +            if m.product_id.id == prod.product_id.id:
 +                move_lines_obj.write(cr, uid, [m.id], {'product_qty': qty})
 +            else:
 +                for sub_product_line in prod.bom_id.sub_products:
 +                    if sub_product_line.product_id.id == m.product_id.id:
 +                        factor = prod_obj._get_subproduct_factor(cr, uid, prod.id, m.id, context=context)
 +                        subproduct_qty = sub_product_line.subproduct_type == 'variable' and qty * factor or sub_product_line.product_qty
 +                        move_lines_obj.write(cr, uid, [m.id], {'product_qty': subproduct_qty})
 +
 +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
Simple merge
@@@ -90,18 -86,17 +90,18 @@@ class project_work(osv.osv)
  
          if not emp.journal_id:
              raise osv.except_osv(_('Bad Configuration !'),
 -                 _('No journal defined on the related employee.\nFill in the timesheet tab of the employee form.'))
 +                 _('Please define journal on the related employee.\nFill in the timesheet tab of the employee form.'))
  
-         acc_id = emp.product_id.product_tmpl_id.property_account_expense.id
 -        a = emp.product_id.property_account_expense.id
 -        if not a:
 -            a = emp.product_id.categ_id.property_account_expense_categ.id
 -            if not a:
++        acc_id = emp.product_id.property_account_expense.id
 +        if not acc_id:
 +            acc_id = emp.product_id.categ_id.property_account_expense_categ.id
 +            if not acc_id:
                  raise osv.except_osv(_('Bad Configuration !'),
 -                        _('No product and product category property account defined on the related employee.\nFill in the timesheet tab of the employee form.'))
 +                        _('Please define product and product category property account on the related employee.\nFill in the timesheet tab of the employee form.'))
 +
          res['product_id'] = emp.product_id.id
          res['journal_id'] = emp.journal_id.id
 -        res['general_account_id'] = a
 +        res['general_account_id'] = acc_id
          res['product_uom_id'] = emp.product_id.uom_id.id
          return res
  
@@@ -563,11 -481,11 +563,11 @@@ class purchase_order(osv.osv)
          self.write(cr, uid, ids, {'state':'approved'}, context=context)
          self.invoice_done_send_note(cr, uid, ids, context=context)
          return True
 -        
 -    def has_stockable_product(self,cr, uid, ids, *args):
 +
 +    def has_stockable_product(self, cr, uid, ids, *args):
          for order in self.browse(cr, uid, ids):
              for order_line in order.order_line:
-                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
+                 if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
                      return True
          return False
  
@@@ -1041,44 -959,6 +1041,44 @@@ class procurement_order(osv.osv)
          'purchase_id': fields.many2one('purchase.order', 'Purchase Order'),
      }
  
 +    def check_buy(self, cr, uid, ids, context=None):
 +        ''' return True if the supply method of the mto product is 'buy'
 +        '''
 +        user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
 +        for procurement in self.browse(cr, uid, ids, context=context):
-             if procurement.product_id.product_tmpl_id.supply_method <> 'buy':
++            if procurement.product_id.supply_method <> 'buy':
 +                return False
 +        return True
 +
 +    def check_supplier_info(self, cr, uid, ids, context=None):
 +        partner_obj = self.pool.get('res.partner')
 +        user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
 +        for procurement in self.browse(cr, uid, ids, context=context):
 +            if not procurement.product_id.seller_ids:
 +                message = _('No supplier defined for this product !')
 +                self.message_post(cr, uid, [procurement.id], body=message)
 +                cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
 +                return False
 +            partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
 +
 +            if not partner:
 +                message = _('No default supplier defined for this product')
 +                self.message_post(cr, uid, [procurement.id], body=message)
 +                cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
 +                return False
 +            if user.company_id and user.company_id.partner_id:
 +                if partner.id == user.company_id.partner_id.id:
 +                    raise osv.except_osv(_('Configuration Error!'), _('The product "%s" has been defined with your company as reseller which seems to be a configuration error!' % procurement.product_id.name))
 +
 +            address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
 +            if not address_id:
 +                message = _('No address defined for the supplier')
 +                self.message_post(cr, uid, [procurement.id], body=message)
 +                cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
 +                return False
 +        return True
 +
 +
      def action_po_assign(self, cr, uid, ids, context=None):
          """ This is action which call from workflow to assign purchase order to procurements
          @return: True
              purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
  
              #Passing partner_id to context for purchase order line integrity of Line name
 -            context.update({'lang': partner.lang, 'partner_id': partner_id})
 +            new_context = context.copy()
 +            new_context.update({'lang': partner.lang, 'partner_id': partner_id})
  
 -            product = prod_obj.browse(cr, uid, procurement.product_id.id, context=context)
 +            product = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context)
-             taxes_ids = procurement.product_id.product_tmpl_id.supplier_taxes_id
+             taxes_ids = procurement.product_id.supplier_taxes_id
              taxes = acc_pos_obj.map_tax(cr, uid, partner.property_account_position, taxes_ids)
  
              name = product.partner_ref
Simple merge
index 99967a4,0000000..21bf13a
mode 100644,000000..100644
--- /dev/null
@@@ -1,661 -1,0 +1,661 @@@
 +# -*- coding: utf-8 -*-
 +##############################################################################
 +#
 +#    OpenERP, Open Source Management Solution
 +#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
 +#
 +#    This program is free software: you can redistribute it and/or modify
 +#    it under the terms of the GNU Affero General Public License as
 +#    published by the Free Software Foundation, either version 3 of the
 +#    License, or (at your option) any later version.
 +#
 +#    This program is distributed in the hope that it will be useful,
 +
 +#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +#    GNU Affero General Public License for more details.
 +#
 +#    You should have received a copy of the GNU Affero General Public License
 +#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 +#
 +##############################################################################
 +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 import netsvc
 +from openerp.tools.translate import _
 +
 +class sale_shop(osv.osv):
 +    _inherit = "sale.shop"
 +    _columns = {
 +        'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
 +    }
 +
 +sale_shop()
 +
 +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 shipping_policy_change(self, cr, uid, ids, policy, context=None):
 +        if not policy:
 +            return {}
 +        inv_qty = 'order'
 +        if policy == 'prepaid':
 +            inv_qty = 'order'
 +        elif policy == 'picking':
 +            inv_qty = 'procurement'
 +        return {'value': {'invoice_quantity': inv_qty}}
 +
 +    def write(self, cr, uid, ids, vals, context=None):
 +        if vals.get('order_policy', False):
 +            if vals['order_policy'] == 'prepaid':
 +                vals.update({'invoice_quantity': 'order'})
 +            elif vals['order_policy'] == 'picking':
 +                vals.update({'invoice_quantity': 'procurement'})
 +        return super(sale_order, self).write(cr, uid, ids, vals, context=context)
 +
 +    def create(self, cr, uid, vals, context=None):
 +        if vals.get('order_policy', False):
 +            if vals['order_policy'] == 'prepaid':
 +                vals.update({'invoice_quantity': 'order'})
 +            if vals['order_policy'] == 'picking':
 +                vals.update({'invoice_quantity': 'procurement'})
 +        order =  super(sale_order, self).create(cr, uid, vals, context=context)
 +        return order
 +
 +    # This is False
 +    def _picked_rate(self, cr, uid, ids, name, arg, context=None):
 +        if not ids:
 +            return {}
 +        res = {}
 +        tmp = {}
 +        for id in ids:
 +            tmp[id] = {'picked': 0.0, 'total': 0.0}
 +        cr.execute('''SELECT
 +                p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
 +            FROM
 +                stock_move m
 +            LEFT JOIN
 +                stock_picking p on (p.id=m.picking_id)
 +            LEFT JOIN
 +                procurement_order mp on (mp.move_id=m.id)
 +            WHERE
 +                p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
 +
 +        for item in cr.dictfetchall():
 +            if item['move_state'] == 'cancel':
 +                continue
 +
 +            if item['picking_type'] == 'in':#this is a returned picking
 +                tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
 +                if item['procurement_state'] == 'done' or item['move_state'] == 'done':
 +                    tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
 +            else:
 +                tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
 +                if item['procurement_state'] == 'done' or item['move_state'] == 'done':
 +                    tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
 +
 +        for order in self.browse(cr, uid, ids, context=context):
 +            if order.shipped:
 +                res[order.id] = 100.0
 +            else:
 +                res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
 +        return res
 +    
 +    _columns = {
 +          'state': fields.selection([
 +            ('draft', 'Draft Quotation'),
 +            ('sent', 'Quotation Sent'),
 +            ('cancel', 'Cancelled'),
 +            ('waiting_date', 'Waiting Schedule'),
 +            ('progress', 'Sale Order'),
 +            ('manual', 'Sale to Invoice'),
 +            ('shipping_except', 'Shipping Exception'),
 +            ('invoice_except', 'Invoice Exception'),
 +            ('done', 'Done'),
 +            ], 'Status', readonly=True,help="Gives the status of the quotation or sales order.\
 +              \nThe exception status is automatically set when a cancel operation occurs \
 +              in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
 +               but waiting for the scheduler to run on the order date.", select=True),
 +        'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
 +        'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
 +            'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
 +            help="""Pick 'Deliver each product when available' if you allow partial delivery."""),
 +        'order_policy': fields.selection([
 +                ('manual', 'On Demand'),
 +                ('picking', 'On Delivery Order'),
 +                ('prepaid', 'Before Delivery'),
 +            ], '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."""),
 +        'picking_ids': fields.one2many('stock.picking.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders that has been generated for this sales order."),
 +        'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
 +        'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
 +        'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', 
 +                                             help="The sale order will automatically create the invoice proposition (draft invoice).\
 +                                              You have to choose  if you want your invoice based on ordered ", required=True, readonly=True, states={'draft': [('readonly', False)]}),
 +    }
 +    _defaults = {
 +             'picking_policy': 'direct',
 +             'order_policy': 'manual',
 +             'invoice_quantity': 'order',
 +         }
 +
 +    # Form filling
 +    def unlink(self, cr, uid, ids, context=None):
 +        sale_orders = self.read(cr, uid, ids, ['state'], context=context)
 +        unlink_ids = []
 +        for s in sale_orders:
 +            if s['state'] in ['draft', 'cancel']:
 +                unlink_ids.append(s['id'])
 +            else:
 +                raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it.\nTo do so, you must first cancel related picking for delivery orders.'))
 +
 +        return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
 +
 +    def action_view_delivery(self, cr, uid, ids, context=None):
 +        '''
 +        This function returns an action that display existing delivery orders of given sale 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')
 +        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 = []
 +        for so in self.browse(cr, uid, ids, context=context):
 +            pick_ids += [picking.id for picking in so.picking_ids]
 +        #choose the view_mode accordingly
 +        if len(pick_ids) > 1:
 +            result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
 +        else:
 +            res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
 +            result['views'] = [(res and res[1] or False, 'form')]
 +            result['res_id'] = pick_ids and pick_ids[0] or False
 +        return result
 +
 +    def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
 +        picking_obj = self.pool.get('stock.picking')
 +        res = super(sale_order,self).action_invoice_create( cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context)
 +        for order in self.browse(cr, uid, ids, context=context):
 +            if order.order_policy == 'picking':
 +                picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
 +        return res
 +
 +    def action_cancel(self, cr, uid, ids, context=None):
 +        wf_service = netsvc.LocalService("workflow")
 +        if context is None:
 +            context = {}
 +        sale_order_line_obj = self.pool.get('sale.order.line')
 +        proc_obj = self.pool.get('procurement.order')
 +        for sale in self.browse(cr, uid, ids, context=context):
 +            for pick in sale.picking_ids:
 +                if pick.state not in ('draft', 'cancel'):
 +                    raise osv.except_osv(
 +                        _('Cannot cancel sales order!'),
 +                        _('You must first cancel all delivery order(s) attached to this sales order.'))
 +                if pick.state == 'cancel':
 +                    for mov in pick.move_lines:
 +                        proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
 +                        if proc_ids:
 +                            for proc in proc_ids:
 +                                wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
 +            for r in self.read(cr, uid, ids, ['picking_ids']):
 +                for pick in r['picking_ids']:
 +                    wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
 +        return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
 +
 +    def action_wait(self, cr, uid, ids, context=None):
 +        res = super(sale_order, self).action_wait(cr, uid, ids, context=context)
 +        for o in self.browse(cr, uid, ids):
 +            noprod = self.test_no_product(cr, uid, o, context)
 +            if noprod and o.order_policy=='picking':
 +                self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
 +        return res
 +
 +    def procurement_lines_get(self, cr, uid, ids, *args):
 +        res = []
 +        for order in self.browse(cr, uid, ids, context={}):
 +            for line in order.order_line:
 +                if line.procurement_id:
 +                    res.append(line.procurement_id.id)
 +        return res
 +
 +    # if mode == 'finished':
 +    #   returns True if all lines are done, False otherwise
 +    # if mode == 'canceled':
 +    #   returns True if there is at least one canceled line, False otherwise
 +    def test_state(self, cr, uid, ids, mode, *args):
 +        assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
 +        finished = True
 +        canceled = False
 +        write_done_ids = []
 +        write_cancel_ids = []
 +        for order in self.browse(cr, uid, ids, context={}):
 +            for line in order.order_line:
 +                if (not line.procurement_id) or (line.procurement_id.state=='done'):
 +                    if line.state != 'done':
 +                        write_done_ids.append(line.id)
 +                else:
 +                    finished = False
 +                if line.procurement_id:
 +                    if (line.procurement_id.state == 'cancel'):
 +                        canceled = True
 +                        if line.state != 'exception':
 +                            write_cancel_ids.append(line.id)
 +        if write_done_ids:
 +            self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
 +        if write_cancel_ids:
 +            self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
 +
 +        if mode == 'finished':
 +            return finished
 +        elif mode == 'canceled':
 +            return canceled
 +
 +    def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
 +        return {
 +            'name': line.name,
 +            'origin': order.name,
 +            'date_planned': date_planned,
 +            'product_id': line.product_id.id,
 +            'product_qty': line.product_uom_qty,
 +            'product_uom': line.product_uom.id,
 +            'product_uos_qty': (line.product_uos and line.product_uos_qty)\
 +                    or line.product_uom_qty,
 +            'product_uos': (line.product_uos and line.product_uos.id)\
 +                    or line.product_uom.id,
 +            'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
 +            'procure_method': line.type,
 +            'move_id': move_id,
 +            'company_id': order.company_id.id,
 +            'note': line.name,
 +        }
 +
 +    def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
 +        location_id = order.shop_id.warehouse_id.lot_stock_id.id
 +        output_id = order.shop_id.warehouse_id.lot_output_id.id
 +        return {
 +            'name': line.name,
 +            'picking_id': picking_id,
 +            'product_id': line.product_id.id,
 +            'date': date_planned,
 +            'date_expected': date_planned,
 +            'product_qty': line.product_uom_qty,
 +            'product_uom': line.product_uom.id,
 +            'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
 +            'product_uos': (line.product_uos and line.product_uos.id)\
 +                    or line.product_uom.id,
 +            'product_packaging': line.product_packaging.id,
 +            'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
 +            'location_id': location_id,
 +            'location_dest_id': output_id,
 +            'sale_line_id': line.id,
 +            'tracking_id': False,
 +            'state': 'draft',
 +            #'state': 'waiting',
 +            'company_id': order.company_id.id,
 +            'price_unit': line.product_id.standard_price or 0.0
 +        }
 +
 +    def _prepare_order_picking(self, cr, uid, order, context=None):
 +        pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
 +        return {
 +            'name': pick_name,
 +            'origin': order.name,
 +            'date': order.date_order,
 +            'type': 'out',
 +            'state': 'auto',
 +            'move_type': order.picking_policy,
 +            'sale_id': order.id,
 +            'partner_id': order.partner_shipping_id.id,
 +            'note': order.note,
 +            'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
 +            'company_id': order.company_id.id,
 +        }
 +
 +    def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
 +        # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
 +        """
 +        Define ship_recreate for process after shipping exception
 +        param order: sale order to which the order lines belong
 +        param line: sale order line records to procure
 +        param move_id: the ID of stock move
 +        param proc_id: the ID of procurement
 +        """
 +        move_obj = self.pool.get('stock.move')
 +        if order.state == 'shipping_except':
 +            for pick in order.picking_ids:
 +                for move in pick.move_lines:
 +                    if move.state == 'cancel':
 +                        mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
 +                        if mov_ids:
 +                            for mov in move_obj.browse(cr, uid, mov_ids):
 +                                # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
 +                                move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
 +                                self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
 +        return True
 +
 +    def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
 +        date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
 +        date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
 +        return date_planned
 +
 +    def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
 +        """Create the required procurements to supply sale order lines, also connecting
 +        the procurements to appropriate stock moves in order to bring the goods to the
 +        sale order's requested location.
 +
 +        If ``picking_id`` is provided, the stock moves will be added to it, otherwise
 +        a standard outgoing picking will be created to wrap the stock moves, as returned
 +        by :meth:`~._prepare_order_picking`.
 +
 +        Modules that wish to customize the procurements or partition the stock moves over
 +        multiple stock pickings may override this method and call ``super()`` with
 +        different subsets of ``order_lines`` and/or preset ``picking_id`` values.
 +
 +        :param browse_record order: sale order to which the order lines belong
 +        :param list(browse_record) order_lines: sale order line records to procure
 +        :param int picking_id: optional ID of a stock picking to which the created stock moves
 +                               will be added. A new picking will be created if ommitted.
 +        :return: True
 +        """
 +        move_obj = self.pool.get('stock.move')
 +        picking_obj = self.pool.get('stock.picking')
 +        procurement_obj = self.pool.get('procurement.order')
 +        proc_ids = []
 +
 +        for line in order_lines:
 +            if line.state == 'done':
 +                continue
 +
 +            date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
 +
 +            if line.product_id:
-                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
++                if line.product_id.type in ('product', 'consu'):
 +                    if not picking_id:
 +                        picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
 +                    move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
 +                else:
 +                    # a service has no stock move
 +                    move_id = False
 +
 +                proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
 +                proc_ids.append(proc_id)
 +                line.write({'procurement_id': proc_id})
 +                self.ship_recreate(cr, uid, order, line, move_id, proc_id)
 +
 +        wf_service = netsvc.LocalService("workflow")
 +        if picking_id:
 +            wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
 +            self.delivery_send_note(cr, uid, [order.id], picking_id, context)
 +
 +
 +        for proc_id in proc_ids:
 +            wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
 +
 +        val = {}
 +        if order.state == 'shipping_except':
 +            val['state'] = 'progress'
 +            val['shipped'] = False
 +
 +            if (order.order_policy == 'manual'):
 +                for line in order.order_line:
 +                    if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
 +                        val['state'] = 'manual'
 +                        break
 +        order.write(val)
 +        return True
 +
 +    def action_ship_create(self, cr, uid, ids, context=None):
 +        for order in self.browse(cr, uid, ids, context=context):
 +            self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
 +        return True
 +
 +    def action_ship_end(self, cr, uid, ids, context=None):
 +        for order in self.browse(cr, uid, ids, context=context):
 +            val = {'shipped': True}
 +            if order.state == 'shipping_except':
 +                val['state'] = 'progress'
 +                if (order.order_policy == 'manual'):
 +                    for line in order.order_line:
 +                        if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
 +                            val['state'] = 'manual'
 +                            break
 +            for line in order.order_line:
 +                towrite = []
 +                if line.state == 'exception':
 +                    towrite.append(line.id)
 +                if towrite:
 +                    self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
 +            res = self.write(cr, uid, [order.id], val)
 +            if res:
 +                self.delivery_end_send_note(cr, uid, [order.id], context=context)
 +        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:
-                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
++                if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
 +                    return True
 +        return False
 +
 +    # ------------------------------------------------
 +    # OpenChatter methods and notifications
 +    # ------------------------------------------------
 +    
 +    def get_needaction_user_ids(self, cr, uid, ids, context=None):
 +        result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
 +        return result
 +
 +    def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
 +        for order in self.browse(cr, uid, ids, context=context):
 +            for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
 +                # convert datetime field to a datetime, using server format, then
 +                # convert it to the user TZ and re-render it with %Z to add the timezone
 +                picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
 +                picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
 +                self.message_post(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
 +
 +    def delivery_end_send_note(self, cr, uid, ids, context=None):
 +        self.message_post(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
 +
 +class sale_order_line(osv.osv):
 +
 +    def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
 +        res = {}
 +        for line in self.browse(cr, uid, ids, context=context):
 +            try:
 +                res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
 +            except:
 +                res[line.id] = 1
 +        return res
 +
 +    _inherit = 'sale.order.line'
 +    _columns = { 
 +        'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
 +        'procurement_id': fields.many2one('procurement.order', 'Procurement'),
 +        'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
 +        'product_packaging': fields.many2one('product.packaging', 'Packaging'),
 +        'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
 +        'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
 +    }
 +    _defaults = {
 +        'delay': 0.0,
 +        'product_packaging': False,
 +    }
 +
 +    def _get_line_qty(self, cr, uid, line, context=None):
 +        if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
 +            return self.pool.get('procurement.order').quantity_get(cr, uid,
 +                   line.procurement_id.id, context=context)
 +        else:
 +            return super(sale_order_line, self)._get_line_qty(cr, uid, line, context=context)
 +
 +
 +    def _get_line_uom(self, cr, uid, line, context=None):
 +        if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
 +            return self.pool.get('procurement.order').uom_get(cr, uid,
 +                    line.procurement_id.id, context=context)
 +        else:
 +            return super(sale_order_line, self)._get_line_uom(cr, uid, line, context=context)
 +
 +    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:
 +            return {'value': {'product_packaging': False}}
 +        product_obj = self.pool.get('product.product')
 +        product_uom_obj = self.pool.get('product.uom')
 +        pack_obj = self.pool.get('product.packaging')
 +        warning = {}
 +        result = {}
 +        warning_msgs = ''
 +        if flag:
 +            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']
 +
 +        products = product_obj.browse(cr, uid, product, context=context)
 +        if not products.packaging:
 +            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
 +            pack = pack_obj.browse(cr, uid, packaging, context=context)
 +            q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
 +#            qty = qty - qty % q + q
 +            if qty and (q and not (qty % q) == 0):
 +                ean = pack.ean or _('(n/a)')
 +                qty_pack = pack.qty
 +                type_ul = pack.ul
 +                if not warning_msgs:
 +                    warn_msg = _("You selected a quantity of %d Units.\n"
 +                                "But it's not compatible with the selected packaging.\n"
 +                                "Here is a proposition of quantities according to the packaging:\n"
 +                                "EAN: %s Quantity: %s Type of ul: %s") % \
 +                                    (qty, ean, qty_pack, type_ul.name)
 +                    warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
 +                warning = {
 +                       'title': _('Configuration Error!'),
 +                       'message': warning_msgs
 +                }
 +            result['product_uom_qty'] = qty
 +
 +        return {'value': result, 'warning': warning}
 +
 +    def product_id_change(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):
 +        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')
 +        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,
 +            lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
 +
 +        if not product:
 +            res['value'].update({'product_packaging': False})
 +            return res
 +
 +        #update of result obtained in super function
 +        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 ''
 +        product_obj = product_obj.browse(cr, uid, product, context=context)
 +        res['value']['delay'] = (product_obj.sale_delay or 0.0)
 +        res['value']['type'] = product_obj.procure_method
 +
 +        #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
 +        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'):
 +            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"
 +
 +        #update of warning messages
 +        if warning_msgs:
 +            warning = {
 +                       'title': _('Configuration Error!'),
 +                       'message' : warning_msgs
 +                    }
 +        res.update({'warning': warning})
 +        return res
 +
 +
 +class sale_advance_payment_inv(osv.osv_memory):
 +    _inherit = "sale.advance.payment.inv"
 +
 +    def _create_invoices(self, cr, uid, inv_values, sale_id, context=None):
 +        result = super(sale_advance_payment_inv, self)._create_invoices(cr, uid, inv_values, sale_id, context=context)
 +        sale_obj = self.pool.get('sale.order')
 +        sale_line_obj = self.pool.get('sale.order.line')
 +        wizard = self.browse(cr, uid, [result], context)
 +        sale = sale_obj.browse(cr, uid, sale_id, context=context)
 +        if sale.order_policy == 'postpaid':
 +            raise osv.except_osv(
 +                _('Error!'),
 +                _("You cannot make an advance on a sales order \
 +                     that is defined as 'Automatic Invoice after delivery'."))
 +
 +        # If invoice on picking: add the cost on the SO
 +        # If not, the advance will be deduced when generating the final invoice
 +        line_name = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('name') or ''
 +        line_tax = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('invoice_line_tax_id') or False
 +        if sale.order_policy == 'picking':
 +            vals = {
 +                'order_id': sale.id,
 +                'name': line_name,
 +                'price_unit': -inv_amount,
 +                'product_uom_qty': wizard.qtty or 1.0,
 +                'product_uos_qty': wizard.qtty or 1.0,
 +                'product_uos': res.get('uos_id', False),
 +                'product_uom': res.get('uom_id', False),
 +                'product_id': wizard.product_id.id or False,
 +                'discount': False,
 +                'tax_id': line_tax,
 +            }
 +            sale_line_obj.create(cr, uid, vals, context=context)
 +        return result
index af24bd2,0000000..800c131
mode 100644,000000..100644
--- /dev/null
@@@ -1,169 -1,0 +1,169 @@@
 +-
 +  In order to test process of the Sale Order,
 +-
 +  First I check the total amount of the Quotation before Approved.
 +-
 +  !assert {model: sale.order, id: sale.sale_order_6, string: The amount of the Quotation is not correctly computed}:
 +    - sum([l.price_subtotal for l in order_line]) == amount_untaxed
 +-
 +  I confirm the quotation with Invoice based on deliveries policy.
 +-
 +  !workflow {model: sale.order, action: order_confirm, ref: sale.sale_order_6}
 +-
 +  I check that invoice should not created before dispatch delivery.
 +-
 +  !python {model: sale.order}: |
 +    order = self.pool.get('sale.order').browse(cr, uid, ref("sale.sale_order_6"))
 +    assert order.state == 'progress', 'Order should be in inprogress.'
 +    assert len(order.invoice_ids) == False, "Invoice should not created."
 +-
 +  I check the details of procurement after confirmed quotation.
 +-
 +  !python {model: sale.order}: |
 +    from datetime import datetime, timedelta
 +    from dateutil.relativedelta import relativedelta
 +    from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
 +    order = self.browse(cr, uid, ref("sale.sale_order_6"))
 +    for order_line in order.order_line:
 +        procurement = order_line.procurement_id
 +        date_planned = datetime.strptime(order.date_order, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=order_line.delay or 0.0)
 +        date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
 +        assert procurement.date_planned == date_planned, "Scheduled date is not correspond."
 +        assert procurement.product_id.id == order_line.product_id.id, "Product is not correspond."
 +        assert procurement.product_qty == order_line.product_uom_qty, "Qty is not correspond."
 +        assert procurement.product_uom.id == order_line.product_uom.id, "UOM is not correspond."
 +        assert procurement.procure_method == order_line.type, "Procurement method is not correspond."
 +-
 +  I run the scheduler.
 +-
 +  !python {model: procurement.order}: |
 +    self.run_scheduler(cr, uid)
 +-
 +  I check the details of delivery order after confirmed quotation.
 +-
 +  !python {model: sale.order}: |
 +    from datetime import datetime, timedelta
 +    from dateutil.relativedelta import relativedelta
 +    from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
 +    sale_order = self.browse(cr, uid, ref("sale.sale_order_6"))
 +    assert sale_order.picking_ids, "Delivery order is not created."
 +    for picking in sale_order.picking_ids:
 +      assert picking.state == "auto" or "confirmed", "Delivery order should be in 'Waitting Availability' state."
 +      assert picking.origin == sale_order.name,"Origin of Delivery order is not correspond with sequence number of sale order."
 +      assert picking.type == 'out',"Shipment should be Outgoing."
 +      assert picking.move_type == sale_order.picking_policy,"Delivery Method is not corresponding with delivery method of sale order."
 +      assert picking.partner_id.id == sale_order.partner_shipping_id.id,"Shipping Address is not correspond with sale order."
 +      assert picking.note == sale_order.note,"Note is not correspond with sale order."
 +      assert picking.invoice_state == (sale_order.order_policy=='picking' and '2binvoiced') or 'none',"Invoice policy is not correspond with sale order."
 +      assert len(picking.move_lines) == len(sale_order.order_line), "Total move of delivery order are not corresposning with total sale order lines."
 +      location_id = sale_order.shop_id.warehouse_id.lot_stock_id.id
 +      output_id = sale_order.shop_id.warehouse_id.lot_output_id.id
 +      for move in picking.move_lines:
 +         order_line = move.sale_line_id
 +         date_planned = datetime.strptime(sale_order.date_order, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=order_line.delay or 0.0)
 +         date_planned = (date_planned - timedelta(days=sale_order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
 +         assert datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) == datetime.strptime(date_planned, DEFAULT_SERVER_DATETIME_FORMAT), "Excepted Date is not correspond with Planned Date."
 +         assert move.product_id.id == order_line.product_id.id,"Product is not correspond."
 +         assert move.product_qty == order_line.product_uom_qty,"Product Quantity is not correspond."
 +         assert move.product_uom.id == order_line.product_uom.id,"Product UOM is not correspond."
 +         assert move.product_uos_qty == (order_line.product_uos and order_line.product_uos_qty) or order_line.product_uom_qty,"Product UOS Quantity is not correspond."
 +         assert move.product_uos == (order_line.product_uos and order_line.product_uos.id) or order_line.product_uom.id,"Product UOS is not correspond"
 +         assert move.product_packaging.id == order_line.product_packaging.id,"Product packaging is not correspond."
 +         assert move.partner_id.id == order_line.address_allotment_id.id or sale_order.partner_shipping_id.id,"Address is not correspond"
 +         #assert move.location_id.id == location_id,"Source Location is not correspond."
 +         #assert move.location_dest_id == output_id,"Destination Location is not correspond."
 +         assert move.price_unit == order_line.product_id.standard_price or 0.0,"Price Unit is not correspond"
 +-
 +  Now, I dispatch delivery order.
 +-
 +  !python {model: stock.partial.picking}: |
 +    order = self.pool.get('sale.order').browse(cr, uid, ref("sale.sale_order_6"))
 +    for pick in order.picking_ids:
 +        data = pick.force_assign()
 +        if data == True:
 +          partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick.id]})
 +          self.do_partial(cr, uid, [partial_id])
 +-
 +  I check sale order to verify shipment.
 +-
 +  !python {model: sale.order}: |
 +    order = self.pool.get('sale.order').browse(cr, uid, ref("sale.sale_order_6"))
 +    assert order.shipped == True, "Sale order is not Delivered."
 +    assert order.picked_rate == 100, "Shipment progress is not 100%."
 +    #assert order.state == 'progress', 'Order should be in inprogress.'
 +    assert len(order.invoice_ids) == False, "Invoice should not created on dispatch delivery order."
 +-
 +  I create Invoice from Delivery Order.
 +-
 +  !python {model: stock.invoice.onshipping}: |
 +    sale = self.pool.get('sale.order')
 +    sale_order = sale.browse(cr, uid, ref("sale.sale_order_6"))
 +    ship_ids = [x.id for x in sale_order.picking_ids]
 +    wiz_id = self.create(cr, uid, {'journal_id': ref('account.sales_journal')},
 +      {'active_ids': ship_ids, 'active_model': 'stock.picking'})
 +    self.create_invoice(cr, uid, [wiz_id], {"active_ids": ship_ids, "active_id": ship_ids[0]})
 +-
 +  I check the invoice details after dispatched delivery.
 +-
 +  !python {model: sale.order}: |
 +    order = self.browse(cr, uid, ref("sale.sale_order_6"))
 +    assert order.invoice_ids, "Invoice is not created."
 +    ac = order.partner_id.property_account_receivable.id
 +    journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)])
 +    for invoice in order.invoice_ids:
 +        assert invoice.type == 'out_invoice',"Invoice should be Customer Invoice."
 +        assert invoice.account_id.id == ac,"Invoice account is not correspond."
 +        assert invoice.reference == order.client_order_ref or order.name,"Reference is not correspond."
 +        assert invoice.partner_id.id == order.partner_id.id,"Customer is not correspond."
 +        assert invoice.currency_id.id == order.pricelist_id.currency_id.id, "Currency is not correspond."
 +        assert invoice.comment == (order.note or ''),"Note is not correspond."
 +        assert invoice.journal_id.id in journal_ids,"Sales Journal is not link on Invoice."
 +        assert invoice.payment_term.id == order.payment_term.id, "Payment term is not correspond."
 +    for so_line in order.order_line:
 +        inv_line = so_line.invoice_lines[0]
-         ac = so_line.product_id.product_tmpl_id.property_account_income.id or so_line.product_id.categ_id.property_account_income_categ.id
++        ac = so_line.product_id.property_account_income.id or so_line.product_id.categ_id.property_account_income_categ.id
 +        assert inv_line.product_id.id == so_line.product_id.id or False,"Product is not correspond"
 +        assert inv_line.account_id.id == ac,"Account of Invoice line is not corresponding."
 +        assert inv_line.uos_id.id == (so_line.product_uos and so_line.product_uos.id) or so_line.product_uom.id, "Product UOS is not correspond."
 +        assert inv_line.price_unit == so_line.price_unit , "Price Unit is not correspond."
 +        assert inv_line.quantity == (so_line.product_uos and so_line.product_uos_qty) or so_line.product_uom_qty , "Product qty is not correspond."
 +        assert inv_line.price_subtotal == so_line.price_subtotal, "Price sub total is not correspond."
 +-
 +  I open the Invoice.
 +-
 +  !python {model: sale.order}: |
 +    import netsvc
 +    wf_service = netsvc.LocalService("workflow")
 +    so = self.browse(cr, uid, ref("sale.sale_order_6"))
 +    for invoice in so.invoice_ids:
 +      wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
 +-
 +  I pay the invoice
 +-
 +  !python {model: account.invoice}: |
 +    sale_order = self.pool.get('sale.order')
 +    order = sale_order.browse(cr, uid, ref("sale.sale_order_6"))
 +    journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
 +    for invoice in order.invoice_ids:
 +        invoice.pay_and_reconcile(
 +            invoice.amount_total, ref('account.cash'), ref('account.period_8'),
 +            journal_ids[0], ref('account.cash'),
 +            ref('account.period_8'), journal_ids[0],
 +            name='test')
 +-
 +  I check the order after paid invoice.
 +-
 +  !python {model: sale.order}: |
 +    order = self.browse(cr, uid, ref("sale.sale_order_6"))
 +    assert order.invoiced == True, "Sale order is not invoiced."
 +    assert order.invoiced_rate == 100, "Invoiced progress is not 100%."
 +    assert order.state == 'done', 'Order should be in closed.'
 +-
 +  I print a sale order report.
 +-
 +  !python {model: sale.order}: |
 +    import netsvc, tools, os
 +    (data, format) = netsvc.LocalService('report.sale.order').create(cr, uid, [ref('sale.sale_order_6')], {}, {})
 +    if tools.config['test_report_directory']:
 +        file(os.path.join(tools.config['test_report_directory'], 'sale-sale_order.'+format), 'wb+').write(data)
 +
Simple merge
@@@ -2896,9 -2727,9 +2894,9 @@@ class stock_inventory(osv.osv)
                  change = line.product_qty - amount
                  lot_id = line.prod_lot_id.id
                  if change:
-                     location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
+                     location_id = line.product_id.property_stock_inventory.id
                      value = {
 -                        'name': 'INV:' + str(line.inventory_id.id) + ':' + line.inventory_id.name,
 +                        'name': _('INV:') + (line.inventory_id.name or ''),
                          'product_id': line.product_id.id,
                          'product_uom': line.product_uom.id,
                          'prodlot_id': lot_id,