diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index 4c1b8de12be..01a4bf9e7e6 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -542,7 +542,8 @@ class account_bank_statement_line(osv.osv): ('reconcile_id', '=', False), ('state', '=', 'valid'), ('account_id.reconcile', '=', True), - ('id', 'not in', excluded_ids)] + ('id', 'not in', excluded_ids), + ('partner_id', 'in', (False, st_line.partner_id.id))] return domain def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None): diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 350f4d22da2..355d1235290 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -742,6 +742,7 @@ class account_invoice(models.Model): else: line['currency_id'] = False line['amount_currency'] = False + line['price'] = self.currency_id.round(line['price']) line['ref'] = ref if self.type in ('out_invoice','in_refund'): total += line['price'] diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py index 313dd54f4db..d5bad8ee582 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis.py +++ b/addons/account_analytic_analysis/account_analytic_analysis.py @@ -691,6 +691,7 @@ class account_analytic_account(osv.osv): 'fiscal_position': fpos_id, 'payment_term': partner_payment_term, 'company_id': contract.company_id.id or False, + 'user_id': contract.manager_id.id or uid, } return invoice diff --git a/addons/account_analytic_plans/security/ir.model.access.csv b/addons/account_analytic_plans/security/ir.model.access.csv index 9ba40d5c1c3..7a8d6e29067 100644 --- a/addons/account_analytic_plans/security/ir.model.access.csv +++ b/addons/account_analytic_plans/security/ir.model.access.csv @@ -2,5 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_account_analytic_plan,account.analytic.plan,model_account_analytic_plan,account.group_account_user,1,1,1,1 access_account_analytic_plan_line,account.analytic.plan.line,model_account_analytic_plan_line,account.group_account_user,1,1,1,1 access_account_analytic_plan_instance,account.analytic.plan.instance,model_account_analytic_plan_instance,account.group_account_user,1,1,1,1 +access_account_analytic_plan_instance_group_user,account.analytic.plan.instance,model_account_analytic_plan_instance,base.group_user,1,0,0,0 access_account_analytic_plan_instance_line,account.analytic.plan.instance.line,model_account_analytic_plan_instance_line,account.group_account_user,1,1,1,1 +access_account_analytic_plan_instance_line_group_user,account.analytic.plan.instance.line,model_account_analytic_plan_instance_line,base.group_user,1,0,0,0 access_account_analytic_plan_line_invoice,account.analytic.plan.line.invoice,model_account_analytic_plan_line,account.group_account_user,1,1,1,1 diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py index a1dffb15a00..9c7213ce84b 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py @@ -277,7 +277,10 @@ class account_analytic_line(osv.osv): for (key_id, company_id, currency_id), analytic_lines in invoice_grouping.items(): # key_id is an account.analytic.account - partner = analytic_lines[0].account_id.partner_id # will be the same for every line + account = analytic_lines[0].account_id + partner = account.partner_id # will be the same for every line + if (not partner) or not (currency_id): + raise UserError(_('Contract incomplete. Please fill in the Customer and Pricelist fields for %s.') % (account.name)) curr_invoice = self._prepare_cost_invoice(cr, uid, partner, company_id, currency_id, analytic_lines, context=context) invoice_context = dict(context, @@ -292,8 +295,6 @@ class account_analytic_line(osv.osv): invoice_lines_grouping = {} for analytic_line in analytic_lines: account = analytic_line.account_id - if (not partner) or not (account.pricelist_id): - raise UserError(_('Contract incomplete. Please fill in the Customer and Pricelist fields for %s.') % (account.name)) if not analytic_line.to_invoice: raise UserError(_('Trying to invoice non invoiceable line for %s.') % (analytic_line.product_id.name)) diff --git a/addons/l10n_ca/account_chart_template_en.xml b/addons/l10n_ca/account_chart_template_en.xml index 640811b7c1d..61a0b7f9ac6 100644 --- a/addons/l10n_ca/account_chart_template_en.xml +++ b/addons/l10n_ca/account_chart_template_en.xml @@ -1,9 +1,22 @@ + + + + + + + + + + + + + + - Canada - Chart of Accounts for english-speaking provinces @@ -16,18 +29,6 @@ - - - - - - - - - - - - property_stock_valuation_account_id diff --git a/addons/l10n_ca/account_chart_template_fr.xml b/addons/l10n_ca/account_chart_template_fr.xml index 4ca745601ca..06fd5242049 100644 --- a/addons/l10n_ca/account_chart_template_fr.xml +++ b/addons/l10n_ca/account_chart_template_fr.xml @@ -1,5 +1,19 @@ + + + + + + + + + + + + + + @@ -15,18 +29,6 @@ - - - - - - - - - - - - property_stock_valuation_account_id diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py index 237e1521c19..c0601ea29d5 100644 --- a/addons/mass_mailing/wizard/test_mailing.py +++ b/addons/mass_mailing/wizard/test_mailing.py @@ -33,6 +33,7 @@ class TestMassMailing(osv.TransientModel): 'body_html': '', 'notification': True, 'mailing_id': mailing.id, + 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], } mail_mail_obj = Mail.browse(cr, uid, Mail.create(cr, uid, mail_values, context=context), context=context) unsubscribe_url = Mail._get_unsubscribe_url(cr, uid, mail_mail_obj, test_mail, context=context) diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/default/keyboard b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/default/keyboard new file mode 100644 index 00000000000..2609b092bcc --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/default/keyboard @@ -0,0 +1,10 @@ +# KEYBOARD CONFIGURATION FILE + +# Consult the keyboard(5) manual page. + +XKBMODEL="pc105" +XKBLAYOUT="us" +XKBVARIANT="" +XKBOPTIONS="" + +BACKSPACE="guess" \ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh index 68b64fb94ca..781b599d51f 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh @@ -31,7 +31,7 @@ apt-get -y autoremove apt-get update apt-get -y dist-upgrade -PKGS_TO_INSTALL="adduser postgresql-client python python-dateutil python-decorator python-docutils python-feedparser python-imaging python-jinja2 python-ldap python-libxslt1 python-lxml python-mako python-mock python-openid python-passlib python-psutil python-psycopg2 python-pybabel python-pychart python-pydot python-pyparsing python-pypdf python-reportlab python-requests python-simplejson python-tz python-unittest2 python-vatnumber python-vobject python-werkzeug python-xlwt python-yaml postgresql python-gevent python-serial python-pip python-dev localepurge vim mc mg" +PKGS_TO_INSTALL="adduser postgresql-client python python-dateutil python-decorator python-docutils python-feedparser python-imaging python-jinja2 python-ldap python-libxslt1 python-lxml python-mako python-mock python-openid python-passlib python-psutil python-psycopg2 python-pybabel python-pychart python-pydot python-pyparsing python-pypdf python-reportlab python-requests python-simplejson python-tz python-unittest2 python-vatnumber python-vobject python-werkzeug python-xlwt python-yaml postgresql python-gevent python-serial python-pip python-dev localepurge vim mc mg screen" apt-get -y install ${PKGS_TO_INSTALL} @@ -48,14 +48,22 @@ pip install pyusb==1.0.0b1 pip install qrcode pip install evdev +groupadd usbusers +usermod -a -G usbusers pi usermod -a -G lp pi sudo -u postgres createuser -s pi mkdir /var/log/odoo chown pi:pi /var/log/odoo +echo "* * * * * rm /var/run/odoo/sessions/*" | crontab - + update-rc.d odoo defaults +# https://www.raspberrypi.org/forums/viewtopic.php?p=79249 +# to not have "setting up console font and keymap" during boot take ages +setupcon + # create dirs for ramdisks create_ramdisk_dir () { mkdir "${1}_ram" diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/99-usb.rules b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/99-usb.rules new file mode 100644 index 00000000000..7a282a8e6e1 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/99-usb.rules @@ -0,0 +1,2 @@ +SUBSYSTEM=="usb", GROUP="usbusers", MODE="0660" +SUBSYSTEMS=="usb", GROUP="usbusers", MODE="0660" \ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.inputrc b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.inputrc new file mode 100644 index 00000000000..131b24148c2 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.inputrc @@ -0,0 +1,53 @@ +# +set meta-flag on +set convert-meta off +set input-meta on +set output-meta on +set show-all-if-ambiguous on +set visible-stats on +set bell-style none +# set bell-style visible +#"\M-k": kill-whole-line +#---------------------------------------------------------- +# arrows left/right/up/down +"\e[C": forward-char +"\e[D": backward-char +"\e[A": previous-history +"\e[B": next-history +# --------------------------------------------------------- +# pgup/pgdn +"\e[6~": history-search-forward +"\e[5~": history-search-backward +#---------------------------------------------------------- +# insert,delete +#"\e[2~": quoted-insert +"\e[3~": delete-char +# esc+delete, esc+backspace +"\e\e[3~": kill-word +"\e\C-h": backward-kill-word +#---------------------------------------------------------- +# ctrl left/right (rxvt) +"\eOc": forward-word +"\eOd": backward-word +# ctrl left/right (xterm) +"\e[1;5C": forward-word +"\e[1;5D": backward-word +# ctrl left/right (unknown) +#"\e[5C": forward-word +#"\e[5D": backward-word +#"\e\e[C": forward-word +#"\e\e[D": backward-word +# --------------------------------------------------------- +# home/end (linux console) +"\e[1~": beginning-of-line +"\e[4~": end-of-line +# home/end (rxvt) +"\e[7~": beginning-of-line +"\e[8~": end-of-line +# home/end (xterm, freebsd console) +"\e[H": beginning-of-line +"\e[F": end-of-line +# home/end (non RH/Debian xterm, unknown) +"\eOH": beginning-of-line +"\eOF": end-of-line +"\eOw": end-of-line diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.vimrc b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.vimrc new file mode 100644 index 00000000000..9e65af338fa --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/.vimrc @@ -0,0 +1,27 @@ +set all& +set autoindent +set backspace=2 +set nobackup +set nocompatible +set noerrorbells +set noexpandtab +set expandtab +set number +set hidden +set history=500 +set hlsearch +set ignorecase +set modeline +set ruler +set shiftwidth=4 +set scrolloff=5 +set showcmd +set showmode +set tabstop=4 +set textwidth=0 +set visualbell +set t_vb= +set wrap +set list +set listchars=tab:~.,trail:.,extends:>,precedes:< +set viminfo="NONE" diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 5075388aa28..aa901325ec6 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -737,8 +737,12 @@ class purchase_order(osv.osv): #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency price_unit = self.pool.get('res.currency').compute(cr, uid, order.currency_id.id, order.company_id.currency_id.id, price_unit, round=False, context=context) res = [] + if order.location_id.usage == 'customer': + name = order_line.product_id.name + else: + name = order_line.name or '' move_template = { - 'name': order_line.name or '', + 'name': name, 'product_id': order_line.product_id.id, 'product_uom': order_line.product_uom.id, 'product_uos': order_line.product_uom.id, @@ -1470,6 +1474,9 @@ class procurement_order(osv.osv): return qty, price + def update_origin_po(self, cr, uid, po, proc, context=None): + pass + def _get_grouping_dicts(self, cr, uid, ids, context=None): """ It will group the procurements according to the pos they should go into. That way, lines going to the same @@ -1586,6 +1593,9 @@ class procurement_order(osv.osv): self.message_post(cr, uid, [proc.id], body=_("Purchase line created and linked to an existing Purchase Order"), context=context) po_obj.write(cr, uid, [add_purchase], {'order_line': line_values},context=context) + for procurment in procuremnts: + self.update_origin_po(cr, uid, po, procurement, context=context) + # Create new purchase orders partner_obj = self.pool.get("res.partner") diff --git a/addons/purchase_analytic_plans/purchase_analytic_plans_view.xml b/addons/purchase_analytic_plans/purchase_analytic_plans_view.xml index 28ca76e8430..ea16ae891b3 100644 --- a/addons/purchase_analytic_plans/purchase_analytic_plans_view.xml +++ b/addons/purchase_analytic_plans/purchase_analytic_plans_view.xml @@ -8,7 +8,7 @@ - + @@ -19,7 +19,7 @@ - + diff --git a/addons/stock/product_view.xml b/addons/stock/product_view.xml index 052dff573a2..c9ec15fd845 100644 --- a/addons/stock/product_view.xml +++ b/addons/stock/product_view.xml @@ -8,7 +8,7 @@ - red:virtual_available<0;blue:virtual_available>=0 and state in ('draft', 'end', 'obsolete');black:virtual_available>=0 and state not in ('draft', 'end', 'obsolete') + red:virtual_available<0;blue:virtual_available>=0 and state in ('draft', 'end', 'obsolete');black:virtual_available>=0 and state not in ('draft', 'end', 'obsolete') @@ -23,7 +23,7 @@ - red:virtual_available<0;blue:virtual_available>=0 and state in ('draft', 'end', 'obsolete');black:virtual_available>=0 and state not in ('draft', 'end', 'obsolete') + red:virtual_available<0;blue:virtual_available>=0 and state in ('draft', 'end', 'obsolete');black:virtual_available>=0 and state not in ('draft', 'end', 'obsolete') @@ -145,7 +145,7 @@ - {'default_groups_ref': ['base.group_user', 'base.group_sale_manager', 'stock.group_stock_manager']} + {'default_groups_ref': ['base.group_user', 'base.group_sale_manager', 'stock.group_stock_manager']} diff --git a/addons/stock_dropshipping/stock_dropshipping.py b/addons/stock_dropshipping/stock_dropshipping.py index 09238d37b12..a68d0520acd 100644 --- a/addons/stock_dropshipping/stock_dropshipping.py +++ b/addons/stock_dropshipping/stock_dropshipping.py @@ -43,3 +43,12 @@ class purchase_order(models.Model): if not self.env.context.get('no_invoice_policy_check'): self._check_invoice_policy() super(purchase_order, self).wkf_confirm_order() + +class procurement_order(models.Model): + _inherit = 'procurement.order' + + @api.model + def update_origin_po(self, po, proc): + super(procurement_order, self).update_origin_po(po, proc) + if proc.sale_line_id and not (proc.origin in po.origin): + po.sudo().write({'origin': po.origin+', '+proc.origin}) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 894faec7e51..13a856907af 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -315,7 +315,7 @@ def login_redirect(): # built the redirect url, keeping all the query parameters of the url redirect_url = '%s?%s' % (request.httprequest.base_url, werkzeug.urls.url_encode(request.params)) return """ """ % (url, redirect_url) diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 7a4f511b485..c0043c945a5 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -357,10 +357,6 @@ body > nav.navbar { } /* ----- BOOTSTRAP FIX ----- */ -.col-md-12 { - float: left; - width: 100%; -} @-moz-document url-prefix() { .table .img-responsive { diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index 52f8d62c9ab..a53e5254708 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -251,10 +251,6 @@ body > nav.navbar /* ----- BOOTSTRAP FIX ----- */ -.col-md-12 - float: left - width: 100% - @-moz-document url-prefix() .table .img-responsive width: 100% diff --git a/doc/cla/corporate/ITLibertas.md b/doc/cla/corporate/ITLibertas.md new file mode 100644 index 00000000000..06626c6d5ad --- /dev/null +++ b/doc/cla/corporate/ITLibertas.md @@ -0,0 +1,17 @@ +Russia, 2015-08-14 + +IT Libertas agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +IT Libertas LLC info@itlibertas.com https://github.com/itlibertas + +List of contributors: + +Denis Baranov baranov@itlibertas.com https://github.com/barachka +Tatiana Deribina deribina@itlibertas.com https://github.com/Sapfiriana +Aleksandr Shevelev shevelev@itlibertas.com https://github.com/alexalv diff --git a/doc/cla/individual/flh.md b/doc/cla/individual/flh.md new file mode 100644 index 00000000000..90860f3e0c4 --- /dev/null +++ b/doc/cla/individual/flh.md @@ -0,0 +1,11 @@ +France, 2015-08-18 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Florian Hatat github@hatat.me https://github.com/flh diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index 6181ee19c31..cc2e01890b2 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -108,11 +108,9 @@ class ir_http(osv.AbstractModel): datas = attach[0]['datas'] or '' name = attach[0]['name'] - if not datas: - if name.startswith(('http://', 'https://', '/')): - return werkzeug.utils.redirect(name, 301) - else: - return werkzeug.wrappers.Response(status=204) # NO CONTENT + if (not datas and name != request.httprequest.path and + name.startswith(('http://', 'https://', '/'))): + return werkzeug.utils.redirect(name, 301) response = werkzeug.wrappers.Response() server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT diff --git a/openerp/addons/base/tests/test_search.py b/openerp/addons/base/tests/test_search.py index af14fc88e85..6cb8490546d 100644 --- a/openerp/addons/base/tests/test_search.py +++ b/openerp/addons/base/tests/test_search.py @@ -5,6 +5,18 @@ import openerp.tests.common as common class test_search(common.TransactionCase): + def patch_order(self, model, order): + m_e = self.env[model] + m_r = self.registry(model) + + old_order = m_e._order + + @self.addCleanup + def cleanup(): + m_r._order = type(m_e)._order = old_order + + m_r._order = type(m_e)._order = order + def test_00_search_order(self): registry, cr, uid = self.registry, self.cr, self.uid @@ -96,15 +108,13 @@ class test_search(common.TransactionCase): self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order') # Do: order on many2one, but not by specifying in order parameter of search, but by overriding _order of res_users - old_order = users_obj._order - users_obj._order = 'country_id desc, name asc, login desc' + self.patch_order('res.users', 'country_id desc, name asc, login desc') user_ids = users_obj.search(cr, search_user, []) expected_ids = [search_user, c, b, a] test_user_ids = filter(lambda x: x in expected_ids, user_ids) self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order') - users_obj._order = old_order - def test_11_indirect_inherits_m2order(self): + def test_11_indirect_inherits_m2o_order(self): registry, cr, uid = self.registry, self.cr, self.uid Cron = registry('ir.cron') Users = registry('res.users') @@ -119,6 +129,48 @@ class test_search(common.TransactionCase): expected_ids = [cron_ids[l] for l in 'ABC'] self.assertEqual(ids, expected_ids) + def test_12_m2o_order_loop_self(self): + registry, cr, uid = self.registry, self.cr, self.uid + + Cats = registry('ir.module.category') + ids = {} + def create(name, **kw): + ids[name] = Cats.create(cr, uid, dict(kw, name=name)) + + self.patch_order('ir.module.category', 'parent_id desc, name') + + create('A') + create('B', parent_id=ids['A']) + create('C', parent_id=ids['A']) + create('D') + create('E', parent_id=ids['D']) + create('F', parent_id=ids['D']) + + expected_order = [ids[x] for x in 'ADEFBC'] + domain = [('id', 'in', ids.values())] + search_result = Cats.search(cr, uid, domain) + self.assertEqual(search_result, expected_order) + + def test_13_m2o_order_loop_multi(self): + Users = self.env['res.users'] + + # will sort by login desc of the creator, then by name + self.patch_order('res.partner', 'create_uid, name') + self.patch_order('res.users', 'partner_id, login desc') + + kw = dict(groups_id=[(6, 0, [self.ref('base.group_system'), + self.ref('base.group_partner_manager')])]) + + u1 = Users.create(dict(name='Q', login='m', **kw)).id + u2 = Users.sudo(user=u1).create(dict(name='B', login='f', **kw)).id + u3 = Users.create(dict(name='C', login='c', **kw)).id + u4 = Users.sudo(user=u2).create(dict(name='D', login='z', **kw)).id + + expected_order = [u2, u4, u3, u1] + + domain = [('id', 'in', [u1, u2, u3, u4])] + search_result = list(Users.search(domain)._ids) + self.assertEqual(search_result, expected_order) if __name__ == '__main__': unittest2.main() diff --git a/openerp/models.py b/openerp/models.py index e101da0b5ec..046c406f939 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -4536,7 +4536,7 @@ class BaseModel(object): apply_rule(rule_where_clause, rule_where_clause_params, rule_tables, parent_model=inherited_model) - def _generate_m2o_order_by(self, alias, order_field, query, reverse_direction): + def _generate_m2o_order_by(self, alias, order_field, query, reverse_direction, seen): """ Add possibly missing JOIN to ``query`` and generate the ORDER BY clause for m2o fields, either native m2o fields or function/related fields that are stored, including @@ -4546,18 +4546,18 @@ class BaseModel(object): """ if order_field not in self._columns and order_field in self._inherit_fields: # also add missing joins for reaching the table containing the m2o field - qualified_field = self._inherits_join_calc(alias, order_field, query) order_field_column = self._inherit_fields[order_field][2] + qualified_field = self._inherits_join_calc(alias, order_field, query) + alias, order_field = qualified_field.replace('"', '').split('.', 1) else: - qualified_field = '"%s"."%s"' % (alias, order_field) order_field_column = self._columns[order_field] assert order_field_column._type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()' if not order_field_column._classic_write and not getattr(order_field_column, 'store', False): - _logger.debug("Many2one function/related fields must be stored " \ - "to be used as ordering fields! Ignoring sorting for %s.%s", - self._name, order_field) - return + _logger.debug("Many2one function/related fields must be stored " + "to be used as ordering fields! Ignoring sorting for %s.%s", + self._name, order_field) + return [] # figure out the applicable order_by for the m2o dest_model = self.pool[order_field_column._obj] @@ -4568,12 +4568,14 @@ class BaseModel(object): # Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here # as we don't want to exclude results that have NULL values for the m2o - src_table, src_field = qualified_field.replace('"', '').split('.', 1) - dst_alias, dst_alias_statement = query.add_join((src_table, dest_model._table, src_field, 'id', src_field), implicit=False, outer=True) + join = (alias, dest_model._table, order_field, 'id', order_field) + dst_alias, dst_alias_statement = query.add_join(join, implicit=False, outer=True) return dest_model._generate_order_by_inner(dst_alias, m2o_order, query, - reverse_direction=reverse_direction) + reverse_direction=reverse_direction, seen=seen) - def _generate_order_by_inner(self, alias, order_spec, query, reverse_direction=False): + def _generate_order_by_inner(self, alias, order_spec, query, reverse_direction=False, seen=None): + if seen is None: + seen = set() order_by_elements = [] self._check_qorder(order_spec) for order_part in order_spec.split(','): @@ -4594,7 +4596,10 @@ class BaseModel(object): inner_clauses = ['"%s"."%s"' % (alias, order_field)] add_dir = True elif order_column._type == 'many2one': - inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse) + key = (self._name, order_column._obj, order_field) + if key not in seen: + seen.add(key) + inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse, seen) else: continue # ignore non-readable or "non-joinable" fields elif order_field in self._inherit_fields: @@ -4604,7 +4609,10 @@ class BaseModel(object): inner_clauses = [self._inherits_join_calc(alias, order_field, query)] add_dir = True elif order_column._type == 'many2one': - inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse) + key = (parent_obj._name, order_column._obj, order_field) + if key not in seen: + seen.add(key) + inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse, seen) else: continue # ignore non-readable or "non-joinable" fields else: