[MERGE] forward port of branch 8.0 up to 2e092ac
authorChristophe Simonis <chs@odoo.com>
Wed, 3 Dec 2014 14:18:36 +0000 (15:18 +0100)
committerChristophe Simonis <chs@odoo.com>
Wed, 3 Dec 2014 14:18:36 +0000 (15:18 +0100)
28 files changed:
addons/account_voucher/account_voucher.py
addons/mail/wizard/mail_compose_message.py
addons/mrp/procurement.py
addons/mrp/stock.py
addons/procurement/procurement.py
addons/product/product.py
addons/product/product_view.xml
addons/purchase/stock.py
addons/stock/product.py
addons/stock/stock.py
addons/stock/wizard/stock_return_picking.py
addons/stock_account/stock_account.py
addons/web/static/src/js/pyeval.js
doc/howtos/backend.rst
doc/howtos/backend/exercise-basic-action
doc/howtos/backend/exercise-creation
doc/howtos/backend/exercise-dashboard
doc/howtos/backend/exercise-kanban
doc/howtos/themes.rst
doc/howtos/web.rst
doc/howtos/website.rst
doc/reference/views.rst
openerp/addons/base/ir/ir_actions.py
openerp/addons/base/tests/test_orm.py
openerp/http.py
openerp/models.py
openerp/service/server.py
setup/redhat/postinstall.sh

index 9724451..e56f4be 100644 (file)
@@ -884,11 +884,11 @@ class account_voucher(osv.osv):
         else:
             currency_id = journal.company_id.currency_id.id
 
-        period_id = self.pool['account.period'].find(cr, uid, context=dict(context, company_id=company_id))
+        period_ids = self.pool['account.period'].find(cr, uid, context=dict(context, company_id=company_id))
         vals['value'].update({
             'currency_id': currency_id,
             'payment_rate_currency_id': currency_id,
-            'period_id' : period_id
+            'period_id': period_ids and period_ids[0] or False
         })
         #in case we want to register the payment directly from an invoice, it's confusing to allow to switch the journal 
         #without seeing that the amount is expressed in the journal currency, and not in the invoice currency. So to avoid
index 04decd6..4fc7de7 100644 (file)
@@ -73,6 +73,9 @@ class mail_compose_message(osv.TransientModel):
         result['res_id'] = result.get('res_id', context.get('active_id'))
         result['parent_id'] = result.get('parent_id', context.get('message_id'))
 
+        if not result['model'] or not self.pool.get(result['model']) or not hasattr(self.pool[result['model']], 'message_post'):
+            result['no_auto_thread'] = True
+
         # default values according to composition mode - NOTE: reply is deprecated, fall back on comment
         if result['composition_mode'] == 'reply':
             result['composition_mode'] = 'comment'
index 0c17cd1..58727d9 100644 (file)
@@ -111,7 +111,6 @@ class procurement_order(osv.osv):
 
                 res[procurement.id] = produce_id
                 self.write(cr, uid, [procurement.id], {'production_id': produce_id})
-                procurement.refresh()
                 self.production_order_create_note(cr, uid, procurement, context=context)
                 production_obj.action_compute(cr, uid, [produce_id], properties=[x.id for x in procurement.property_ids])
                 production_obj.signal_workflow(cr, uid, [produce_id], 'button_confirm')
index a5f62b3..e22105e 100644 (file)
@@ -243,6 +243,10 @@ class stock_warehouse(osv.osv):
         'manufacture_pull_id': fields.many2one('procurement.rule', 'Manufacture Rule'),
     }
 
+    _defaults = {
+        'manufacture_to_resupply': True,
+    }
+
     def _get_manufacture_pull_rule(self, cr, uid, warehouse, context=None):
         route_obj = self.pool.get('stock.location.route')
         data_obj = self.pool.get('ir.model.data')
index 12e731e..ee7c502 100644 (file)
@@ -203,7 +203,6 @@ class procurement_order(osv.osv):
             if procurement.state not in ("running", "done"):
                 try:
                     if self._assign(cr, uid, procurement, context=context):
-                        procurement.refresh()
                         res = self._run(cr, uid, procurement, context=context or {})
                         if res:
                             self.write(cr, uid, [procurement.id], {'state': 'running'}, context=context)
index b5c99c8..589ff9e 100644 (file)
@@ -142,7 +142,7 @@ class product_uom(osv.osv):
             string='Bigger Ratio',
             help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
                     '1 * (this unit) = ratio * (reference unit)', required=True),
-        'rounding': fields.float('Rounding Precision', digits_compute=dp.get_precision('Product Unit of Measure'), required=True,
+        'rounding': fields.float('Rounding Precision', digits=0, required=True,
             help="The computed quantity will be a multiple of this value. "\
                  "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."),
         'active': fields.boolean('Active', help="By unchecking the active field you can disable a unit of measure without deleting it."),
@@ -156,13 +156,14 @@ class product_uom(osv.osv):
         'rounding': 0.01,
         'factor': 1,
         'uom_type': 'reference',
+        'factor': 1.0,
     }
 
     _sql_constraints = [
         ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!')
     ]
 
-    def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False, round=True):
+    def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False, round=True, rounding_method='UP'):
         if not from_uom_id or not qty or not to_uom_id:
             return qty
         uoms = self.browse(cr, uid, [from_uom_id, to_uom_id])
@@ -170,9 +171,9 @@ class product_uom(osv.osv):
             from_unit, to_unit = uoms[0], uoms[-1]
         else:
             from_unit, to_unit = uoms[-1], uoms[0]
-        return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit, round=round)
+        return self._compute_qty_obj(cr, uid, from_unit, qty, to_unit, round=round, rounding_method=rounding_method)
 
-    def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, round=True, context=None):
+    def _compute_qty_obj(self, cr, uid, from_unit, qty, to_unit, round=True, rounding_method='UP', context=None):
         if context is None:
             context = {}
         if from_unit.category_id.id != to_unit.category_id.id:
@@ -184,7 +185,7 @@ class product_uom(osv.osv):
         if to_unit:
             amount = amount * to_unit.factor
             if round:
-                amount = ceiling(amount, to_unit.rounding)
+                amount = float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method)
         return amount
 
     def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
index 0b5015d..f07769b 100644 (file)
             <field name="view_type">form</field>
             <field name="context">{'search_default_product_tmpl_id': [active_id], 'default_product_tmpl_id': active_id}</field>
             <field name="search_view_id" ref="product_search_form_view"/>
+            <field name="view_id" eval="False"/> <!-- Force empty -->
             <field name="help" type="html">
               <p class="oe_view_nocontent_create">
                 Click to define a new product.
                         </group>
                         <group>
                             <field name="active"/>
-                            <field name="rounding"/>
+                            <field name="rounding" digits="[42, 5]"/>
                         </group>
                     </group>
                 </form>
index d62cbb5..9257c8d 100644 (file)
@@ -100,7 +100,7 @@ class stock_move(osv.osv):
             # If partner given, search price in its purchase pricelist
             if partner and partner.property_product_pricelist_purchase:
                 pricelist_obj = self.pool.get("product.pricelist")
-                pricelist = partner.property_product_pricelist.id
+                pricelist = partner.property_product_pricelist_purchase.id
                 price = pricelist_obj.price_get(cr, uid, [pricelist],
                                     move.product_id.id, move.product_uom_qty, partner, {
                                                                                 'uom': move.product_uom.id,
index 41335b9..23f4d3f 100644 (file)
@@ -23,6 +23,7 @@ from openerp.osv import fields, osv
 from openerp.tools.translate import _
 from openerp.tools.safe_eval import safe_eval as eval
 import openerp.addons.decimal_precision as dp
+from openerp.tools.float_utils import float_round
 
 class product_product(osv.osv):
     _inherit = "product.product"
@@ -136,14 +137,18 @@ class product_product(osv.osv):
         moves_in = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_in))
         moves_out = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_out))
         res = {}
-        for id in ids:
+        for product in self.browse(cr, uid, ids, context=context):
+            id = product.id
+            qty_available = float_round(quants.get(id, 0.0), precision_rounding=product.uom_id.rounding)
+            incoming_qty = float_round(moves_in.get(id, 0.0), precision_rounding=product.uom_id.rounding)
+            outgoing_qty = float_round(moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding)
+            virtual_available = float_round(quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding)
             res[id] = {
-                'qty_available': quants.get(id, 0.0),
-                'incoming_qty': moves_in.get(id, 0.0),
-                'outgoing_qty': moves_out.get(id, 0.0),
-                'virtual_available': quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0),
+                'qty_available': qty_available,
+                'incoming_qty': incoming_qty,
+                'outgoing_qty': outgoing_qty,
+                'virtual_available': virtual_available,
             }
-
         return res
 
     def _search_product_quantity(self, cr, uid, obj, name, domain, context):
index bdc78e3..c6c53fd 100644 (file)
@@ -25,6 +25,7 @@ import json
 import time
 
 from openerp.osv import fields, osv
+from openerp.tools.float_utils import float_compare, float_round
 from openerp.tools.translate import _
 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
 from openerp import SUPERUSER_ID, api
@@ -377,9 +378,10 @@ class stock_quant(osv.osv):
             if move.picking_id:
                 self.pool.get('stock.picking').write(cr, uid, [move.picking_id.id], {'recompute_pack_op': True}, context=context)
         #check if move'state needs to be set as 'assigned'
-        if reserved_availability == move.product_qty and move.state in ('confirmed', 'waiting'):
+        rounding = move.product_id.uom_id.rounding
+        if float_compare(reserved_availability, move.product_qty, precision_rounding=rounding) == 0 and move.state in ('confirmed', 'waiting')  :
             self.pool.get('stock.move').write(cr, uid, [move.id], {'state': 'assigned'}, context=context)
-        elif reserved_availability > 0 and not move.partially_available:
+        elif float_compare(reserved_availability, 0, precision_rounding=rounding) > 0 and not move.partially_available:
             self.pool.get('stock.move').write(cr, uid, [move.id], {'partially_available': True}, context=context)
 
     def quants_move(self, cr, uid, quants, move, location_to, location_from=False, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, context=None):
@@ -402,7 +404,6 @@ class stock_quant(osv.osv):
                 quant = self._quant_create(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=location_from, force_location_to=location_to, context=context)
             else:
                 self._quant_split(cr, uid, quant, qty, context=context)
-                quant.refresh()
                 to_move_quants.append(quant)
             quants_reconcile.append(quant)
         if to_move_quants:
@@ -412,7 +413,6 @@ class stock_quant(osv.osv):
         if location_to.usage == 'internal':
             if self.search(cr, uid, [('product_id', '=', move.product_id.id), ('qty','<', 0)], limit=1, context=context):
                 for quant in quants_reconcile:
-                    quant.refresh()
                     self._quant_reconcile_negative(cr, uid, quant, move, context=context)
 
     def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
@@ -438,7 +438,8 @@ class stock_quant(osv.osv):
         if not prefered_domain_list:
             return self.quants_get(cr, uid, location, product, qty, domain=domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
         for prefered_domain in prefered_domain_list:
-            if res_qty > 0:
+            res_qty_cmp = float_compare(res_qty, 0, precision_rounding=product.uom_id.rounding)
+            if res_qty_cmp > 0:
                 #try to replace the last tuple (None, res_qty) with something that wasn't chosen at first because of the prefered order
                 quants.pop()
                 tmp_quants = self.quants_get(cr, uid, location, product, res_qty, domain=domain + prefered_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
@@ -485,10 +486,11 @@ class stock_quant(osv.osv):
             context = {}
         price_unit = self.pool.get('stock.move').get_price_unit(cr, uid, move, context=context)
         location = force_location_to or move.location_dest_id
+        rounding = move.product_id.uom_id.rounding
         vals = {
             'product_id': move.product_id.id,
             'location_id': location.id,
-            'qty': qty,
+            'qty': float_round(qty, precision_rounding=rounding),
             'cost': price_unit,
             'history_ids': [(4, move.id)],
             'in_date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -503,7 +505,7 @@ class stock_quant(osv.osv):
             #it means that a negative quant has to be created as well.
             negative_vals = vals.copy()
             negative_vals['location_id'] = force_location_from and force_location_from.id or move.location_id.id
-            negative_vals['qty'] = -qty
+            negative_vals['qty'] = float_round(-qty, precision_rounding=rounding)
             negative_vals['cost'] = price_unit
             negative_vals['negative_move_id'] = move.id
             negative_vals['package_id'] = src_package_id
@@ -516,11 +518,13 @@ class stock_quant(osv.osv):
 
     def _quant_split(self, cr, uid, quant, qty, context=None):
         context = context or {}
-        if (quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty):
+        rounding = quant.product_id.uom_id.rounding
+        if float_compare(abs(quant.qty), abs(qty), precision_rounding=rounding) <= 0: # if quant <= qty in abs, take it entirely
             return False
-        new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': quant.qty - qty}, context=context)
-        self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty}, context=context)
-        quant.refresh()
+        qty_round = float_round(qty, precision_rounding=rounding)
+        new_qty_round = float_round(quant.qty - qty, precision_rounding=rounding)
+        new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': new_qty_round, 'history_ids': [(4, x.id) for x in quant.history_ids]}, context=context)
+        self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty_round}, context=context)
         return self.browse(cr, uid, new_quant, context=context)
 
     def _get_latest_move(self, cr, uid, quant, context=None):
@@ -549,9 +553,11 @@ class stock_quant(osv.osv):
             dom += [('lot_id', '=', quant.lot_id.id)]
         dom += [('owner_id', '=', quant.owner_id.id)]
         dom += [('package_id', '=', quant.package_id.id)]
+        dom += [('id', '!=', quant.propagated_from_id.id)]
         quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, dom, context=context)
+        product_uom_rounding = quant.product_id.uom_id.rounding
         for quant_neg, qty in quants:
-            if not quant_neg:
+            if not quant_neg or not solving_quant:
                 continue
             to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
             if not to_solve_quant_ids:
@@ -559,7 +565,7 @@ class stock_quant(osv.osv):
             solving_qty = qty
             solved_quant_ids = []
             for to_solve_quant in self.browse(cr, uid, to_solve_quant_ids, context=context):
-                if solving_qty <= 0:
+                if float_compare(solving_qty, 0, precision_rounding=product_uom_rounding) <= 0:
                     continue
                 solved_quant_ids.append(to_solve_quant.id)
                 self._quant_split(cr, uid, to_solve_quant, min(solving_qty, to_solve_quant.qty), context=context)
@@ -571,12 +577,15 @@ class stock_quant(osv.osv):
                 remaining_to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id), ('id', 'not in', solved_quant_ids)], context=context)
                 if remaining_to_solve_quant_ids:
                     self.write(cr, SUPERUSER_ID, remaining_to_solve_quant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
+            if solving_quant.propagated_from_id and solved_quant_ids:
+                self.write(cr, uid, solved_quant_ids, {'propagated_from_id': solving_quant.propagated_from_id.id}, context=context)
             #delete the reconciled quants, as it is replaced by the solved quants
             self.unlink(cr, SUPERUSER_ID, [quant_neg.id], context=context)
-            #price update + accounting entries adjustments
-            self._price_update(cr, uid, solved_quant_ids, solving_quant.cost, context=context)
-            #merge history (and cost?)
-            self._quants_merge(cr, uid, solved_quant_ids, solving_quant, context=context)
+            if solved_quant_ids:
+                #price update + accounting entries adjustments
+                self._price_update(cr, uid, solved_quant_ids, solving_quant.cost, context=context)
+                #merge history (and cost?)
+                self._quants_merge(cr, uid, solved_quant_ids, solving_quant, context=context)
             self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context)
             solving_quant = remaining_solving_quant
 
@@ -607,16 +616,17 @@ class stock_quant(osv.osv):
             domain += [('company_id', '=', self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id)]
         res = []
         offset = 0
-        while quantity > 0:
+        while float_compare(quantity, 0, precision_rounding=product.uom_id.rounding) > 0:
             quants = self.search(cr, uid, domain, order=orderby, limit=10, offset=offset, context=context)
             if not quants:
                 res.append((None, quantity))
                 break
             for quant in self.browse(cr, uid, quants, context=context):
-                if quantity >= abs(quant.qty):
+                rounding = product.uom_id.rounding
+                if float_compare(quantity, abs(quant.qty), precision_rounding=rounding) >= 0:
                     res += [(quant, abs(quant.qty))]
                     quantity -= abs(quant.qty)
-                elif quantity != 0:
+                elif float_compare(quantity, 0.0, precision_rounding=rounding) != 0:
                     res += [(quant, quantity)]
                     quantity = 0
                     break
@@ -864,7 +874,6 @@ class stock_picking(osv.osv):
         for pick in self.browse(cr, uid, ids, context=context):
             if pick.state == 'draft':
                 self.action_confirm(cr, uid, [pick.id], context=context)
-            pick.refresh()
             #skip the moves that don't need to be checked
             move_ids = [x.id for x in pick.move_lines if x.state not in ('draft', 'cancel', 'done')]
             if not move_ids:
@@ -1015,6 +1024,14 @@ class stock_picking(osv.osv):
                 product_putaway_strats[product.id] = location
             return location or picking.location_dest_id.id
 
+        # If we encounter an UoM that is smaller than the default UoM or the one already chosen, use the new one instead.
+        product_uom = {} # Determines UoM used in pack operations
+        for move in picking.move_lines:
+            if not product_uom.get(move.product_id.id):
+                product_uom[move.product_id.id] = move.product_id.uom_id.id
+            if move.product_uom.id != move.product_id.uom_id.id and move.product_uom.factor > product_uom[move.product_id.id]:
+                product_uom[move.product_id.id] = move.product_uom.id
+
         pack_obj = self.pool.get("stock.quant.package")
         quant_obj = self.pool.get("stock.quant")
         vals = []
@@ -1065,17 +1082,24 @@ class stock_picking(osv.osv):
                 qtys_grouped[key] = qty
 
         # Create the necessary operations for the grouped quants and remaining qtys
+        uom_obj = self.pool.get('product.uom')
         for key, qty in qtys_grouped.items():
+            product = self.pool.get("product.product").browse(cr, uid, key[0], context=context)
+            uom_id = product.uom_id.id
+            qty_uom = qty
+            if product_uom.get(key[0]):
+                uom_id = product_uom[key[0]]
+                qty_uom = uom_obj._compute_qty(cr, uid, product.uom_id.id, qty, uom_id)
             vals.append({
                 'picking_id': picking.id,
-                'product_qty': qty,
+                'product_qty': qty_uom,
                 'product_id': key[0],
                 'package_id': key[1],
                 'lot_id': key[2],
                 'owner_id': key[3],
                 'location_id': key[4],
                 'location_dest_id': key[5],
-                'product_uom_id': self.pool.get("product.product").browse(cr, uid, key[0], context=context).uom_id.id,
+                'product_uom_id': uom_id,
             })
         return vals
 
@@ -1111,8 +1135,8 @@ class stock_picking(osv.osv):
                 move_quants = move.reserved_quant_ids
                 picking_quants += move_quants
                 forced_qty = (move.state == 'assigned') and move.product_qty - sum([x.qty for x in move_quants]) or 0
-                #if we used force_assign() on the move, or if the move is incomming, forced_qty > 0
-                if forced_qty:
+                #if we used force_assign() on the move, or if the move is incoming, forced_qty > 0
+                if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0:
                     if forced_qties.get(move.product_id):
                         forced_qties[move.product_id] += forced_qty
                     else:
@@ -1165,15 +1189,21 @@ class stock_picking(osv.osv):
             '''method that creates the link between a given operation and move(s) of given product, for the given quantity.
             Returns True if it was possible to create links for the requested quantity (False if there was not enough quantity on stock moves)'''
             qty_to_assign = qty
+            prod_obj = self.pool.get("product.product")
+            product = prod_obj.browse(cr, uid, product_id)
+            rounding = product.uom_id.rounding
+            qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
             if prod2move_ids.get(product_id):
-                while prod2move_ids[product_id] and qty_to_assign > 0:
+                while prod2move_ids[product_id] and qtyassign_cmp > 0:
                     qty_on_link = _create_link_for_index(operation_id, 0, product_id, qty_to_assign, quant_id=False)
                     qty_to_assign -= qty_on_link
-            return qty_to_assign == 0
+                    qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
+            return qtyassign_cmp == 0
 
         uom_obj = self.pool.get('product.uom')
         package_obj = self.pool.get('stock.quant.package')
         quant_obj = self.pool.get('stock.quant')
+        link_obj = self.pool.get('stock.move.operation.link')
         quants_in_package_done = set()
         prod2move_ids = {}
         still_to_do = []
@@ -1189,8 +1219,9 @@ class stock_picking(osv.osv):
         operations = picking.pack_operation_ids
         operations = sorted(operations, key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.lot_id and -1 or 0))
         #delete existing operations to start again from scratch
-        cr.execute("DELETE FROM stock_move_operation_link WHERE operation_id in %s", (tuple([x.id for x in operations]),))
-
+        links = link_obj.search(cr, uid, [('operation_id', 'in', [x.id for x in operations])], context=context)
+        if links:
+            link_obj.unlink(cr, uid, links, context=context)
         #1) first, try to create links when quants can be identified without any doubt
         for ops in operations:
             #for each operation, create the links with the stock move by seeking on the matching reserved quants,
@@ -1230,7 +1261,8 @@ class stock_picking(osv.osv):
                             max_qty_on_link = min(quant.qty, qty_to_assign)
                             qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
                             qty_to_assign -= qty_on_link
-                if qty_to_assign > 0:
+                qty_assign_cmp = float_compare(qty_to_assign, 0, precision_rounding=ops.product_id.uom_id.rounding)
+                if qty_assign_cmp > 0:
                     #qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed
                     #all the quants (because they leave no choice on their related move and needs to be processed with higher priority)
                     still_to_do += [(ops, ops.product_id.id, qty_to_assign)]
@@ -1259,14 +1291,22 @@ class stock_picking(osv.osv):
         """
         Creates an extra move when there is no corresponding original move to be copied
         """
+        uom_obj = self.pool.get("product.uom")
+        uom_id = product.uom_id.id
+        qty = remaining_qty
+        if op.product_id and op.product_uom_id and op.product_uom_id.id != product.uom_id.id:
+            if op.product_uom_id.factor > product.uom_id.factor: #If the pack operation's is a smaller unit
+                uom_id = op.product_uom_id.id
+                #HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
+                qty = uom_obj._compute_qty_obj(cr, uid, product.uom_id, remaining_qty, op.product_uom_id, rounding_method='HALF-UP')
         picking = op.picking_id
         res = {
             'picking_id': picking.id,
             'location_id': picking.location_id.id,
             'location_dest_id': picking.location_dest_id.id,
             'product_id': product.id,
-            'product_uom': product.uom_id.id,
-            'product_uom_qty': remaining_qty,
+            'product_uom': uom_id,
+            'product_uom_qty': qty,
             'name': _('Extra Move: ') + product.name,
             'state': 'draft',
             }
@@ -1281,8 +1321,8 @@ class stock_picking(osv.osv):
         moves = []
         for op in picking.pack_operation_ids:
             for product_id, remaining_qty in operation_obj._get_remaining_prod_quantities(cr, uid, op, context=context).items():
-                if remaining_qty > 0:
-                    product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
+                product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
+                if float_compare(remaining_qty, 0, precision_rounding=product.uom_id.rounding) > 0:
                     vals = self._prepare_values_extra_move(cr, uid, op, product, remaining_qty, context=context)
                     moves.append(move_obj.create(cr, uid, vals, context=context))
         if moves:
@@ -1341,9 +1381,7 @@ class stock_picking(osv.osv):
                 if not all_op_processed:
                     todo_move_ids += self._create_extra_moves(cr, uid, picking, context=context)
 
-                picking.refresh()
-                #split move lines eventually
-
+                #split move lines if needed
                 toassign_move_ids = []
                 for move in picking.move_lines:
                     remaining_qty = move.remaining_qty
@@ -1352,10 +1390,11 @@ class stock_picking(osv.osv):
                         continue
                     elif move.state == 'draft':
                         toassign_move_ids.append(move.id)
-                    if remaining_qty == 0:
+                    if float_compare(remaining_qty, 0,  precision_rounding = move.product_id.uom_id.rounding) == 0:
                         if move.state in ('draft', 'assigned', 'confirmed'):
                             todo_move_ids.append(move.id)
-                    elif remaining_qty > 0 and remaining_qty < move.product_qty:
+                    elif float_compare(remaining_qty,0, precision_rounding = move.product_id.uom_id.rounding) > 0 and \
+                                float_compare(remaining_qty, move.product_qty, precision_rounding = move.product_id.uom_id.rounding) < 0:
                         new_move = stock_move_obj.split(cr, uid, move, remaining_qty, context=context)
                         todo_move_ids.append(move.id)
                         #Assign move as it was assigned before
@@ -1368,7 +1407,6 @@ class stock_picking(osv.osv):
                     self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=context)
                 elif context.get('do_only_split'):
                     context = dict(context, split=todo_move_ids)
-            picking.refresh()
             self._create_backorder(cr, uid, picking, context=context)
             if toassign_move_ids:
                 stock_move_obj.action_assign(cr, uid, toassign_move_ids, context=context)
@@ -1549,7 +1587,7 @@ class stock_move(osv.osv):
         uom_obj = self.pool.get('product.uom')
         res = {}
         for m in self.browse(cr, uid, ids, context=context):
-            res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, round=False, context=context)
+            res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, context=context)
         return res
 
     def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None):
@@ -1559,8 +1597,8 @@ class stock_move(osv.osv):
             qty = move.product_qty
             for record in move.linked_move_operation_ids:
                 qty -= record.qty
-            #converting the remaining quantity in the move UoM
-            res[move.id] = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, qty, move.product_uom, round=False, context=context)
+            # Keeping in product default UoM
+            res[move.id] = float_round(qty, precision_rounding=move.product_id.uom_id.rounding)
         return res
 
     def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None):
@@ -1654,11 +1692,10 @@ class stock_move(osv.osv):
         'date': fields.datetime('Date', required=True, select=True, help="Move date: scheduled date until move is done, then date of actual move processing", states={'done': [('readonly', True)]}),
         'date_expected': fields.datetime('Expected Date', states={'done': [('readonly', True)]}, required=True, select=True, help="Scheduled date for the processing of this move"),
         'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type', '<>', 'service')], states={'done': [('readonly', True)]}),
-        'product_qty': fields.function(_quantity_normalize, fnct_inv=_set_product_qty, _type='float', store={
+        'product_qty': fields.function(_quantity_normalize, fnct_inv=_set_product_qty, type='float', digits=0, store={
                 'stock.move': (lambda self, cr, uid, ids, ctx: ids, ['product_id', 'product_uom_qty', 'product_uom'], 20),
                 'product.product': (_get_moves_from_prod, ['uom_id'], 20),
             }, string='Quantity',
-            digits_compute=dp.get_precision('Product Unit of Measure'),
             help='Quantity in the default UoM of the product'),
         'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
             required=True, states={'done': [('readonly', True)]},
@@ -1715,8 +1752,8 @@ class stock_move(osv.osv):
         'quant_ids': fields.many2many('stock.quant', 'stock_quant_move_rel', 'move_id', 'quant_id', 'Moved Quants'),
         'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_id', 'Reserved quants'),
         'linked_move_operation_ids': fields.one2many('stock.move.operation.link', 'move_id', string='Linked Operations', readonly=True, help='Operations that impact this move for the computation of the remaining quantities'),
-        'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity',
-                                         digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]},),
+        'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity', digits=0,
+                                         states={'done': [('readonly', True)]}, help="Remaining Quantity in default UoM according to operations matched with this move"),
         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
         'group_id': fields.many2one('procurement.group', 'Procurement Group'),
         'rule_id': fields.many2one('procurement.rule', 'Procurement Rule', help='The pull rule that created this stock move'),
@@ -1808,9 +1845,9 @@ class stock_move(osv.osv):
             'company_id': move.company_id and move.company_id.id or False,
             'date_planned': move.date,
             'product_id': move.product_id.id,
-            'product_qty': move.product_qty,
+            'product_qty': move.product_uom_qty,
             'product_uom': move.product_uom.id,
-            'product_uos_qty': (move.product_uos and move.product_uos_qty) or move.product_qty,
+            'product_uos_qty': (move.product_uos and move.product_uos_qty) or move.product_uom_qty,
             'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id,
             'location_id': move.location_id.id,
             'move_dest_id': move.id,
@@ -2170,7 +2207,6 @@ class stock_move(osv.osv):
         for move in todo_moves:
             if move.linked_move_operation_ids:
                 continue
-            move.refresh()
             #then if the move isn't totally assigned, try to find quants without any specific domain
             if move.state != 'assigned':
                 qty_already_assigned = move.reserved_availability
@@ -2300,11 +2336,14 @@ class stock_move(osv.osv):
                 # Handle pack in pack
                 if not ops.product_id and ops.package_id and ops.result_package_id.id != ops.package_id.parent_id.id:
                     self.pool.get('stock.quant.package').write(cr, SUPERUSER_ID, [ops.package_id.id], {'parent_id': ops.result_package_id.id}, context=context)
+                if not move_qty.get(move.id):
+                    raise osv.except_osv(_("Error"), _("The roundings of your Unit of Measures %s on the move vs. %s on the product don't allow to do these operations or you are not transferring the picking at once. ") % (move.product_uom.name, move.product_id.uom_id.name))
                 move_qty[move.id] -= record.qty
         #Check for remaining qtys and unreserve/check move_dest_id in
         move_dest_ids = set()
         for move in self.browse(cr, uid, ids, context=context):
-            if move_qty[move.id] > 0:  # (=In case no pack operations in picking)
+            move_qty_cmp = float_compare(move_qty[move.id], 0, precision_rounding=move.product_id.uom_id.rounding)
+            if move_qty_cmp > 0:  # (=In case no pack operations in picking)
                 main_domain = [('qty', '>', 0)]
                 prefered_domain = [('reservation_id', '=', move.id)]
                 fallback_domain = [('reservation_id', '=', False)]
@@ -2418,7 +2457,8 @@ class stock_move(osv.osv):
         uom_obj = self.pool.get('product.uom')
         context = context or {}
 
-        uom_qty = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, qty, move.product_uom)
+        #HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
+        uom_qty = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, qty, move.product_uom, rounding_method='HALF-UP', context=context)
         uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty
 
         defaults = {
@@ -2571,7 +2611,6 @@ class stock_inventory(osv.osv):
                 if inventory_line.product_qty < 0 and inventory_line.product_qty != inventory_line.theoretical_qty:
                     raise osv.except_osv(_('Warning'), _('You cannot set a negative product quantity in an inventory line:\n\t%s - qty: %s' % (inventory_line.product_id.name, inventory_line.product_qty)))
             self.action_check(cr, uid, [inv.id], context=context)
-            inv.refresh()
             self.write(cr, uid, [inv.id], {'state': 'done'}, context=context)
             self.post_inventory(cr, uid, inv, context=context)
         return True
@@ -3325,7 +3364,6 @@ class stock_warehouse(osv.osv):
         new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context)
         warehouse = self.browse(cr, uid, new_id, context=context)
         self.create_sequences_and_picking_types(cr, uid, warehouse, context=context)
-        warehouse.refresh()
 
         #create routes and push/pull rules
         new_objects_dict = self.create_routes(cr, uid, new_id, warehouse, context=context)
@@ -3445,7 +3483,6 @@ class stock_warehouse(osv.osv):
                 self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive)
                 # Check if we need to change something to resupply warehouses and associated MTO rules
                 self._check_resupply(cr, uid, warehouse, vals.get('reception_steps'), vals.get('delivery_steps'), context=context)
-                warehouse.refresh()
             if vals.get('code') or vals.get('name'):
                 name = warehouse.name
                 #rename sequence
@@ -3587,7 +3624,6 @@ class stock_location_path(osv.osv):
                 'date_expected': newdate,
                 'location_dest_id': rule.location_dest_id.id
             })
-            move.refresh()
             #avoid looping if a push rule is not well configured
             if rule.location_dest_id.id != old_dest_location:
                 #call again push_apply to see if a next step is defined
@@ -3803,10 +3839,7 @@ class stock_pack_operation(osv.osv):
                     qty = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context)
                 for record in ops.linked_move_operation_ids:
                     qty -= record.qty
-                #converting the remaining quantity in the pack operation UoM
-                if ops.product_uom_id:
-                    qty = uom_obj._compute_qty_obj(cr, uid, ops.product_id.uom_id, qty, ops.product_uom_id, context=context)
-                res[ops.id] = qty
+                res[ops.id] = float_round(qty, precision_rounding=ops.product_id.uom_id.rounding)
         return res
 
     def product_id_change(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
@@ -3852,7 +3885,7 @@ class stock_pack_operation(osv.osv):
         'cost': fields.float("Cost", help="Unit Cost for this product line"),
         'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'),
         'linked_move_operation_ids': fields.one2many('stock.move.operation.link', 'operation_id', string='Linked Moves', readonly=True, help='Moves impacted by this operation for the computation of the remaining quantities'),
-        'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Qty'),
+        'remaining_qty': fields.function(_get_remaining_qty, type='float', digits = 0, string="Remaining Qty", help="Remaining quantity in default UoM according to moves matched with this operation. "),
         'location_id': fields.many2one('stock.location', 'Source Location', required=True),
         'location_dest_id': fields.many2one('stock.location', 'Destination Location', required=True),
         'processed': fields.selection([('true','Yes'), ('false','No')],'Has been processed?', required=True),
index 112b233..deb624e 100644 (file)
@@ -103,7 +103,7 @@ class stock_return_picking(osv.osv_memory):
         # Cancel assignment of existing chained assigned moves
         moves_to_unreserve = []
         for move in pick.move_lines:
-            to_check_moves = [move.move_dest_id]
+            to_check_moves = [move.move_dest_id] if move.move_dest_id.id else []
             while to_check_moves:
                 current_move = to_check_moves.pop()
                 if current_move.state not in ('done', 'cancel') and current_move.reserved_quant_ids:
index 9707263..243d982 100644 (file)
@@ -267,7 +267,6 @@ class stock_move(osv.osv):
     def _store_average_cost_price(self, cr, uid, move, context=None):
         ''' move is a browe record '''
         product_obj = self.pool.get('product.product')
-        move.refresh()
         if any([q.qty <= 0 for q in move.quant_ids]):
             #if there is a negative quant, the standard price shouldn't be updated
             return
index bad98e7..57e2eaa 100644 (file)
                  && this.day === other.day)
                 ? py.True : py.False;
         },
+        replace: function () {
+            var args = py.PY_parseArgs(arguments, [
+                ['year', py.None], ['month', py.None], ['day', py.None]
+            ]);
+            var params = {};
+            for(var key in args) {
+                if (!args.hasOwnProperty(key)) { continue; }
+
+                var arg = args[key];
+                params[key] = (arg === py.None ? this[key] : asJS(arg));
+            }
+            return py.PY_call(datetime.date, params);
+        },
         __add__: function (other) {
             if (!py.PY_isInstance(other, datetime.timedelta)) {
                 return py.NotImplemented;
index b72540c..e1ba28e 100644 (file)
@@ -8,7 +8,7 @@ Start/Stop the Odoo server
 ==========================
 
 Odoo uses a client/server architecture in which clients are web browsers
-accessing the odoo server via RPC.
+accessing the Odoo server via RPC.
 
 Business logic and extension is generally performed on the server side,
 although supporting client features (e.g. new data representation such as
@@ -1691,14 +1691,14 @@ server with the library ``xmlrpclib``::
    uid = xmlrpclib.ServerProxy(root + 'common').login(DB, USER, PASS)
    print "Logged in as %s (uid: %d)" % (USER, uid)
 
-   # Create a new idea
+   # Create a new note
    sock = xmlrpclib.ServerProxy(root + 'object')
    args = {
-       'name' : 'Another idea',
-       'description' : 'This is another idea of mine',
-       'inventor_id': uid,
+       'color' : 8,
+       'memo' : 'This is a note',
+       'create_uid': uid,
    }
-   idea_id = sock.execute(DB, uid, PASS, 'idea.idea', 'create', args)
+   note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args)
 
 .. exercise:: Add a new service to the client
 
@@ -1780,13 +1780,13 @@ with the standard Python libraries ``urllib2`` and ``json``::
     url = "http://%s:%s/jsonrpc" % (HOST, PORT)
     uid = call(url, "common", "login", DB, USER, PASS)
 
-    # create a new idea
+    # create a new note
     args = {
-        'name' : 'Another idea',
-        'description' : 'This is another idea of mine',
-        'inventor_id': uid,
+        'color' : 8,
+        'memo' : 'This is another note',
+        'create_uid': uid,
     }
-    idea_id = call(url, "object", "execute", DB, uid, PASS, 'idea.idea', 'create', args)
+    note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args)
 
 Here is the same program, using the library
 `jsonrpclib <https://pypi.python.org/pypi/jsonrpclib>`::
@@ -1805,13 +1805,13 @@ Here is the same program, using the library
         args = [DB, uid, PASS, model, method] + list(args)
         return server.call(service="object", method="execute", args=args)
 
-    # create a new idea
+    # create a new note
     args = {
-        'name' : 'Another idea',
-        'description' : 'This is another idea of mine',
-        'inventor_id': uid,
+        'color' : 8,
+        'memo' : 'This is another note',
+        'create_uid': uid,
     }
-    idea_id = invoke('idea.idea', 'create', args)
+    note_id = invoke('note.note', 'create', args)
 
 Examples can be easily adapted from XML-RPC to JSON-RPC.
 
index 1287315..ac84e34 100644 (file)
@@ -36,7 +36,7 @@ Index: addons/openacademy/views/openacademy.xml
 +            </field>
 +        </record>
 +
-+        <!-- top level menu: no paret -->
++        <!-- top level menu: no parent -->
 +        <menuitem id="main_openacademy_menu" name="Open Academy"/>
 +        <!-- A first level in the left side menu is needed
 +             before using action= attribute -->
index 0fdb3e3..4b780bd 100644 (file)
@@ -133,7 +133,7 @@ Index: addons/openacademy/templates.xml
 +        <!-- <template id="listing"> -->
 +        <!--   <ul> -->
 +        <!--     <li t-foreach="objects" t-as="object"> -->
-+        <!--       <a t-attf-href="#{ root }/objects/#{ object.id }"> -->
++        <!--       <a t-attf-href="{{ root }}/objects/{{ object.id }}"> -->
 +        <!--         <t t-esc="object.display_name"/> -->
 +        <!--       </a> -->
 +        <!--     </li> -->
index 7c76e69..bf175ed 100644 (file)
@@ -26,11 +26,12 @@ Index: addons/openacademy/views/session_board.xml
 ===================================================================
 --- /dev/null  1970-01-01 00:00:00.000000000 +0000
 +++ addons/openacademy/views/session_board.xml 2014-08-26 17:26:21.531783052 +0200
-@@ -0,0 +1,63 @@
+@@ -0,0 +1,66 @@
 +<?xml version="1.0"?>
 +<openerp>
 +    <data>
 +        <record model="ir.actions.act_window" id="act_session_graph">
++            <field name="name">Attendees by course</field>
 +            <field name="res_model">openacademy.session</field>
 +            <field name="view_type">form</field>
 +            <field name="view_mode">graph</field>
@@ -38,12 +39,14 @@ Index: addons/openacademy/views/session_board.xml
 +                   ref="openacademy.openacademy_session_graph_view"/>
 +        </record>
 +        <record model="ir.actions.act_window" id="act_session_calendar">
++            <field name="name">Sessions</field>
 +            <field name="res_model">openacademy.session</field>
 +            <field name="view_type">form</field>
 +            <field name="view_mode">calendar</field>
 +            <field name="view_id" ref="openacademy.session_calendar_view"/>
 +        </record>
 +        <record model="ir.actions.act_window" id="act_course_list">
++            <field name="name">Courses</field>
 +            <field name="res_model">openacademy.course</field>
 +            <field name="view_type">form</field>
 +            <field name="view_mode">tree,form</field>
index 892ef6e..7d69770 100644 (file)
@@ -30,9 +30,9 @@ Index: addons/openacademy/views/openacademy.xml
 +                    <templates>
 +                        <t t-name="kanban-box">
 +                            <div
-+                                    t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)}
++                                    t-attf-class="oe_kanban_color_{{kanban_getcolor(record.color.raw_value)}}
 +                                                  oe_kanban_global_click_edit oe_semantic_html_override
-+                                                  oe_kanban_card #{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}">
++                                                  oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}">
 +                                <div class="oe_dropdown_kanban">
 +                                    <!-- dropdown menu -->
 +                                    <div class="oe_dropdown_toggle">
index 35cb439..2c6eef2 100644 (file)
@@ -8,7 +8,7 @@ Basic set up
 ============
 
 Create a basic theme module with :command:`odoo.py scaffold` and the ``theme``
-template: from the root odoo folder, use
+template: from the root Odoo folder, use
 
 .. code-block:: console
 
index 384297d..ba8337f 100644 (file)
@@ -45,7 +45,14 @@ If you browse the ``petstore`` folder, you should see the following content:
 .. code-block:: text
 
     oepetstore
+    |-- images
+    |   |-- alligator.jpg
+    |   |-- ball.jpg
+    |   |-- crazy_circle.jpg
+    |   |-- fish.jpg
+    |   `-- mice.jpg
     |-- __init__.py
+    |-- oepetstore.message_of_the_day.csv
     |-- __openerp__.py
     |-- petstore_data.xml
     |-- petstore.py
@@ -78,9 +85,9 @@ sub-folders are conventional and not strictly necessary.
     currently look like::
 
         openerp.oepetstore = function(instance, local) {
-            var _t = openerp.web._t,
-                _lt = openerp.web._lt;
-            var QWeb = openerp.web.qweb;
+            var _t = instance.web._t,
+                _lt = instance.web._lt;
+            var QWeb = instance.web.qweb;
 
             local.HomePage = instance.Widget.extend({
                 start: function() {
@@ -88,8 +95,8 @@ sub-folders are conventional and not strictly necessary.
                 },
             });
 
-            openerp.web.client_actions.add(
-                'petstore.homepage', 'local.HomePage');
+            instance.web.client_actions.add(
+                'petstore.homepage', 'instance.oepetstore.HomePage');
         }
 
 Which only prints a small message in the browser's console.
@@ -528,7 +535,7 @@ characteristics:
     structural extensibility where an XML-based templating engine can be
     generically altered using e.g. XPath or CSS and a tree-alteration DSL (or
     even just XSLT). This flexibility and extensibility is a core
-    characteristic of Odoo, and losting it was considered unacceptable.
+    characteristic of Odoo, and losing it was considered unacceptable.
 
 Using QWeb
 ----------
@@ -1121,6 +1128,7 @@ Exercise
                     },
                     start: function() {
                         this.input_changed();
+                        return this._super();
                     },
                     input_changed: function() {
                         var color = [
@@ -1136,9 +1144,9 @@ Exercise
                 local.HomePage = instance.Widget.extend({
                     template: "HomePage",
                     start: function() {
-                        this.colorInput = new local.ColorInputWidget(this)
-                            .on("change:color", this, this.color_changed);
-                            .appendTo(this.$el);
+                        this.colorInput = new local.ColorInputWidget(this);
+                        this.colorInput.on("change:color", this, this.color_changed);
+                        return this.colorInput.appendTo(this.$el);
                     },
                     color_changed: function() {
                         this.$(".oe_color_div").css("background-color", this.colorInput.get("color"));
@@ -1560,8 +1568,10 @@ Exercises
                             .filter([['categ_id.name', '=', "Pet Toys"]])
                             .limit(5)
                             .all()
-                            .then(function (result) {
-                                self.$el.append(QWeb.render('PetToys', {item: item}));
+                            .then(function (results) {
+                                _(results).each(function (item) {
+                                    self.$el.append(QWeb.render('PetToy', {item: item}));
+                                });
                             });
                     }
                 });
@@ -1578,7 +1588,7 @@ Exercises
                         <div class="oe_petstore_homepage_right"></div>
                     </div>
                 </t>
-                <t t-name="MessageofTheDay">
+                <t t-name="MessageOfTheDay">
                     <div class="oe_petstore_motd">
                         <p class="oe_mywidget_message_of_the_day"></p>
                     </div>
@@ -1732,7 +1742,7 @@ attributes are:
             <t t-name="PetToy">
                 <div class="oe_petstore_pettoy" t-att-data-id="item.id">
                     <p><t t-esc="item.name"/></p>
-                    <p><img t-attf-src="data:image/jpg;base64,#{item.image}"/></p>
+                    <p><img t-attf-src="data:image/jpg;base64,{{item.image}}"/></p>
                 </div>
             </t>
 
index 2187956..5092cac 100644 (file)
@@ -142,7 +142,7 @@ First define an Odoo model file and import it:
 .. patch::
 
 Then setup :ref:`basic access control <reference/security/acl>` for the model
-and and add them to the manifest:
+and add them to the manifest:
 
 .. patch::
 
@@ -211,7 +211,7 @@ integration and a few other services (e.g. default styling, theming) via the
 #. then add the ``website=True`` flag on the controller, this sets up a few
    new variables on :ref:`the request object <reference/http/request>` and
    allows using the website layout in our template
-#. use the wesite layout in the template
+#. use the website layout in the template
 
 .. patch::
 
@@ -307,7 +307,7 @@ interfaces. Change the *person* template to use ``t-field``:
 
 Restart Odoo and upgrade the module, there is now a placeholder under the
 teacher's name and a new zone for blocks in :guilabel:`Edit` mode. Content
-dropped there is stored in the correspoding teacher's ``biography`` field, and
+dropped there is stored in the corresponding teacher's ``biography`` field, and
 thus specific to that teacher.
 
 The teacher's name is also editable, and when saved the change is visible on
@@ -344,7 +344,7 @@ The conceptual structure of the Odoo backend is simple:
 #. actions. Actions have various types: links, reports, code which Odoo should
    execute or data display. Data display actions are called *window actions*,
    and tell Odoo to display a given *model* according to a set of views…
-#. a view has a type, a the broad category to which it corresponds (a list,
+#. a view has a type, a broad category to which it corresponds (a list,
    a graph, a calendar) and an *architecture* which customises the way the
    model is displayed inside the view.
 
@@ -457,8 +457,8 @@ though they may have to be looked for.
     * to extend a model in-place, it's :attr:`inherited
       <openerp.models.Model._inherit>` without giving it a new
       :attr:`~openerp.models.Model._name`
-    * ``product.template`` already uses the discussions system, so we
-      can remove it from our extension model
+    * ``product.template`` already uses the discussions system, so we can
+      remove it from our extension model
     * we're creating our courses as *published* by default so they can be
       seen without having to log in
 
index 3f5b2ea..bfb49c8 100644 (file)
@@ -132,6 +132,14 @@ root can have the following attributes:
     <reference/views/form>`'s fields and buttons are thus accepted by list
     views although they may not have any meaning if the list view is
     non-editable
+``default_order``
+    overrides the ordering of the view, replacing the model's default order.
+    The value is a comma-separated list of fields, postfixed by ``desc`` to
+    sort in reverse order:
+
+    .. code-block:: xml
+
+        <tree default_order="sequence,name desc">
 ``colors``
     allows changing the color of a row's text based on the corresponding
     record's attributes.
index 943fa09..9fe6093 100644 (file)
@@ -272,7 +272,7 @@ class ir_actions_act_window(osv.osv):
     _columns = {
         'name': fields.char('Action Name', translate=True),
         'type': fields.char('Action Type', required=True),
-        'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='cascade'),
+        'view_id': fields.many2one('ir.ui.view', 'View Ref.', ondelete='set null'),
         'domain': fields.char('Domain Value',
             help="Optional domain filtering of the destination data, as a Python expression"),
         'context': fields.char('Context Value', required=True,
index c6bb931..0c877a4 100644 (file)
@@ -111,6 +111,18 @@ class TestORM(common.TransactionCase):
         found = self.partner.search_read(self.cr, UID, [['name', '=', 'Does not exists']], ['name'])
         self.assertEqual(len(found), 0)
 
+    def test_exists(self):
+        partner = self.partner.browse(self.cr, UID, [])
+
+        # check that records obtained from search exist
+        recs = partner.search([])
+        self.assertTrue(recs)
+        self.assertEqual(recs.exists(), recs)
+
+        # check that there is no record with id 0
+        recs = partner.browse([0])
+        self.assertFalse(recs.exists())
+
     def test_groupby_date(self):
         partners = dict(
             A='2012-11-19',
index 3435204..eddbcb6 100644 (file)
@@ -516,16 +516,19 @@ class JsonRequest(WebRequest):
         try:
             return super(JsonRequest, self)._handle_exception(exception)
         except Exception:
-            if not isinstance(exception, openerp.exceptions.Warning):
+            if not isinstance(exception, (openerp.exceptions.Warning, SessionExpiredException)):
                 _logger.exception("Exception during JSON request handling.")
             error = {
                     'code': 200,
-                    'message': "OpenERP Server Error",
+                    'message': "Odoo Server Error",
                     'data': serialize_exception(exception)
             }
             if isinstance(exception, AuthenticationError):
                 error['code'] = 100
-                error['message'] = "OpenERP Session Invalid"
+                error['message'] = "Odoo Session Invalid"
+            if isinstance(exception, SessionExpiredException):
+                error['code'] = 100
+                error['message'] = "Odoo Session Expired"
             return self._json_response(error=error)
 
     def dispatch(self):
index 559d97c..ac935c5 100644 (file)
@@ -241,6 +241,11 @@ class MetaModel(api.Meta):
         if not self._custom:
             self.module_to_models.setdefault(self._module, []).append(self)
 
+        # check for new-api conversion error: leave comma after field definition
+        for key, val in attrs.iteritems():
+            if type(val) is tuple and len(val) == 1 and isinstance(val[0], Field):
+                _logger.error("Trailing comma after field definition: %s.%s", self, key)
+
         # transform columns into new-style fields (enables field inheritance)
         for name, column in self._columns.iteritems():
             if name in self.__dict__:
@@ -4782,14 +4787,15 @@ class BaseModel(object):
 
         By convention, new records are returned as existing.
         """
-        ids = filter(None, self._ids)           # ids to check in database
+        ids, new_ids = [], []
+        for i in self._ids:
+            (ids if isinstance(i, (int, long)) else new_ids).append(i)
         if not ids:
             return self
         query = """SELECT id FROM "%s" WHERE id IN %%s""" % self._table
-        self._cr.execute(query, (ids,))
-        ids = ([r[0] for r in self._cr.fetchall()] +    # ids in database
-               [id for id in self._ids if not id])      # new ids
-        existing = self.browse(ids)
+        self._cr.execute(query, [tuple(ids)])
+        ids = [r[0] for r in self._cr.fetchall()]
+        existing = self.browse(ids + new_ids)
         if len(existing) < len(self):
             # mark missing records in cache with a failed value
             exc = MissingError(_("Record does not exist or has been deleted."))
index 2189cc6..9d10e29 100644 (file)
@@ -862,10 +862,11 @@ def preload_registries(dbnames):
             # run test_file if provided
             if test_file:
                 _logger.info('loading test file %s', test_file)
-                if test_file.endswith('yml'):
-                    load_test_file_yml(registry, test_file)
-                elif test_file.endswith('py'):
-                    load_test_file_py(registry, test_file)
+                with openerp.api.Environment.manage():
+                    if test_file.endswith('yml'):
+                        load_test_file_yml(registry, test_file)
+                    elif test_file.endswith('py'):
+                        load_test_file_py(registry, test_file)
 
             if registry._assertion_report.failures:
                 rc += 1
index c7e5e9d..04ea928 100644 (file)
@@ -54,4 +54,4 @@ ExecStart=/usr/bin/odoo.py --config=/etc/odoo/openerp-server.conf
 [Install]
 WantedBy=multi-user.target
 EOF
-easy_install pyPdf vatnumber pydot
+easy_install pyPdf vatnumber pydot psycogreen