From: Christophe Simonis Date: Wed, 3 Dec 2014 14:18:36 +0000 (+0100) Subject: [MERGE] forward port of branch 8.0 up to 2e092ac X-Git-Url: http://git.inspyration.org/?a=commitdiff_plain;h=e17222a38d2d4cd32057a59e65be82edd103836c;hp=4720c8898b24a0eb60232697f827bed3ebcf40ab;p=odoo%2Fodoo.git [MERGE] forward port of branch 8.0 up to 2e092ac --- diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index 9724451..e56f4be 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 diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 04decd6..4fc7de7 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 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' diff --git a/addons/mrp/procurement.py b/addons/mrp/procurement.py index 0c17cd1..58727d9 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/mrp/stock.py b/addons/mrp/stock.py index a5f62b3..e22105e 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') diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index 12e731e..ee7c502 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/product/product.py b/addons/product/product.py index b5c99c8..589ff9e 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."), @@ -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/product/product_view.xml b/addons/product/product_view.xml index 0b5015d..f07769b 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -217,6 +217,7 @@ form {'search_default_product_tmpl_id': [active_id], 'default_product_tmpl_id': active_id} +

Click to define a new product. @@ -624,7 +625,7 @@ - + diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py index d62cbb5..9257c8d 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, diff --git a/addons/stock/product.py b/addons/stock/product.py index 41335b9..23f4d3f 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 bdc78e3..c6c53fd 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, 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), diff --git a/addons/stock/wizard/stock_return_picking.py b/addons/stock/wizard/stock_return_picking.py index 112b233..deb624e 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: diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index 9707263..243d982 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 diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index bad98e7..57e2eaa 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; diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst index b72540c..e1ba28e 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 1287315..ac84e34 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-creation b/doc/howtos/backend/exercise-creation index 0fdb3e3..4b780bd 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-dashboard b/doc/howtos/backend/exercise-dashboard index 7c76e69..bf175ed 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/backend/exercise-kanban b/doc/howtos/backend/exercise-kanban index 892ef6e..7d69770 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/themes.rst b/doc/howtos/themes.rst index 35cb439..2c6eef2 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 384297d..ba8337f 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
- +

@@ -1732,7 +1742,7 @@ attributes are:

-

+

diff --git a/doc/howtos/website.rst b/doc/howtos/website.rst index 2187956..5092cac 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 diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 3f5b2ea..bfb49c8 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. diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 943fa09..9fe6093 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -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, diff --git a/openerp/addons/base/tests/test_orm.py b/openerp/addons/base/tests/test_orm.py index c6bb931..0c877a4 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/http.py b/openerp/http.py index 3435204..eddbcb6 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): diff --git a/openerp/models.py b/openerp/models.py index 559d97c..ac935c5 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__: @@ -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.")) diff --git a/openerp/service/server.py b/openerp/service/server.py index 2189cc6..9d10e29 100644 --- a/openerp/service/server.py +++ b/openerp/service/server.py @@ -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 diff --git a/setup/redhat/postinstall.sh b/setup/redhat/postinstall.sh index c7e5e9d..04ea928 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