From 73db5b3eef51180b0dd81e212213ee9bf284abd5 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 2 Dec 2014 15:02:02 +0100 Subject: [PATCH 01/25] [FIX] account_voucher: period selection in on_change The find method returns a list of ids while the on_chage expects an int --- addons/account_voucher/account_voucher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index 78060e8a733..b1e8e8ecb08 100644 --- a/addons/account_voucher/account_voucher.py +++ b/addons/account_voucher/account_voucher.py @@ -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 From cf16632b44cbb093564124f81843eec82b836d38 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Fri, 14 Nov 2014 12:18:07 +0100 Subject: [PATCH 02/25] [IMP] No digits_compute on product_qty of move + rounding on qty_available fields + add float_compare --- addons/stock/product.py | 17 ++++++++----- addons/stock/stock.py | 55 +++++++++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/addons/stock/product.py b/addons/stock/product.py index 8dbe4c77227..56770784f9e 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -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): diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 902a9f287e9..80a9aaf87d5 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -25,6 +25,7 @@ import json import time from openerp.osv import fields, osv +from openerp.tools.float_utils import float_compare 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,9 @@ 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'): + if float_compare(reserved_availability, move.product_qty, precision_rounding=move.product_uom.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=move.product_uom.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): @@ -438,7 +439,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) @@ -516,9 +518,12 @@ 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): + + if (quant.qty > 0 and float_compare(quant.qty, qty, precision_rounding=quant.product_id.uom_id.rounding) <= 0)\ + or (quant.qty <= 0 and float_compare(quant.qty, qty, precision_rounding=quant.product_id.uom_id.rounding) >= 0) : + #(quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty): return False - new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': quant.qty - qty}, context=context) + new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': quant.qty - qty, 'history_ids': [(4, x.id) for x in quant.history_ids]}, context=context) self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty}, context=context) quant.refresh() return self.browse(cr, uid, new_quant, context=context) @@ -550,8 +555,9 @@ class stock_quant(osv.osv): dom += [('owner_id', '=', quant.owner_id.id)] dom += [('package_id', '=', quant.package_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) @@ -607,16 +613,18 @@ 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): + qty_cmp = float_compare(quantity, abs(quant.qty), precision_rounding=product.uom_id.rounding) + qty0_cmp = float_compare(quantity, 0.0, precision_rounding=product.uom_id.rounding) + if qty_cmp >= 0: res += [(quant, abs(quant.qty))] quantity -= abs(quant.qty) - elif quantity != 0: + elif qty0_cmp != 0: res += [(quant, quantity)] quantity = 0 break @@ -1111,8 +1119,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,11 +1173,16 @@ 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') @@ -1190,7 +1203,6 @@ class stock_picking(osv.osv): 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]),)) - #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, @@ -1210,7 +1222,7 @@ class stock_picking(osv.osv): need_rereserve = True elif ops.product_id.id: #Check moves with same product - qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context) + qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, round=False, context=context) for move_dict in prod2move_ids.get(ops.product_id.id, []): move = move_dict['move'] for quant in move.reserved_quant_ids: @@ -1230,7 +1242,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)] @@ -1352,10 +1365,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 @@ -1654,11 +1668,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)]}, From 244c8ee6259fdb699b992996d8ef9ff82b3294b1 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Tue, 18 Nov 2014 16:38:21 +0100 Subject: [PATCH 03/25] [FIX] Propate propagated_from_id when reconciling negative quants to the quant that loses it When a negative quant is created but the positive quant counterpart is reconciling a negative quant that of course also has a positive counterpart, the latter should eventually let its field propagated_from_id tell that it originated from the very first negative quant as the second negative quant will have disappeared through reconciliation. --- addons/stock/stock.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 80a9aaf87d5..cb690e8cc9e 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -550,10 +550,12 @@ class stock_quant(osv.osv): """ solving_quant = quant dom = [('qty', '<', 0)] + prod = quant.product_id.id if quant.lot_id: 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: @@ -577,6 +579,8 @@ 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: + self.write(cr, uid, solved_quant_ids, {'propagated_from_id': solving_quant.propagated_from_id.id}) #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 @@ -586,6 +590,8 @@ class stock_quant(osv.osv): self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context) solving_quant = remaining_solving_quant + + def _price_update(self, cr, uid, ids, newprice, context=None): self.write(cr, SUPERUSER_ID, ids, {'cost': newprice}, context=context) From 77f8732680196970089acd9f5e54478c76890a0b Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Thu, 20 Nov 2014 11:59:29 +0100 Subject: [PATCH 04/25] [IMP] Rounding should be done on move immediately to default UoM and quants should be rounded to the default UoM + remaining_qty [FIX] Remaining qty should each time be in the default UoM of the product Even with different UoM we want a consistent matching between moves and pack operations. When calculating the remaining qty on move/pack operation we always start by converting the qty on the move/operation to the default UoM and afterwards we subtract the links between them which will also be in the default UoM of the product. In order to create backorders / extra moves these quantities are used. --- addons/stock/stock.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index cb690e8cc9e..2b718315c01 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -25,7 +25,7 @@ import json import time from openerp.osv import fields, osv -from openerp.tools.float_utils import float_compare +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 @@ -378,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 float_compare(reserved_availability, move.product_qty, precision_rounding=move.product_uom.rounding) == 0 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 float_compare(reserved_availability, 0, precision_rounding=move.product_uom.rounding) > 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): @@ -487,10 +488,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), @@ -505,7 +507,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 @@ -518,13 +520,15 @@ class stock_quant(osv.osv): def _quant_split(self, cr, uid, quant, qty, context=None): context = context or {} - - if (quant.qty > 0 and float_compare(quant.qty, qty, precision_rounding=quant.product_id.uom_id.rounding) <= 0)\ - or (quant.qty <= 0 and float_compare(quant.qty, qty, precision_rounding=quant.product_id.uom_id.rounding) >= 0) : + rounding = quant.product_id.uom_id.rounding + if (quant.qty > 0 and float_compare(quant.qty, qty, precision_rounding=rounding) <= 0)\ + or (quant.qty <= 0 and float_compare(quant.qty, qty, precision_rounding=rounding) >= 0) : #(quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty): return False - new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': quant.qty - qty, 'history_ids': [(4, x.id) for x in quant.history_ids]}, context=context) - self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty}, context=context) + 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) quant.refresh() return self.browse(cr, uid, new_quant, context=context) @@ -550,7 +554,6 @@ class stock_quant(osv.osv): """ solving_quant = quant dom = [('qty', '<', 0)] - prod = quant.product_id.id if quant.lot_id: dom += [('lot_id', '=', quant.lot_id.id)] dom += [('owner_id', '=', quant.owner_id.id)] @@ -590,8 +593,6 @@ class stock_quant(osv.osv): self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context) solving_quant = remaining_solving_quant - - def _price_update(self, cr, uid, ids, newprice, context=None): self.write(cr, SUPERUSER_ID, ids, {'cost': newprice}, context=context) @@ -1569,7 +1570,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): @@ -1579,8 +1580,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] = qty return res def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None): @@ -1735,7 +1736,7 @@ class stock_move(osv.osv): '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)]},), + 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'), @@ -3814,9 +3815,6 @@ 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 return res @@ -3863,7 +3861,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', 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), From 910376a1574a55885e02c77b83619416d8005647 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Fri, 21 Nov 2014 15:19:53 +0100 Subject: [PATCH 05/25] [IMP] Rounding of the UoM should have an unlimited precision --- addons/product/product.py | 2 +- addons/product/product_view.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/product/product.py b/addons/product/product.py index d05e6bfe636..2b70e1a3101 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -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."), diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index c9e26c56131..7e8dcd2b5b2 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -610,7 +610,7 @@ - + From bb45636ba7e3a758dcbe4517d7b62a313e3a308e Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Fri, 28 Nov 2014 13:14:08 +0100 Subject: [PATCH 06/25] [IMP] When preparing pack operations, use the smallest UoM of the moves if they are smaller than the default UoM. Otherwise use default UoM. The same is done when extra moves are generated. It is going to check if the UoM of the operation is smaller if it has one. Throw an error when a key can not be found in action_done because there were links on a move that was not supposed to be done (e.g. 0.5 Dozen when Dozen is rounded at 1) [IMP] Throw an error when a key can not be found because of UoMs/picking + extra float_compare [IMP] Integrate remarks qdp --- addons/stock/stock.py | 67 +++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 2b718315c01..2d49d26063a 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -521,9 +521,7 @@ class stock_quant(osv.osv): def _quant_split(self, cr, uid, quant, qty, context=None): context = context or {} rounding = quant.product_id.uom_id.rounding - if (quant.qty > 0 and float_compare(quant.qty, qty, precision_rounding=rounding) <= 0)\ - or (quant.qty <= 0 and float_compare(quant.qty, qty, precision_rounding=rounding) >= 0) : - #(quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty): + if float_compare(abs(quant.qty), abs(qty), precision_rounding=rounding) <= 0: # if quant <= qty in abs, take it entirely return False qty_round = float_round(qty, precision_rounding=rounding) new_qty_round = float_round(quant.qty - qty, precision_rounding=rounding) @@ -582,14 +580,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: - self.write(cr, uid, solved_quant_ids, {'propagated_from_id': solving_quant.propagated_from_id.id}) + 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 @@ -626,12 +625,11 @@ class stock_quant(osv.osv): res.append((None, quantity)) break for quant in self.browse(cr, uid, quants, context=context): - qty_cmp = float_compare(quantity, abs(quant.qty), precision_rounding=product.uom_id.rounding) - qty0_cmp = float_compare(quantity, 0.0, precision_rounding=product.uom_id.rounding) - if qty_cmp >= 0: + 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 qty0_cmp != 0: + elif float_compare(quantity, 0.0, precision_rounding=rounding) != 0: res += [(quant, quantity)] quantity = 0 break @@ -1030,6 +1028,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 = [] @@ -1080,17 +1086,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 @@ -1279,14 +1292,21 @@ 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 + qty = uom_obj._compute_qty_obj(cr, uid, product.uom_id, remaining_qty, op.product_uom_id) 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', } @@ -1301,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: @@ -1735,7 +1755,7 @@ 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', + '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'), @@ -2320,11 +2340,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)] @@ -3861,7 +3884,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", help="Remaining quantity in default UoM according to moves matched with this operation. "), + '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), From 49c8fc21baa806cab4631f58caa487f4b75f9996 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Mon, 1 Dec 2014 16:05:07 +0100 Subject: [PATCH 07/25] [FIX] When creating a procurement from a move, respect the right UoM conversions --- addons/stock/stock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 2d49d26063a..254855bb089 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1848,9 +1848,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, From c255fe33846f5b61134775d87270cd0135a514a9 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Tue, 2 Dec 2014 14:28:54 +0100 Subject: [PATCH 08/25] [IMP] Add a method half-up for uom conversion in case we want to convert from default UoM to something else + remaining_qty round + default factor on UoM --- addons/product/product.py | 9 +++++---- addons/stock/stock.py | 12 +++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/addons/product/product.py b/addons/product/product.py index 2b70e1a3101..e5afb4ef38d 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -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): diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 254855bb089..b124d348b87 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1242,7 +1242,7 @@ class stock_picking(osv.osv): need_rereserve = True elif ops.product_id.id: #Check moves with same product - qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, round=False, context=context) + qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context) for move_dict in prod2move_ids.get(ops.product_id.id, []): move = move_dict['move'] for quant in move.reserved_quant_ids: @@ -1298,7 +1298,8 @@ class stock_picking(osv.osv): 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 - qty = uom_obj._compute_qty_obj(cr, uid, product.uom_id, remaining_qty, op.product_uom_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, @@ -1601,7 +1602,7 @@ class stock_move(osv.osv): for record in move.linked_move_operation_ids: qty -= record.qty # Keeping in product default UoM - res[move.id] = qty + 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): @@ -2461,7 +2462,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 = { @@ -3838,7 +3840,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 - 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): From 83b28737e9277d0612211900d49f539bb631dfb6 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Tue, 2 Dec 2014 16:01:38 +0100 Subject: [PATCH 09/25] [FIX] mail: no_auto_thread default True if the model has no mail.thread --- addons/mail/wizard/mail_compose_message.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 04decd6d2bf..61a867c20e3 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -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 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' From 38f0508bba7daf95d347e77abf15d184158da0e5 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Mon, 1 Dec 2014 09:10:16 +0100 Subject: [PATCH 10/25] [FIX] doc: typos and code samples from howtos * The dashboard from the backend howto doesn't specify "name" field which causes non-null constraint violation. * Some useful examples uses idea.idea" model which isn't available in 8.0 so I replaced them with "note.note". closes #3993 --- doc/howtos/backend.rst | 32 +++++++++++----------- doc/howtos/backend/exercise-basic-action | 2 +- doc/howtos/backend/exercise-dashboard | 5 +++- doc/howtos/themes.rst | 2 +- doc/howtos/web.rst | 34 +++++++++++++++--------- doc/howtos/website.rst | 12 ++++----- 6 files changed, 50 insertions(+), 37 deletions(-) diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst index b72540cf2d3..e1ba28e3324 100644 --- a/doc/howtos/backend.rst +++ b/doc/howtos/backend.rst @@ -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 `:: @@ -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. diff --git a/doc/howtos/backend/exercise-basic-action b/doc/howtos/backend/exercise-basic-action index 1287315653f..ac84e344d6a 100644 --- a/doc/howtos/backend/exercise-basic-action +++ b/doc/howtos/backend/exercise-basic-action @@ -36,7 +36,7 @@ Index: addons/openacademy/views/openacademy.xml + + + -+ ++ + + diff --git a/doc/howtos/backend/exercise-dashboard b/doc/howtos/backend/exercise-dashboard index 7c76e690207..bf175edc7d3 100644 --- a/doc/howtos/backend/exercise-dashboard +++ b/doc/howtos/backend/exercise-dashboard @@ -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 @@ + + + + ++ Attendees by course + openacademy.session + form + graph @@ -38,12 +39,14 @@ Index: addons/openacademy/views/session_board.xml + ref="openacademy.openacademy_session_graph_view"/> + + ++ Sessions + openacademy.session + form + calendar + + + ++ Courses + openacademy.course + form + tree,form diff --git a/doc/howtos/themes.rst b/doc/howtos/themes.rst index 35cb43989aa..2c6eef2d405 100644 --- a/doc/howtos/themes.rst +++ b/doc/howtos/themes.rst @@ -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 diff --git a/doc/howtos/web.rst b/doc/howtos/web.rst index 384297dc6a9..8dc89d8f549 100644 --- a/doc/howtos/web.rst +++ b/doc/howtos/web.rst @@ -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
- +

diff --git a/doc/howtos/website.rst b/doc/howtos/website.rst index 2187956abaf..5092cac5df6 100644 --- a/doc/howtos/website.rst +++ b/doc/howtos/website.rst @@ -142,7 +142,7 @@ First define an Odoo model file and import it: .. patch:: Then setup :ref:`basic access control ` 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 ` 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 ` 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 From 8c8056d95f0f993284cb5255c1d2921de04a9ff2 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Mon, 17 Nov 2014 09:55:46 +0100 Subject: [PATCH 11/25] [IMP] Query should pass through orm and as such the refreshes can be removed in the code, which will go faster as it is done automatically by the new api --- addons/mrp/procurement.py | 1 - addons/procurement/procurement.py | 1 - addons/stock/stock.py | 19 +++++-------------- addons/stock_account/stock_account.py | 1 - 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/addons/mrp/procurement.py b/addons/mrp/procurement.py index 0c17cd1cc76..58727d9c160 100644 --- a/addons/mrp/procurement.py +++ b/addons/mrp/procurement.py @@ -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') diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index 12d40a39be1..085d620cc87 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -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) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index b124d348b87..823d8249a8c 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -404,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: @@ -414,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): @@ -527,7 +525,6 @@ class stock_quant(osv.osv): 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) - quant.refresh() return self.browse(cr, uid, new_quant, context=context) def _get_latest_move(self, cr, uid, quant, context=None): @@ -877,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: @@ -1207,6 +1203,7 @@ class stock_picking(osv.osv): 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 = [] @@ -1222,7 +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, @@ -1382,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 @@ -1410,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) @@ -2211,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 @@ -2616,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 @@ -3362,7 +3356,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) @@ -3482,7 +3475,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 @@ -3624,7 +3616,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 diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index 9707263391a..243d982f8f6 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -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 From 2592f149a0f0e1ba77c2fa8c429aa344b3b19d36 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Tue, 2 Dec 2014 16:50:15 +0100 Subject: [PATCH 12/25] [FIX] mail: model can be None or uninstalled This is related to rev. 83b28737e9277d0612211900d49f539bb631dfb6 --- addons/mail/wizard/mail_compose_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 61a867c20e3..4fc7de78333 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -73,7 +73,7 @@ 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 hasattr(self.pool[result['model']], 'message_post'): + 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 From b374c51474c3b9976c6b6334f281bc84f370a2f3 Mon Sep 17 00:00:00 2001 From: Abhishek Jaiswal Date: Tue, 23 Sep 2014 10:23:53 +0530 Subject: [PATCH 13/25] [ADD] document @default_order in list views closes #2623 --- doc/reference/views.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 3f5b2ea2043..bfb49c81261 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -132,6 +132,14 @@ root can have the following attributes: `'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 + + ``colors`` allows changing the color of a row's text based on the corresponding record's attributes. From bd2633753d854cc499c09c02d47d75f09dfa379b Mon Sep 17 00:00:00 2001 From: Mario Arias Badila Date: Sun, 5 Oct 2014 14:25:08 -0600 Subject: [PATCH 14/25] [ADD] pyeval: date.replace method Needed to create filters like "previous month" It was just defined for "datetime", but is also needed for "date" closes #2915 --- addons/web/static/src/js/pyeval.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index bad98e7b2a0..57e2eaa1499 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -479,6 +479,19 @@ && 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; From 640e77eaa3c2091ec387d0a16aa24608cf4ad7a2 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 2 Dec 2014 16:46:29 +0100 Subject: [PATCH 15/25] [FIX] base: deleting a view must not delete linked actions --- openerp/addons/base/ir/ir_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 168537f334c..9112fc4f947 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -271,7 +271,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, From bc24d1b010540f16217003e222a3469bc9712a92 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 2 Dec 2014 17:00:44 +0100 Subject: [PATCH 16/25] [FIX] product: force empty view_id for action `product_variant_action` --- addons/product/product_view.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index 7e8dcd2b5b2..c764ab06045 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -215,6 +215,7 @@ form {'search_default_product_tmpl_id': [active_id], 'default_product_tmpl_id': active_id} +

Click to define a new product. From ffda02329595faf38dd6fb337217fd28772ba565 Mon Sep 17 00:00:00 2001 From: Mack Date: Tue, 11 Nov 2014 22:26:33 +0800 Subject: [PATCH 17/25] [FIX] http.py: no traceback for SessionExpiredException Rebase and close #3618 --- openerp/http.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openerp/http.py b/openerp/http.py index 343520433ea..eddbcb669a3 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -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): From 536d00d2ba3fae4ed0e24b621d7ca98917a7a260 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Tue, 2 Dec 2014 17:24:26 +0100 Subject: [PATCH 18/25] [FIX] Avoid endless loop when doing a return picking --- addons/stock/wizard/stock_return_picking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock/wizard/stock_return_picking.py b/addons/stock/wizard/stock_return_picking.py index 112b233bf1f..deb624ea797 100644 --- a/addons/stock/wizard/stock_return_picking.py +++ b/addons/stock/wizard/stock_return_picking.py @@ -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: From d82aa69cccd7ce91389c915c8ffb6b0559db2ca5 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 2 Dec 2014 14:54:24 +0100 Subject: [PATCH 19/25] [IMP] models: add check for common conversion error in field definitions --- openerp/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openerp/models.py b/openerp/models.py index 33327ef7279..ad91afeb5d1 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -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__: From b6f3795e33a2d560ccf319f11e933c8628dab0f9 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Tue, 2 Dec 2014 18:15:43 +0100 Subject: [PATCH 20/25] [IMP] Manufacture should be by default in all warehouses --- addons/mrp/stock.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/mrp/stock.py b/addons/mrp/stock.py index a5f62b3d3c0..e22105edd42 100644 --- a/addons/mrp/stock.py +++ b/addons/mrp/stock.py @@ -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') From c7b6a962395d6791db54695e46f52f66fbfccf25 Mon Sep 17 00:00:00 2001 From: Grover Menacho Date: Thu, 27 Nov 2014 10:08:43 -0400 Subject: [PATCH 21/25] [FIX] action_confirm was not asking for purchase pricelist, otherwise it was asking for sale pricelist --- addons/purchase/stock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py index d62cbb5a43c..9257c8d3942 100644 --- a/addons/purchase/stock.py +++ b/addons/purchase/stock.py @@ -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, From 4bcb31f7060149831a26e030e64ee54b71e456b6 Mon Sep 17 00:00:00 2001 From: Yajo Date: Tue, 2 Dec 2014 18:58:57 +0100 Subject: [PATCH 22/25] [FIX] packaging: redhat dependencies Install psycogreen from easy_install (not in EPEL), required for openerp-gevent --- setup/redhat/postinstall.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/redhat/postinstall.sh b/setup/redhat/postinstall.sh index c7e5e9d8eb2..04ea928b13a 100644 --- a/setup/redhat/postinstall.sh +++ b/setup/redhat/postinstall.sh @@ -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 From 894a898e9e119ce0a101994c40cceae076a2bf13 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 3 Dec 2014 10:44:05 +0100 Subject: [PATCH 23/25] [FIX] models: exists() should not consider record with id 0 as existing --- openerp/addons/base/tests/test_orm.py | 12 ++++++++++++ openerp/models.py | 11 ++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/tests/test_orm.py b/openerp/addons/base/tests/test_orm.py index c6bb9310acb..0c877a43622 100644 --- a/openerp/addons/base/tests/test_orm.py +++ b/openerp/addons/base/tests/test_orm.py @@ -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', diff --git a/openerp/models.py b/openerp/models.py index ad91afeb5d1..3f1bf70a54e 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -4774,14 +4774,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.")) From 2275ab5d952b90779e1363fa2cc4f8927586c812 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 3 Dec 2014 14:20:51 +0100 Subject: [PATCH 24/25] [FIX] doc: remaining instances of ruby-style interpolation in templates fixes #3157 --- doc/howtos/backend/exercise-creation | 2 +- doc/howtos/backend/exercise-kanban | 4 ++-- doc/howtos/web.rst | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/howtos/backend/exercise-creation b/doc/howtos/backend/exercise-creation index 0fdb3e3c73a..4b780bddbb7 100644 --- a/doc/howtos/backend/exercise-creation +++ b/doc/howtos/backend/exercise-creation @@ -133,7 +133,7 @@ Index: addons/openacademy/templates.xml + + + -+ ++ + + + diff --git a/doc/howtos/backend/exercise-kanban b/doc/howtos/backend/exercise-kanban index 892ef6e0059..7d69770d33e 100644 --- a/doc/howtos/backend/exercise-kanban +++ b/doc/howtos/backend/exercise-kanban @@ -30,9 +30,9 @@ Index: addons/openacademy/views/openacademy.xml + + +

++ oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}"> +
+ +
diff --git a/doc/howtos/web.rst b/doc/howtos/web.rst index 8dc89d8f549..ba8337ffd34 100644 --- a/doc/howtos/web.rst +++ b/doc/howtos/web.rst @@ -1742,7 +1742,7 @@ attributes are:

-

+

From 2e092ace29d88e0ea892708b93dffa94a283e771 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 3 Dec 2014 14:48:54 +0100 Subject: [PATCH 25/25] [FIX] must manage() Environment before loading test file --- openerp/service/server.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openerp/service/server.py b/openerp/service/server.py index 5ea05df8112..43f9aadcbe7 100644 --- a/openerp/service/server.py +++ b/openerp/service/server.py @@ -907,10 +907,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