From bb90e55fb5f8e38f3c80495a96e747306de9edd4 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 25 Nov 2015 15:50:28 +0100 Subject: [PATCH 01/82] [FIX] product_visible_discount: corrected price This applies the commits 503820a and f26b94fd (and their subsequent corrections) to the calculation of the discount. Indeed, the calculation of the discount must take into account the corrected price, otherwise the discount is wrongly computed. opw-656604 --- .../product_visible_discount.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/addons/product_visible_discount/product_visible_discount.py b/addons/product_visible_discount/product_visible_discount.py index d5f53197e0b..549be580530 100644 --- a/addons/product_visible_discount/product_visible_discount.py +++ b/addons/product_visible_discount/product_visible_discount.py @@ -20,6 +20,7 @@ # ############################################################################## +from openerp import SUPERUSER_ID from openerp.osv import fields, osv from openerp.tools.translate import _ @@ -80,6 +81,7 @@ class sale_order_line(osv.osv): result=res['value'] pricelist_obj=self.pool.get('product.pricelist') product_obj = self.pool.get('product.product') + account_tax_obj = self.pool.get('account.tax') if product and pricelist and self.pool.get('res.users').has_group(cr, uid, 'sale.group_discount_per_so_line'): if result.get('price_unit',False): price=result['price_unit'] @@ -94,6 +96,16 @@ class sale_order_line(osv.osv): so_pricelist = pricelist_obj.browse(cr, uid, pricelist, context=context) new_list_price, currency_id = get_real_price_curency(list_price, product.id, qty, uom, pricelist) + + # The superuser is used by website_sale in order to create a sale order. We need to make + # sure we only select the taxes related to the company of the partner. This should only + # apply if the partner is linked to a company. + if uid == SUPERUSER_ID and context.get('company_id'): + taxes = product.taxes_id.filtered(lambda r: r.company_id.id == context['company_id']) + else: + taxes = product.taxes_id + new_list_price = account_tax_obj._fix_tax_included_price(cr, uid, new_list_price, taxes, result.get('tax_id', [])) + if so_pricelist.visible_discount and list_price[pricelist][0] != 0 and new_list_price != 0: if product.company_id and so_pricelist.currency_id.id != product.company_id.currency_id.id: # new_list_price is in company's currency while price in pricelist currency From fd56268c6ca4e8a6f94f16de9129bd12be87303f Mon Sep 17 00:00:00 2001 From: Cedric Snauwaert Date: Thu, 26 Nov 2015 09:42:31 +0100 Subject: [PATCH 02/82] [FIX] web: list_view, context when doing name_get call in m2m was not compute correctly --- addons/web/static/src/js/views/list_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index 86ae1790632..cb6daf093ce 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -1177,7 +1177,7 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{ ids = value; } new Model(column.relation) - .call('name_get', [ids, this.dataset.context]).done(function (names) { + .call('name_get', [ids, this.dataset.get_context()]).done(function (names) { // FIXME: nth horrible hack in this poor listview record.set(column.id + '__display', _(names).pluck(1).join(', ')); From 5c0fd9780e1509e0c62ee33e1c305d1d01530f21 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 26 Nov 2015 09:52:29 +0100 Subject: [PATCH 03/82] [FIX] calendar: duplicated term --- addons/calendar/i18n/calendar.pot | 5 ----- 1 file changed, 5 deletions(-) diff --git a/addons/calendar/i18n/calendar.pot b/addons/calendar/i18n/calendar.pot index 28c48a2b77c..3190ec2b1d0 100644 --- a/addons/calendar/i18n/calendar.pot +++ b/addons/calendar/i18n/calendar.pot @@ -1464,11 +1464,6 @@ msgstr "" msgid "count cannot be negative or 0." msgstr "" -#. module: calendar -#: field:calendar.alarm,duration_minutes:0 -msgid "Duration in minutes" -msgstr "" - #. module: calendar #: code:addons/calendar/calendar.py:1245 #, python-format From 2f0135c8206c8cc3e97677987f707af3be8f84b9 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Fri, 20 Nov 2015 11:18:09 -0800 Subject: [PATCH 04/82] [FIX] product: resets fields applied_on: The field applied_on in pricelist rules is not taken into account: - In the price computation - In the name computation When creating a pricelist item, if you select (applied_on = Product) first and set a product, then select (applied_on = Global), and save the rule, the computation will still take the product_tmpl_id in the price computation. Instead of fixing the whole _price_rule_get_multi method, this patch sets the fields that should not be taken into account to False. compute_price: Add a behavior similar to the applied_on field, to reset unnecessary fields to zero when the computation method is changed. Original idea from @tgr-odoo opw-657148 Closes #9671 --- addons/product/pricelist.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py index ffbdfaa23b2..ab8c84f6433 100644 --- a/addons/product/pricelist.py +++ b/addons/product/pricelist.py @@ -324,6 +324,18 @@ class product_pricelist_item(osv.osv): class product_pricelist_item_new(models.Model): _inherit = "product.pricelist.item" + _applied_on_field_map = { + '0_product_variant': 'product_id', + '1_product': 'product_tmpl_id', + '2_product_category': 'categ_id', + } + + _compute_price_field_map = { + 'fixed': ['fixed_price'], + 'percentage': ['percent_price'], + 'formula': ['price_discount', 'price_surcharge', 'price_round', 'price_min_margin', 'price_max_margin'], + } + @api.one @api.depends('categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \ 'pricelist_id', 'percent_price', 'price_discount', 'price_surcharge') @@ -347,3 +359,16 @@ class product_pricelist_item_new(models.Model): #functional fields used for usability purposes name = Fields.Char(compute='_get_pricelist_item_name_price', string='Name', multi='item_name_price', help="Explicit rule name for this pricelist line.") price = Fields.Char(compute='_get_pricelist_item_name_price', string='Price', multi='item_name_price', help="Explicit rule name for this pricelist line.") + + @api.onchange('applied_on') + def _onchange_applied_on(self): + for applied_on, field in self._applied_on_field_map.iteritems(): + if self.applied_on != applied_on: + setattr(self, field, False) + + @api.onchange('compute_price') + def _onchange_compute_price(self): + for compute_price, field in self._compute_price_field_map.iteritems(): + if self.compute_price != compute_price: + for f in field: + setattr(self, f, 0.0) From b459c4dc0324e2a3949d02ae246f4233cd6131ea Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Nov 2015 10:02:41 +0100 Subject: [PATCH 05/82] [FIX] website: possibility to set no url for the home menu Unsetting the URL of the menu `Home`, in Settings > Configuration > Website Settings > Configure Website, leaded to the unavailability of the website. opw-657572 --- addons/website/controllers/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 7ef9dbe38b2..3862d19f0f8 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -40,9 +40,9 @@ class Website(openerp.addons.web.controllers.main.Home): else: first_menu = main_menu.child_id and main_menu.child_id[0] if first_menu: - if not (first_menu.url.startswith(('/page/', '/?', '/#')) or (first_menu.url=='/')): + if first_menu.url and (not (first_menu.url.startswith(('/page/', '/?', '/#')) or (first_menu.url == '/'))): return request.redirect(first_menu.url) - if first_menu.url.startswith('/page/'): + if first_menu.url and first_menu.url.startswith('/page/'): return request.registry['ir.http'].reroute(first_menu.url) return self.page(page) From 001e558178801fc76956ad866fb3ef0df1074738 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 26 Nov 2015 10:09:51 +0100 Subject: [PATCH 06/82] [FIX] calendar: do not forwardport .pot content --- addons/calendar/i18n/calendar.pot | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/addons/calendar/i18n/calendar.pot b/addons/calendar/i18n/calendar.pot index 52a1881c53f..cabab319b3c 100644 --- a/addons/calendar/i18n/calendar.pot +++ b/addons/calendar/i18n/calendar.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 9.0\n" +"Project-Id-Version: Odoo Server 9.0c\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-11-25 13:28+0000\n" -"PO-Revision-Date: 2015-11-25 13:28+0000\n" +"POT-Creation-Date: 2015-11-26 09:05+0000\n" +"PO-Revision-Date: 2015-11-26 09:05+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -381,26 +381,26 @@ msgid "${object.event_id.name} - Reminder" msgstr "" #. module: calendar -#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form -#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form_popup -msgid " hours" -msgstr "" - -#. module: calendar -#: code:addons/calendar/calendar.py:802 +#: code:addons/calendar/calendar.py:775 #, python-format msgid "%s at %s To\n" " %s at %s (%s)" msgstr "" #. module: calendar -#: code:addons/calendar/calendar.py:800 +#: code:addons/calendar/calendar.py:773 #, python-format msgid "%s at (%s To %s) (%s)" msgstr "" #. module: calendar -#: code:addons/calendar/calendar.py:1628 +#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form +#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form_popup +msgid " hours" +msgstr "" + +#. module: calendar +#: code:addons/calendar/calendar.py:1620 #, python-format msgid "A email has been send to specify that the date has been changed !" msgstr "" @@ -526,13 +526,6 @@ msgstr "" msgid "Click to schedule a new meeting." msgstr "" -#. module: calendar -#: view:calendar.alarm:calendar.view_calendar_alarm_tree -#: model:ir.actions.act_window,name:calendar.action_calendar_alarm -#: model:ir.ui.menu,name:calendar.menu_calendar_alarm -msgid "Calendar Alarm" -msgstr "" - #. module: calendar #: model:ir.model.fields,field_description:calendar.field_calendar_event_color_partner_id msgid "Color index of creator" From a98db073846b44de2d5abe956784d980e4cccfe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 25 Nov 2015 10:37:39 +0100 Subject: [PATCH 07/82] [FIX] mail: fix various search-related problems This commit tries to correctly use messages from the cache instead of all messages. This means that we need to correctly add messages to the domain-specific caches, and correctly invalidate them when necessary. Also, to avoid increasing the algorithmical complexity of the process, particular care was made to avoid scanning too many lists. For example, the channel caches have now a messages field, instead of message_ids. --- addons/mail/static/src/js/chat_manager.js | 61 ++++++++++++++++------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index f297a751fa9..db6d4a6b018 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -39,6 +39,13 @@ function add_message (data, options) { messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg); _.each(msg.channel_ids, function (channel_id) { var channel = chat_manager.get_channel(channel_id); + + if (channel) { + add_to_cache(msg, []); + if (options.domain && options.domain !== []) { + add_to_cache(msg, options.domain); + } + } if (channel && channel.hidden) { channel.hidden = false; chat_manager.bus.trigger('new_channel', channel); @@ -61,6 +68,8 @@ function add_message (data, options) { if (!options.silent) { chat_manager.bus.trigger('new_message', msg); } + } else if (options.domain && options.domain !== []) { + add_to_cache(msg, options.domain); } return msg; } @@ -207,7 +216,7 @@ function make_channel (data, options) { cache: {'[]': { all_history_loaded: false, loaded: false, - message_ids: [], + messages: [], }}, }; if (channel.type === "channel" && data.public !== "private") { @@ -229,17 +238,39 @@ function get_channel_cache (channel, domain) { channel.cache[stringified_domain] = { all_history_loaded: false, loaded: false, - message_ids: [], + messages: [], }; } return channel.cache[stringified_domain]; } +function invalidate_caches(channel_ids) { + _.each(channel_ids, function (channel_id) { + var channel = chat_manager.get_channel(channel_id); + if (channel) { + channel.cache = { '[]': channel.cache['[]']}; + } + }); +} + +function add_to_cache(message, domain) { + _.each(message.channel_ids, function (channel_id) { + var channel = chat_manager.get_channel(channel_id); + if (channel) { + var channel_cache = get_channel_cache(channel, domain); + var index = _.sortedIndex(channel_cache.messages, message, 'id'); + if (channel_cache.messages[index] !== message) { + channel_cache.messages.splice(index, 0, message); + } + } + }); +} + function remove_message_from_channel (channel_id, message) { message.channel_ids = _.without(message.channel_ids, channel_id); var channel = _.findWhere(channels, { id: channel_id }); _.each(channel.cache, function (cache) { - cache.message_ids = _.without(cache.message_ids, message.id); + cache.messages = _.without(cache.messages, message); }); } @@ -256,28 +287,21 @@ function fetch_from_channel (channel, options) { domain = new data.CompoundDomain(domain, options.domain || []); } if (options.load_more) { - var min_message_id = _.chain(messages) - .filter(function (msg) { return _.contains(cache.message_ids, msg.id); }) - .pluck("id") - .min() - .value(); - + var min_message_id = cache.messages[0].id; domain = new data.CompoundDomain([['id', '<', min_message_id]], domain); } return MessageModel.call('message_fetch', [domain], {limit: LIMIT}).then(function (msgs) { - cache.message_ids = _.uniq(cache.message_ids.concat(_.pluck(msgs, 'id'))); if (!cache.all_history_loaded) { cache.all_history_loaded = msgs.length < LIMIT; } cache.loaded = true; _.each(msgs, function (msg) { - add_message(msg, {channel_id: channel.id, silent: true}); - }); - return _.filter(messages, function (m) { - return _.contains(cache.message_ids, m.id); + add_message(msg, {channel_id: channel.id, silent: true, domain: options.domain}); }); + var channel_cache = get_channel_cache(channel, options.domain || []); + return channel_cache.messages; }); } @@ -335,6 +359,7 @@ function on_notification (notification) { function on_needaction_notification (message) { message = add_message(message, { channel_id: 'channel_inbox', show_notification: true} ); + invalidate_caches(message.channel_ids); needaction_counter++; _.each(message.channel_ids, function (channel_id) { var channel = chat_manager.get_channel(channel_id); @@ -360,6 +385,7 @@ function on_channel_notification (message) { } }); add_message(message, { show_notification: true }); + invalidate_caches(message.channel_ids); }); } @@ -382,6 +408,7 @@ function on_toggle_star_notification (data) { _.each(data.message_ids, function (msg_id) { var message = _.findWhere(messages, { id: msg_id }); if (message) { + invalidate_caches(message.channel_ids); message.is_starred = data.starred; if (!message.is_starred) { remove_message_from_channel("channel_starred", message); @@ -395,6 +422,7 @@ function on_mark_as_read_notification (data) { _.each(data.message_ids, function (msg_id) { var message = _.findWhere(messages, { id: msg_id }); if (message) { + invalidate_caches(message.channel_ids); remove_message_from_channel("channel_inbox", message); chat_manager.bus.trigger('update_message', message); } @@ -419,6 +447,7 @@ function on_mark_as_unread_notification (data) { _.each(data.message_ids, function (message_id) { var message = _.findWhere(messages, { id: message_id }); if (message) { + invalidate_caches(message.channel_ids); add_channel_to_message(message, 'channel_inbox'); } }); @@ -460,9 +489,7 @@ var chat_manager = { var channel = this.get_channel(options.channel_id); var channel_cache = get_channel_cache(channel, options.domain); if (channel_cache.loaded) { - return $.when(_.filter(messages, function (message) { - return _.contains(message.channel_ids, options.channel_id); - })); + return $.when(channel_cache.messages); } else { return fetch_from_channel(channel, {domain: options.domain}); } From 5b82c9ccb400c93a2a405a69786dbca148141526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 25 Nov 2015 14:41:11 +0100 Subject: [PATCH 08/82] [FIX] mail: better heuristic for displaying origin channel --- addons/mail/static/src/js/chat_manager.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index db6d4a6b018..d638ddaf45f 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -127,12 +127,14 @@ function make_message (data) { if (_.contains(data.starred_partner_ids, session.partner_id)) { msg.is_starred = true; } - var real_channels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred'); - var origin = real_channels.length ? real_channels[0] : undefined; - var channel = origin && chat_manager.get_channel(origin); - if (channel) { - msg.origin_id = origin; - msg.origin_name = channel.name; + if (msg.model === 'mail.channel') { + var real_channels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred'); + var origin = real_channels.length === 1 ? real_channels[0] : undefined; + var channel = origin && chat_manager.get_channel(origin); + if (channel) { + msg.origin_id = origin; + msg.origin_name = channel.name; + } } return msg; } From 78ea8eb0586ff108bde4da3c144ff48b73dbedf8 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 26 Nov 2015 10:40:59 +0100 Subject: [PATCH 09/82] [FIX] core: correcly handle pidfile deletion. As atexit function are inherited by subprocess, the pidfile was always deleted when the first worker (http or cron) died. Now, only the process that created the pidfile will delete it. --- openerp/cli/server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openerp/cli/server.py b/openerp/cli/server.py index f3932b4bf60..d532a067b5b 100644 --- a/openerp/cli/server.py +++ b/openerp/cli/server.py @@ -80,9 +80,9 @@ def report_configuration(): ('database user', config['db_user'])]: _logger.info("%s: %s", name, value) -def rm_pid_file(): +def rm_pid_file(main_pid): config = openerp.tools.config - if not openerp.evented and config['pidfile']: + if config['pidfile'] and main_pid == os.getpid(): try: os.unlink(config['pidfile']) except OSError: @@ -95,10 +95,10 @@ def setup_pid_file(): """ config = openerp.tools.config if not openerp.evented and config['pidfile']: + pid = os.getpid() with open(config['pidfile'], 'w') as fd: - pidtext = "%d" % (os.getpid()) - fd.write(pidtext) - atexit.register(rm_pid_file) + fd.write(str(pid)) + atexit.register(rm_pid_file, pid) def export_translation(): From adf6d0e557dd952c6366b9010638e4fe52563803 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Nov 2015 11:06:58 +0100 Subject: [PATCH 10/82] [FIX] sale_layout: remove `categ_sequence` default value. `categ_sequence` are stored related fields, to `sale_layout_cat_id.sequence`. Setting a default value for them has as side effect to rewrite the value on the related field. To reproduce the issue: - Create a sale layout category, with 10 as sequence - Create an invoice with one line, with this sale layout category - Come back to the sale layout category, notice that the sequence has been changed to 0. Besides, as the sequence was rewritten on the sale layout category, the sequence was rewritten on all sale order lines and invoice lines having this sale layout category. If you have a bunch of them, this could take a while. The `categ_sequence` default value is supposed to come from the sale layout category. opw-651937 --- addons/sale_layout/models/sale_layout.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/addons/sale_layout/models/sale_layout.py b/addons/sale_layout/models/sale_layout.py index ed42fabb991..d2b5777c059 100644 --- a/addons/sale_layout/models/sale_layout.py +++ b/addons/sale_layout/models/sale_layout.py @@ -84,10 +84,6 @@ class AccountInvoiceLine(osv.Model): sale_layout_cat_id = openerp.fields.Many2one('sale_layout.category', string='Section') categ_sequence = openerp.fields.Integer(related='sale_layout_cat_id.sequence', string='Layout Sequence', store=True) - _defaults = { - 'categ_sequence': 0 - } - class SaleOrder(osv.Model): _inherit = 'sale.order' @@ -117,10 +113,6 @@ class SaleOrderLine(osv.Model): # Store is intentionally set in order to keep the "historic" order. } - _defaults = { - 'categ_sequence': 0 - } - _order = 'order_id, categ_sequence, sale_layout_cat_id, sequence, id' def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None): From bef0da3a7aff74fecce7b418164257ad0cd37c4d Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 26 Nov 2015 11:55:00 +0100 Subject: [PATCH 11/82] [FIX] account: Total Receivable and Payable The fields Total Receivable and Payable are calculated as debit-credit. However, this leads to a wrong computation if, for example, a payment is used to pay several invoices. For example: - Create a customer invoice of 2500 USD - Register a payment of 3000 USD The Total Receivable will be 3000 USD instead of 500 USD. At the same time, this commits changes the sign of the field. The help depicts the Total Receivable field as "Total amount this customer owes you". Therefore, one would expect that if the customer owes 250 USD, the Total Receivable is 250 USD, not -250 USD. The same logic applies to Total Payable. opw-657053 --- addons/account/models/partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 39cf7a0a17f..f681c48ed0e 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -232,7 +232,7 @@ class ResPartner(models.Model): def _credit_debit_get(self): tables, where_clause, where_params = self.env['account.move.line']._query_get() where_params = [tuple(self.ids)] + where_params - self._cr.execute("""SELECT l.partner_id, act.type, SUM(l.debit-l.credit) + self._cr.execute("""SELECT l.partner_id, act.type, SUM(l.amount_residual) FROM account_move_line l LEFT JOIN account_account a ON (l.account_id=a.id) LEFT JOIN account_account_type act ON (a.user_type_id=act.id) From da667b573ddb9a4151deba07babba70e323d1973 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Fri, 4 Sep 2015 12:44:56 +0200 Subject: [PATCH 12/82] [FIX] stock: barcode interface, do not reload locations all the time Locations were search & read each time you changed the quantity of a product in a picking. Once the locations loaded the firs time, this is unlikely the locations will change during the operation. It shoudln't, at least. Therefore, for performances, we avoid to load the locations each time the picking is reloaded. opw-648529 Fixes #8344 --- addons/stock/static/src/js/widgets.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/stock/static/src/js/widgets.js b/addons/stock/static/src/js/widgets.js index 3b877dfb9f4..161a16be339 100644 --- a/addons/stock/static/src/js/widgets.js +++ b/addons/stock/static/src/js/widgets.js @@ -690,6 +690,9 @@ function openerp_picking_widgets(instance){ } return loaded_picking.then(function(){ + if (!_.isEmpty(self.locations)){ + return $.when(); + } return new instance.web.Model('stock.location').call('search',[[['usage','=','internal']]]).then(function(locations_ids){ return new instance.web.Model('stock.location').call('read',[locations_ids, []]).then(function(locations){ self.locations = locations; From 7137891a8a5d024a7f36a5af1d3a3c2686a9a96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 26 Nov 2015 10:48:29 +0100 Subject: [PATCH 13/82] [FIX] mail, im_livechat: correctly displayed message date Messages are required to be displayed in a smarter way: relative to the current date, if it's a recent message, or absolute if not. This commits rework most of the date processing code to comply with these requirements. Messages were preprocessed each time a thread is rendered, which is stupid. It should be preprocessed only when the message is created. This was a small problem because livechat does not use the chat manager, so a simplified version of the preprocessing had to be done. --- .../im_livechat/static/src/js/im_livechat.js | 19 +++++++++-- addons/mail/static/src/js/chat_manager.js | 26 +++++++++++++- addons/mail/static/src/js/thread.js | 34 ++++++------------- addons/mail/static/src/xml/thread.xml | 14 ++++---- 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index e3609946525..1344172fc5d 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -128,15 +128,28 @@ var LivechatButton = Widget.extend({ }, add_message: function (data) { - this.messages.push({ + var msg = { id: data.id, attachment_ids: data.attachment_ids, author_id: data.author_id, body: data.body, - date: data.date, + date: moment(time.str_to_datetime(data.date)), is_needaction: false, is_note: data.is_note, - }); + }; + + // Compute displayed author name or email + msg.displayed_author = msg.author_id && msg.author_id[1] || + this.options.default_username; + + // Compute the avatar_url + if (msg.author_id && msg.author_id[0]) { + msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; + } else { + msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg"; + } + + this.messages.push(msg); }, render_messages: function () { diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index d638ddaf45f..331fd6772a5 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -6,6 +6,7 @@ var core = require('web.core'); var data = require('web.data'); var Model = require('web.Model'); var session = require('web.session'); +var time = require('web.time'); var web_client = require('web.web_client'); var _t = core._t; @@ -80,7 +81,7 @@ function make_message (data) { author_id: data.author_id, body_short: data.body_short || "", body: data.body || "", - date: data.date, + date: moment(time.str_to_datetime(data.date)), message_type: data.message_type, subtype_description: data.subtype_description, is_note: data.is_note, @@ -136,6 +137,29 @@ function make_message (data) { msg.origin_name = channel.name; } } + + // Compute displayed author name or email + if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) { + msg.mailto = msg.email_from; + } else { + msg.displayed_author = msg.author_id && msg.author_id[1] || + msg.email_from || _t('Anonymous'); + } + + // Compute the avatar_url + if (msg.author_id && msg.author_id[0]) { + msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; + } else if (msg.message_type === 'email') { + msg.avatar_src = "/mail/static/src/img/email_icon.png"; + } else { + msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg"; + } + + // Compute url of attachments + _.each(msg.attachment_ids, function(a) { + a.url = '/web/content/' + a.id + '?download=true'; + }); + return msg; } diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 6fd5838aa48..1f2404dfe47 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -2,7 +2,6 @@ odoo.define('mail.ChatThread', function (require) { "use strict"; var core = require('web.core'); -var time = require('web.time'); var Widget = require('web.Widget'); var QWeb = core.qweb; @@ -49,7 +48,6 @@ var Thread = Widget.extend({ display_order: ORDER.ASC, display_needactions: true, display_stars: true, - default_username: _t('Anonymous'), display_document_link: true, display_avatar: true, squash_close_messages: true, @@ -67,7 +65,7 @@ var Thread = Widget.extend({ // one are both comments wrote by the same author at the same minute var prev_msg; _.each(msgs, function (msg) { - if (!prev_msg || (Math.abs(moment(msg.date).diff(prev_msg.date)) > 60000) || + if (!prev_msg || (Math.abs(msg.date.diff(prev_msg.date)) > 60000) || prev_msg.message_type !== 'comment' || msg.message_type !== 'comment' || (prev_msg.author_id[0] !== msg.author_id[0])) { msg.display_author = true; @@ -113,31 +111,19 @@ var Thread = Widget.extend({ var msg = _.extend({}, message); // Set the date in the browser timezone - msg.date = moment(time.str_to_datetime(msg.date)).format('YYYY-MM-DD HH:mm:ss'); + var date = msg.date.format('YYYY-MM-DD'); - // Compute displayed author name or email - if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) { - msg.mailto = msg.email_from; + if (date === moment().format('YYYY-MM-DD')) { + msg.day = _t("Today"); + msg.hour = msg.date.fromNow(); + } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) { + msg.day = _t("Yesterday"); + msg.hour = msg.date.format('hh:mm'); } else { - msg.displayed_author = msg.author_id && msg.author_id[1] || - msg.email_from || - this.options.default_username; + msg.day = msg.date.format('LL'); + msg.hour = msg.date.format('hh:mm'); } - // Compute the avatar_url - if (msg.author_id && msg.author_id[0]) { - msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; - } else if (msg.message_type === 'email') { - msg.avatar_src = "/mail/static/src/img/email_icon.png"; - } else { - msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg"; - } - - // Compute url of attachments - _.each(msg.attachment_ids, function(a) { - a.url = '/web/content/' + a.id + '?download=true'; - }); - return msg; }, diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 394627940cb..70996dd7d47 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -30,11 +30,11 @@ -
+
- + - +
@@ -56,8 +56,8 @@ t-att-src="message.avatar_src" class="o_thread_message_avatar"/> - - + + - - - + + - on From 8c29e7f17a3885875199bc5605eb08a0ff7c0f85 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Nov 2015 15:00:00 +0100 Subject: [PATCH 14/82] [FIX] sale: `Sales` filter The filter `Sales` in the sales orders search views was defined as `[('state','in',('progress','Done'))]` but - progress no longer exists. It's `sale` now. - `Done` is the label, `done` is the value opw-657552 --- addons/sale/sale_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index 1f3c71b01dc..f755eaf56b3 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -361,7 +361,7 @@ - + From 971ea77a447b21ea4857f4cb3f53414d52a047cd Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 26 Nov 2015 14:58:02 +0100 Subject: [PATCH 15/82] [FIX] website_forum: typo, entity need double escaping --- addons/website_forum/views/website_forum.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index 2575599c4de..e890cd774df 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -25,7 +25,7 @@ From dd8052a7ce93f79423ea6a0a23ef454fa3915f65 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 26 Nov 2015 15:12:31 +0100 Subject: [PATCH 16/82] [I18N] website_forum: reexport source terms Remove incorrect term from calendar and add missing sentence --- addons/website_forum/i18n/website_forum.pot | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/addons/website_forum/i18n/website_forum.pot b/addons/website_forum/i18n/website_forum.pot index 8196f8ae988..85a5822230e 100644 --- a/addons/website_forum/i18n/website_forum.pot +++ b/addons/website_forum/i18n/website_forum.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 9.0\n" +"Project-Id-Version: Odoo Server 9.0c\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-10-09 09:20+0000\n" -"PO-Revision-Date: 2015-10-09 09:20+0000\n" +"POT-Creation-Date: 2015-11-26 14:10+0000\n" +"PO-Revision-Date: 2015-11-26 14:10+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -222,13 +222,19 @@ msgid "awarded users" msgstr "" #. module: website_forum -#: code:addons/website_forum/models/forum.py:419 +#: code:addons/website_forum/models/forum.py:483 #, python-format msgid "

A new answer for %s has been posted. Click here to access the post.

" msgstr "" #. module: website_forum -#: code:addons/website_forum/models/forum.py:425 +#: code:addons/website_forum/models/forum.py:499 +#, python-format +msgid "

A new question %s has been asked on %s and require your validation. Click here to access the question.

" +msgstr "" + +#. module: website_forum +#: code:addons/website_forum/models/forum.py:489 #, python-format msgid "

A new question %s has been asked on %s. Click here to access the question.

" msgstr "" @@ -1621,11 +1627,6 @@ msgstr "" msgid "Last activity date" msgstr "" -#. module: website_forum -#: model:ir.model.fields,field_description:website_forum.field_res_users_calendar_last_notif_ack -msgid "Last notification marked as read from base Calendar" -msgstr "" - #. module: website_forum #: model:ir.ui.view,arch_db:website_forum.header msgid "Last updated:" @@ -2058,7 +2059,7 @@ msgstr "" #. module: website_forum #: model:ir.ui.view,arch_db:website_forum.header_footer_custom -msgid "Q&A" +msgid "Q&amp;A" msgstr "" #. module: website_forum From 86015ac1c5722cb9cbe1f5c97a031a37bec5321e Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 26 Nov 2015 14:25:43 +0100 Subject: [PATCH 17/82] [FIX] purchase: add missing drop ship address The drop ship address was missing from the view, making impossible to specify the address where the products needed to be delivered. The fix adds the missing field to the view, and make it visible (and required) if the delivery must be done at a customer location. opw-657054 --- addons/purchase/purchase.py | 7 +++++++ addons/purchase/purchase_view.xml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 894b7a33fcc..182018fa361 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -149,6 +149,8 @@ class PurchaseOrder(models.Model): picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\ help="This will determine picking type of incoming shipment") + default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage',\ + help="Technical field used to display the Drop Ship Address") group_id = fields.Many2one('procurement.group', string="Procurement Group") @api.model @@ -207,6 +209,11 @@ class PurchaseOrder(models.Model): self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id return {} + @api.onchange('picking_type_id') + def _onchange_picking_type_id(self): + if self.picking_type_id.default_location_dest_id.usage != 'customer': + self.dest_address_id = False + @api.multi def action_rfq_send(self): ''' diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index 0c8cd03ff82..fb59784e6d1 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -251,6 +251,8 @@ + + From 8be7eed9ca325e2c12ed0da741bb307640369969 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Nov 2015 15:36:11 +0100 Subject: [PATCH 18/82] [FIX] sale, portal_sale: hide buttons for portal users When a portal user opened his quotation form, all buttons of the regular form were available. They were not working, due the access rights, but it's better to hide them. opw-657654 --- addons/portal_sale/portal_sale_view.xml | 3 +++ addons/sale/wizard/sale_make_invoice_advance.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/addons/portal_sale/portal_sale_view.xml b/addons/portal_sale/portal_sale_view.xml index 8600d9e4062..825d2349f5a 100644 --- a/addons/portal_sale/portal_sale_view.xml +++ b/addons/portal_sale/portal_sale_view.xml @@ -20,6 +20,9 @@ +
+ +
diff --git a/addons/sale/wizard/sale_make_invoice_advance.xml b/addons/sale/wizard/sale_make_invoice_advance.xml index 1425990d52c..397e14ad3ee 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.xml +++ b/addons/sale/wizard/sale_make_invoice_advance.xml @@ -47,6 +47,7 @@ form form new + From 1b8c9aed9f516df1c7a51db41e083df2e36b4bf5 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Nov 2015 13:53:55 +0100 Subject: [PATCH 19/82] [FIX] models: custom fields seen as base fields This was possible to create custom fields `x_*` but seen as base fields. For instance, - Go to Settings > Technical > Database Structure > Fields - Select a field (any) - Click on the model link, to be redirected to the model form - Edit & add a custom field from there. - Save - Notice that the field you just added is saved as a base field. We solve this issue by assuming that all created fields and models are customs, except the ones created by the ORM, by the database initialization, the fields coming from the modules in python. We therefore remove the mechanism on which a field was set as custom according to the fact `manual` was set to True within the context: This is now the case by default. No change was required for the base fields: The `state` `base` was already forced for those fields, that are created using direct SQL requests `INSERT INTO`. opw-657312 --- addons/web_view_editor/static/src/js/view_editor.js | 2 +- openerp/addons/base/ir/ir_model.py | 10 ++-------- openerp/addons/base/ir/ir_model_view.xml | 6 +++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/addons/web_view_editor/static/src/js/view_editor.js b/addons/web_view_editor/static/src/js/view_editor.js index 96cdcc6f5b7..fdedb82f7b6 100644 --- a/addons/web_view_editor/static/src/js/view_editor.js +++ b/addons/web_view_editor/static/src/js/view_editor.js @@ -1007,7 +1007,7 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({ render_new_field :function( result ) { var self = this; var action = { - context: {'default_model_id': result.id, 'manual': true, 'module' : result.model}, + context: {'default_model_id': result.id, 'module' : result.model}, res_model: "ir.model.fields", views: [[false, 'form']], type: 'ir.actions.act_window', diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index da73a3ab52b..ab21f4b8d50 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -129,7 +129,7 @@ class ir_model(osv.osv): _defaults = { 'model': 'x_', - 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base', + 'state': 'manual', } def _check_model_name(self, cr, uid, ids, context=None): @@ -207,8 +207,6 @@ class ir_model(osv.osv): def create(self, cr, user, vals, context=None): if context is None: context = {} - if context and context.get('manual'): - vals['state']='manual' res = super(ir_model,self).create(cr, user, vals, context) if vals.get('state','base')=='manual': # add model in registry @@ -283,7 +281,7 @@ class ir_model_fields(osv.osv): 'selection': "", 'domain': "[]", 'name': 'x_', - 'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base', + 'state': 'manual', 'on_delete': 'set null', 'select_level': '0', 'field_description': '', @@ -369,8 +367,6 @@ class ir_model_fields(osv.osv): vals['model'] = model_data.model if context is None: context = {} - if context and context.get('manual',False): - vals['state'] = 'manual' if vals.get('ttype', False) == 'selection': if not vals.get('selection',False): raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!')) @@ -409,8 +405,6 @@ class ir_model_fields(osv.osv): def write(self, cr, user, ids, vals, context=None): if context is None: context = {} - if context and context.get('manual',False): - vals['state'] = 'manual' #For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later if 'serialization_field_id' in vals or 'name' in vals: diff --git a/openerp/addons/base/ir/ir_model_view.xml b/openerp/addons/base/ir/ir_model_view.xml index bdd5868ce03..5a2547f8e43 100644 --- a/openerp/addons/base/ir/ir_model_view.xml +++ b/openerp/addons/base/ir/ir_model_view.xml @@ -41,7 +41,7 @@
- + @@ -142,7 +142,7 @@ Models ir.model form - {'manual':True} + {} @@ -237,7 +237,7 @@ Fields ir.model.fields form - {'manual':True} + {} From 53cb608b178ec7604ce736770320437a6af93488 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 26 Nov 2015 15:46:37 +0100 Subject: [PATCH 20/82] [FIX] purchase: missing string Even if the field is used for technical purpose only, it is still better to define a string for the translation. --- addons/purchase/purchase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 182018fa361..9d14a8d2ed1 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -149,7 +149,7 @@ class PurchaseOrder(models.Model): picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\ help="This will determine picking type of incoming shipment") - default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage',\ + default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\ help="Technical field used to display the Drop Ship Address") group_id = fields.Many2one('procurement.group', string="Procurement Group") From bbc67ec4024369074aee8ceda2367a719a66fce2 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Thu, 26 Nov 2015 14:18:39 +0100 Subject: [PATCH 21/82] [FIX] web: export data according to domain Some accounting model (i.e. account.tax) are redefining the ORM search method, but with misordered arguments (placing count as fifth argument instead of context). For the export function, this lead to have the "context" variable associated to the "count" function variable. As "count" is then considered as True, the method returns the number of records instead of the list of record ids. This then exported the last record (the count'th one) instead of all the records that matched the domain. The solution here is to explicitly give the context variable by name. --- addons/web/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index f0f455be1e3..b2cb698a256 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1408,7 +1408,7 @@ class ExportFormat(object): Model = request.session.model(model) context = dict(request.context or {}, **params.get('context', {})) - ids = ids or Model.search(domain, 0, False, False, context) + ids = ids or Model.search(domain, 0, False, False, context=context) if not request.env[model]._is_an_ordinary_table(): fields = [field for field in fields if field['name'] != 'id'] From 70847ac5c04baa9414d0c6922a5b206016fb57a0 Mon Sep 17 00:00:00 2001 From: "Xavier, Sodexis" Date: Mon, 23 Nov 2015 13:36:13 +0530 Subject: [PATCH 22/82] [FIX] account_analytic_default: Default analytic account for invoice on delivery When creating an invoice from a DO, if there is no analytical account defined on the SO, then use the default analytical account for that product. Closes #9725 opw-657492 --- .../account_analytic_default.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/addons/account_analytic_default/account_analytic_default.py b/addons/account_analytic_default/account_analytic_default.py index c28a3d104b8..5f1bdaaa6b9 100644 --- a/addons/account_analytic_default/account_analytic_default.py +++ b/addons/account_analytic_default/account_analytic_default.py @@ -153,4 +153,18 @@ class product_template(osv.Model): result['context'] = "{}" return result + +class stock_move(osv.Model): + _inherit = 'stock.move' + + def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None): + # It will set the default analtyic account on the invoice line + partner_id = self.pool['account.invoice'].browse(cr, uid, invoice_line_vals.get('invoice_id'), context=context).partner_id.id + if 'account_analytic_id' not in invoice_line_vals or not invoice_line_vals.get('account_analytic_id'): + rec = self.pool['account.analytic.default'].account_get(cr, uid, move.product_id.id, partner_id, uid, time.strftime('%Y-%m-%d'), company_id=move.company_id.id, context=context) + if rec: + invoice_line_vals.update({'account_analytic_id': rec.analytic_id.id}) + res = super(stock_move, self)._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context) + return res + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 75534d358c8ce666b16091b9982379a2df87fe66 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 25 Nov 2015 17:26:00 +0100 Subject: [PATCH 23/82] [FIX] account: Bank statement reconciliation It was impossible to make a partial reconciliation with several lines. Each time a new line is proposed for the reconciliation, the previous partial reconcialtion is canceled. The partial reconciliation can just be proposed on the last selected line when: self.get("balance") * (last_line.debit - last_line.credit)<0 opw:656501 --- .../account/static/src/js/account_widgets.js | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/addons/account/static/src/js/account_widgets.js b/addons/account/static/src/js/account_widgets.js index a15b2c05e64..eb83681c8cb 100644 --- a/addons/account/static/src/js/account_widgets.js +++ b/addons/account/static/src/js/account_widgets.js @@ -1339,12 +1339,14 @@ openerp.account = function (instance) { // Show or hide partial reconciliation if (self.get("mv_lines_selected").length > 0) { + var last_line = _.last(self.get("mv_lines_selected")); var propose_partial = self.getCreatedLines().length === 0 - && self.get("mv_lines_selected").length === 1 && balance_type === "greater" - && ! self.get("mv_lines_selected")[0].partial_reconcile - && ! self.is_rapprochement; - self.get("mv_lines_selected")[0].propose_partial_reconcile = propose_partial; + && Math.abs(self.get("balance")) < Math.abs(last_line.debit - last_line.credit) + && self.get("balance") * (last_line.debit - last_line.credit) < 0 + && ! last_line.partial_reconcile + && ! last_line.already_paid; + last_line.propose_partial_reconcile = propose_partial; self.updateAccountingViewMatchedLines(); } @@ -1443,6 +1445,12 @@ openerp.account = function (instance) { mvLinesSelectedChanged: function(elt, val) { var self = this; + + // Reset partial reconciliation + _.each(self.get("mv_lines_selected"), function(line) { + if (line.partial_reconcile === true) self.unpartialReconcileLine(line); + if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false; + }); var added_lines = _.difference(val.newValue, val.oldValue); var removed_lines = _.difference(val.oldValue, val.newValue); @@ -1593,15 +1601,6 @@ openerp.account = function (instance) { var mv_lines_selected = self.get("mv_lines_selected"); var lines_selected_num = mv_lines_selected.length; - // Undo partial reconciliation if necessary - if (lines_selected_num !== 1) { - _.each(mv_lines_selected, function(line) { - if (line.partial_reconcile === true) self.unpartialReconcileLine(line); - if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false; - }); - self.updateAccountingViewMatchedLines(); - } - // Compute balance var balance = 0; balance -= self.st_line.amount; From 8aeae17d78578819fa1eb6f85b8e6c092baeb4f4 Mon Sep 17 00:00:00 2001 From: qdp-odoo Date: Thu, 26 Nov 2015 16:42:14 +0100 Subject: [PATCH 24/82] [REF] sale, sale_stock: added some comments to clear some aspects of the code related to the last changes on SO (salepocalypse) --- addons/sale/sale.py | 4 +++- addons/sale_stock/sale_stock.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 559f2ebfa79..3ae2f928a44 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -486,7 +486,9 @@ class SaleOrderLine(models.Model): def _get_invoice_qty(self): """ Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. Note - that this is the case only if the refund is generated from the SO. + that this is the case only if the refund is generated from the SO and that is intentional: if + a refund made would automatically decrease the invoiced quantity, then there is a risk of reinvoicing + it automatically, which may not be wanted at all. That's why the refund has to be created from the SO """ for line in self: qty_invoiced = 0.0 diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 72bf6b0843d..4f417d31e05 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -159,10 +159,14 @@ class SaleOrderLine(models.Model): @api.multi def _get_delivered_qty(self): + """Computes the delivered quantity on sale order lines, based on done stock moves related to its procurements + """ self.ensure_one() super(SaleOrderLine, self)._get_delivered_qty() qty = 0.0 for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped): + #Note that we don't decrease quantity for customer returns on purpose: these are exeptions that must be treated manually. Indeed, + #modifying automatically the delivered quantity may trigger an automatic reinvoicing (refund) of the SO, which is definitively not wanted if move.location_dest_id.usage == "customer": qty += self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom) return qty From e9dadadd07ca85409a0e37e4b62053deab8b59fb Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Thu, 26 Nov 2015 14:30:47 +0100 Subject: [PATCH 25/82] [FIX] account: Bank statement reconciliation The partial reconciliation can just be proposed on the last selected line when: self.get("balance") * (last_line.debit - last_line.credit)<0 --- addons/account/static/src/js/account_reconciliation_widgets.js | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js index 07847965f54..94ea596218f 100644 --- a/addons/account/static/src/js/account_reconciliation_widgets.js +++ b/addons/account/static/src/js/account_reconciliation_widgets.js @@ -1874,6 +1874,7 @@ var bankStatementReconciliationLine = abstractReconciliationLine.extend({ var propose_partial = self.getCreatedLines().length === 0 && balance_type === "greater" && Math.abs(self.get("balance")) < Math.abs(last_line.debit - last_line.credit) + && self.get("balance") * (last_line.debit - last_line.credit) < 0 && ! last_line.partial_reconcile && ! last_line.already_paid; last_line.propose_partial_reconcile = propose_partial; From 9d012d215726f3be375f661fefa460ad964a1b85 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 26 Nov 2015 11:39:31 +0100 Subject: [PATCH 26/82] [FIX] tools, base: typo for t-translation --- openerp/addons/base/tests/test_translate.py | 2 +- openerp/tools/translate.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/tests/test_translate.py b/openerp/addons/base/tests/test_translate.py index 8aab490e0fa..d6944cd24dd 100644 --- a/openerp/addons/base/tests/test_translate.py +++ b/openerp/addons/base/tests/test_translate.py @@ -119,7 +119,7 @@ class TranslationToolsTestCase(unittest.TestCase): terms = [] source = """
stuff before -
Do not translate this
+
Do not translate this
stuff after
""" result = xml_translate(terms.append, source) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 7a5f6452fa2..2ee09b12982 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -215,7 +215,7 @@ class XMLTranslator(object): if ( isinstance(node, SKIPPED_ELEMENT_TYPES) or node.tag in SKIPPED_ELEMENTS or - node.get("translation", "").strip() == "off" or + node.get("t-translation", "").strip() == "off" or node.tag == "attribute" and node.get("name") not in TRANSLATED_ATTRS ): # do not translate the contents of the node From 8423a0df3482567b0e2f77852dda14b80a029401 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 26 Nov 2015 12:01:51 +0100 Subject: [PATCH 27/82] [FIX] http: clear environment on checked called rollback If the current cursor transaction was not empty when using the check called endpoint, at the first call the cursor would be rollbacked but the environment could still have outdated values. This was an issue for example when selecting the route would trigger database insert, for instance when a model was used in the route and this model had also translatable prefetchable fields with tokens not already present in ir.translation opw-652045 --- openerp/http.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/http.py b/openerp/http.py index 54e54d1e597..6580d94d32f 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -305,6 +305,7 @@ class WebRequest(object): # case, the request cursor is unusable. Rollback transaction to create a new one. if self._cr: self._cr.rollback() + self.env.clear() result = self.endpoint(*a, **kw) if isinstance(result, Response) and result.is_qweb: # Early rendering of lazy responses to benefit from @service_model.check protection From fa015c4c2e085b4da75911855d7ea5ebebd23fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 26 Nov 2015 13:11:39 +0100 Subject: [PATCH 28/82] [FIX] website_forum: edit post with tags Tags are lost when editing a post as we handle the wrong parameter. Editing a post should not erase its tags anymore. --- addons/website_forum/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 2438767e075..4a607b6fee8 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -358,7 +358,7 @@ class WebsiteForum(http.Controller): def post_save(self, forum, post, **kwargs): if 'post_name' in kwargs and not kwargs.get('post_name').strip(): return request.website.render('website.http_error', {'status_code': _('Bad Request'), 'status_message': _('Title should not be empty.')}) - post_tags = forum._tag_to_write_vals(kwargs.get('post_tag', '')) + post_tags = forum._tag_to_write_vals(kwargs.get('post_tags', '')) vals = { 'tag_ids': post_tags, 'name': kwargs.get('post_name'), From 65b170b92c7bf26adef6deeb7ebbe69819669600 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Nov 2015 17:21:31 +0100 Subject: [PATCH 29/82] [FIX] analytic: missing depends to `report` The model `report.account.report_analyticjournal` inherit of the model `report.abstract_report`, which is defined in the module `report`. opw-657671 --- addons/analytic/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/analytic/__openerp__.py b/addons/analytic/__openerp__.py index 26ac96ebe8a..773d6a538d5 100644 --- a/addons/analytic/__openerp__.py +++ b/addons/analytic/__openerp__.py @@ -25,7 +25,7 @@ 'author' : 'OpenERP SA', 'website' : 'https://www.odoo.com/page/accounting', 'category': 'Hidden/Dependency', - 'depends' : ['base', 'decimal_precision', 'mail'], + 'depends' : ['base', 'decimal_precision', 'mail', 'report'], 'description': """ Module for defining analytic accounting object. =============================================== From 12f77fa22feb8e9fe81549049ca7abb6ade1b5b6 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Nov 2015 17:57:09 +0100 Subject: [PATCH 30/82] [FIX] mail: notify all partners of the thread As stated in the comment: ``` all notified_partner_ids of the mail.message have to be notified for the parented messages ``` Record rules are applied when browsing one2many fields. Therefore, when browsing `message.notified_partner_ids` with a user other than the SUPERUSER, the multi-company rules are applied, and a regular user could therefore not see all partners of the thread, according to which company the partners are associated with. Nevertheless, all partners of the thread have to be notified, including the ones the regular user cannot see. To reproduce the issue: - Create a second company 'Second company' - Create a third user, associated to the first company 'YourCompany' - Set the demo user as in the 'Second company' - Create a project 'test' in the first company, 'YourCompany' - In the followers of the project, add the Demo user, with as subtypes "Stages changes" only - As the third user, create a new task in this project - Change the stage of this task, as the third user [this is important] - Sign in as the demo user, and see that you cannot access your messages inbox, due to an access rights error. opw-650563 --- addons/mail/mail_message.py | 2 +- addons/mail/mail_thread.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index fbd04f21acc..ed288de1753 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -848,7 +848,7 @@ class mail_message(osv.Model): Call mail_notification.notify to manage the email sending """ notification_obj = self.pool.get('mail.notification') - message = self.browse(cr, uid, newid, context=context) + message = self.browse(cr, SUPERUSER_ID, newid, context=context) partners_to_notify = set([]) # all followers of the mail.message document have to be added as partners and notified if a subtype is defined (otherwise: log message) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index ee4eafc3edd..baef24003a4 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -1861,7 +1861,7 @@ class mail_thread(osv.AbstractModel): if msg_ids: notification_obj = self.pool.get('mail.notification') notification_obj._notify(cr, uid, msg_ids[0], partners_to_notify=partner_ids, context=context) - message = message_obj.browse(cr, uid, msg_ids[0], context=context) + message = message_obj.browse(cr, SUPERUSER_ID, msg_ids[0], context=context) if message.parent_id: partner_ids_to_parent_notify = set(partner_ids).difference(partner.id for partner in message.parent_id.notified_partner_ids) for partner_id in partner_ids_to_parent_notify: From f9a462f3e215f68f544f615cc16f0606f60194a9 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Nov 2015 16:54:48 +0100 Subject: [PATCH 31/82] [FIX] website: different assets_common according to user rights The content of an asset can change according to which modules are installed, but cannot change according to the user rights, according to which user is connected. Otherwise, the assets is re-generated and stored in the filesystem too often, which is against the concept of the assets. opw-657046 --- addons/website/views/website_templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index e5f205996f0..61c62b0daa2 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -6,7 +6,7 @@ From 5b60c30a6a3f2a385ae967f6e42b58b702ef39c0 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 26 Nov 2015 18:15:46 +0100 Subject: [PATCH 32/82] [FIX] l10n_eu_service: no reset of install wizard state Hardcoding the state to `open` is useless as it is the default state, and forces the state to reset upon every update, which is annoying. --- addons/l10n_eu_service/wizard/wizard.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/l10n_eu_service/wizard/wizard.xml b/addons/l10n_eu_service/wizard/wizard.xml index e419c027b2d..cd5cab6d490 100644 --- a/addons/l10n_eu_service/wizard/wizard.xml +++ b/addons/l10n_eu_service/wizard/wizard.xml @@ -59,7 +59,6 @@ automatic - open From 9ef3a9296d02ca27e728de1bb2582821269f1b26 Mon Sep 17 00:00:00 2001 From: qdp-odoo Date: Fri, 27 Nov 2015 11:05:12 +0100 Subject: [PATCH 33/82] [FIX] account_bank_statement_import: raise proper error (osv not defined) --- .../account_bank_statement_import.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/account_bank_statement_import/account_bank_statement_import.py b/addons/account_bank_statement_import/account_bank_statement_import.py index aebdc80644b..66a661b8cd6 100644 --- a/addons/account_bank_statement_import/account_bank_statement_import.py +++ b/addons/account_bank_statement_import/account_bank_statement_import.py @@ -129,7 +129,7 @@ class AccountBankStatementImport(models.TransientModel): if currency_code: currency = self.env['res.currency'].search([('name', '=ilike', currency_code)], limit=1) if not currency: - raise osv.except_osv(_("No currency found matching '%s'.") % currency_code) + raise UserError(_("No currency found matching '%s'.") % currency_code) if currency == company_currency: currency = False @@ -148,10 +148,10 @@ class AccountBankStatementImport(models.TransientModel): # If importing into an existing journal, its currency must be the same as the bank statement if journal: journal_currency = journal.currency_id - if currency == None: + if currency is None: currency = journal_currency if currency and currency != journal_currency: - statement_cur_code = currency == False and company_currency.name or currency.name + statement_cur_code = not currency and company_currency.name or currency.name journal_cur_code = not journal_currency and company_currency.name or journal_currency.name raise UserError(_('The currency of the bank statement (%s) is not the same as the currency of the journal (%s) !') % (statement_cur_code, journal_cur_code)) From db8203c27a21acdbcad2cf1c394b6fea3cf13688 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 24 Nov 2015 15:30:25 +0100 Subject: [PATCH 34/82] [FIX] base,hr,mail: prefetch mention suggestions The purpose of this commit is to improve the overall performance of the mention mechanism of the client action and chatter composer, by pre-fetching some data. First, the employees are fetched at chat initialization (basically, at web client initialization), if hr is installed. Second, when focusing on the composer of a channel, the members of this channel are fetched. In the case of a chatter, the members are already known as they are the followers of the document. When the user types a mention, we first display suggestions from the pre-fetched partners, and we only perform an RPC when there are no more result matching the search string. This RPC searches among user, and then partners if there aren't enough matches. A few side changes occurred: - auto_join set to True on user_ids field of res.partner to boost the query that searches partners that are users - the '/mail/read_followers' route now returns follower's email as well, as we need it for the mention; - a partner can now be mentionned several times in the same message; - mentionned partner previsualization has been removed, mainly because of the previous point, but also because it simplifies the code; --- addons/hr/__init__.py | 1 + addons/hr/res_partner.py | 22 ++++ addons/mail/controllers/main.py | 4 +- addons/mail/models/mail_channel.py | 11 ++ addons/mail/models/res_partner.py | 60 +++------ addons/mail/static/src/js/chat_manager.js | 23 ++++ addons/mail/static/src/js/chatter.js | 39 +++++- addons/mail/static/src/js/client_action.js | 11 +- addons/mail/static/src/js/composer.js | 124 ++++++++++-------- .../mail/static/src/less/client_action.less | 10 -- addons/mail/static/src/less/composer.less | 4 +- addons/mail/static/src/xml/chatter.xml | 2 +- addons/mail/static/src/xml/composer.xml | 33 +---- openerp/addons/base/res/res_partner.py | 2 +- 14 files changed, 201 insertions(+), 145 deletions(-) create mode 100644 addons/hr/res_partner.py diff --git a/addons/hr/__init__.py b/addons/hr/__init__.py index 17a97a0276f..07286aeea05 100644 --- a/addons/hr/__init__.py +++ b/addons/hr/__init__.py @@ -4,3 +4,4 @@ import hr import res_users +import res_partner diff --git a/addons/hr/res_partner.py b/addons/hr/res_partner.py new file mode 100644 index 00000000000..d3b33152853 --- /dev/null +++ b/addons/hr/res_partner.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +from openerp import api, models + +class Partner(models.Model): + _name = "res.partner" + _inherit = ['res.partner'] + + @api.model + def get_static_mention_suggestions(self): + """ Extend the mail's static mention suggestions by adding the employees. """ + suggestions = super(Partner, self).get_static_mention_suggestions() + + employee_group_id = self.env['ir.model.data'].xmlid_to_res_id('base.group_user') + self._cr.execute(""" + SELECT P.id, P.name, P.email + FROM res_users U + INNER JOIN res_groups_users_rel R ON U.id = R.uid + INNER JOIN res_partner P ON P.id = U.partner_id + WHERE R.gid = %s""", (employee_group_id,)) + suggestions.append(self._cr.dictfetchall()) + return suggestions diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index f623654dedf..7e4735bb6c5 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -44,6 +44,7 @@ class MailController(http.Controller): result.append({ 'id': follower.id, 'name': follower.partner_id.name or follower.channel_id.name, + 'email': follower.partner_id.email if follower.partner_id else None, 'res_model': 'res.partner' if follower.partner_id else 'mail.channel', 'res_id': follower.partner_id.id or follower.channel_id.id, 'is_editable': is_editable, @@ -241,6 +242,7 @@ class MailController(http.Controller): values = { 'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(), 'chatter_needaction_auto': request.env.user.chatter_needaction_auto, - 'channel_slots': request.env['mail.channel'].channel_fetch_slot() + 'channel_slots': request.env['mail.channel'].channel_fetch_slot(), + 'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(), } return values diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 2202a8c3ab6..6c5c5e3ac24 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -545,3 +545,14 @@ class Channel(models.Model): ]) ]) return self.search_read(domain, ['id', 'name', 'public'], limit=limit) + + @api.model + def channel_fetch_listeners(self, uuid): + """ Return the id, name and email of partners listening to the given channel """ + self._cr.execute(""" + SELECT P.id, P.name, P.email + FROM mail_channel_partner CP + INNER JOIN res_partner P ON CP.partner_id = P.id + INNER JOIN mail_channel C ON CP.channel_id = C.id + WHERE C.uuid = %s""", (uuid,)) + return self._cr.dictfetchall() diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 8ac1fd51679..e7fee6a5d1d 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -212,49 +212,27 @@ class Partner(models.Model): return 0 @api.model - def get_mention_suggestions(self, search, channel, exclude=None, limit=8): + def get_static_mention_suggestions(self): + """ To be overwritten to return the id, name and email of partners used as static mention + suggestions loaded once at webclient initialization and stored client side. """ + return [] + + @api.model + def get_mention_suggestions(self, search, limit=8): """ Return 'limit'-first partners' id, name and email such that the name or email matches a - 'search' string. Prioritize partners registered to channel 'channel[channel_id]' if - given, or partners that are followers of a document identified by 'channel[res_model]' - and 'channel[res_id]' otherwise, then users, and finally extend the research to all - partners. Exclude partners whose id is in 'exclude'. """ - if exclude is None: - exclude = [] - members = [] - users = [] - partners = [] - search_dom = expression.AND([ - expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]]), - [('id', 'not in', exclude)] - ]) + 'search' string. Prioritize users, and then extend the research to all partners. """ + search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]]) fields = ['id', 'name', 'email'] - def search_partners(domain, fields, limit, exclude): - partners = self.search_read(domain, fields, limit=limit) - limit -= len(partners) - exclude += [partner['id'] for partner in partners] - return partners, limit, exclude + # Search users + domain = expression.AND([[('user_ids.id', '!=', False)], search_dom]) + users = self.search_read(domain, fields, limit=limit) - # Search users registered to the channel - if 'channel_id' in channel: - domain = expression.AND([[('channel_ids', 'in', [channel['channel_id']])], search_dom]) - members, limit, exclude = search_partners(domain, fields, limit, exclude) - else: - domain = expression.AND([ - [('res_model', '=', channel['res_model'])], - [('res_id', '=', channel['res_id'])] - ]) - followers = self.env['mail.followers'].search(domain) - domain = expression.AND([[('id', 'in', followers.mapped('partner_id').ids)], search_dom]) - members, limit, exclude = search_partners(domain, fields, limit, exclude) + # Search partners if less than 'limit' users found + partners = [] + if len(users) < limit: + partners = self.search_read(search_dom, fields, limit=limit) + # Remove duplicates + partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])] - if limit > 0: - # Search users - domain = expression.AND([[('user_ids.id', '!=', False)], search_dom]) - users, limit, exclude = search_partners(domain, fields, limit, exclude) - - if limit > 0: - # Search partners - partners = self.search_read(search_dom, fields, limit=limit) - - return [members, users, partners] + return [users, partners] diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 331fd6772a5..9b89cb88995 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -24,6 +24,7 @@ var channel_defs = {}; var emojis = []; var emoji_substitutions = {}; var needaction_counter = 0; +var mention_partner_suggestions = []; var discuss_ids = {}; // Message and channel manipulation helpers @@ -584,6 +585,27 @@ var chat_manager = { return get_channel_cache(channel, domain).all_history_loaded; }, + get_mention_partner_suggestions: function (channel) { + if (!channel) { + return mention_partner_suggestions; + } + if (!channel.members_deferred) { + channel.members_deferred = ChannelModel + .call("channel_fetch_listeners", [channel.uuid]) + .then(function (members) { + var suggestions = []; + _.each(mention_partner_suggestions, function (partners) { + suggestions.push(_.filter(partners, function (partner) { + return !_.findWhere(members, { id: partner.id }); + })); + }); + + return [members].concat(suggestions); + }); + } + return channel.members_deferred; + }, + get_emojis: function() { return emojis; }, @@ -671,6 +693,7 @@ function init () { _.each(channels, add_channel); }); needaction_counter = result.needaction_inbox_counter; + mention_partner_suggestions = result.mention_partner_suggestions; }); var load_emojis = session.rpc("/mail/chat_init").then(function (result) { diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index f8c9f1e032e..d29312cf6f6 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -37,6 +37,7 @@ var Followers = form_common.AbstractField.extend({ this.value = []; this.followers = []; + this.followers_fetched = $.Deferred(); this.data_subtype = {}; this.view_is_editable = this.__parentedParent.is_action_enabled('edit'); @@ -188,9 +189,14 @@ var Followers = form_common.AbstractField.extend({ }, fetch_followers: function (value_) { + var self = this; this.value = value_ || []; return ajax.jsonRpc('/mail/read_followers', 'call', {'follower_ids': this.value}) - .then(this.proxy('display_followers'), this.proxy('fetch_generic')) + .then(function (followers) { + self.trigger('followers_update', followers); + self.display_followers(followers); + self.fetch_generic(); + }) .then(this.proxy('display_buttons')) .then(this.proxy('fetch_subtypes')); }, @@ -706,6 +712,7 @@ var Chatter = form_common.AbstractField.extend({ if (this.followers) { this.$('.o_chatter_topbar').append(this.followers.$el); this.followers.on('redirect', this, this.on_redirect); + this.followers.on('followers_update', this, this.on_followers_update); } this.thread = new ChatThread(this, { @@ -798,6 +805,30 @@ var Chatter = form_common.AbstractField.extend({ }); }, + on_followers_update: function (followers) { + this.mention_suggestions = []; + var self = this; + var prefetched_partners = chat_manager.get_mention_partner_suggestions(); + var follower_suggestions = []; + _.each(followers, function (follower) { + if (follower.res_model === 'res.partner') { + follower_suggestions.push({ + id: follower.res_id, + name: follower.name, + email: follower.email, + }); + } + }); + if (follower_suggestions.length) { + this.mention_suggestions.push(follower_suggestions); + } + _.each(prefetched_partners, function (partners) { + self.mention_suggestions.push(_.filter(partners, function (partner) { + return !_.findWhere(follower_suggestions, { id: partner.id }); + })); + }); + }, + load_more_messages: function () { this.fetch_and_render_thread(this.msg_ids, {force_fetch: true}); }, @@ -854,9 +885,9 @@ var Chatter = form_common.AbstractField.extend({ internal_subtypes: this.options.internal_subtypes, is_log: options && options.is_log, record_name: this.record_name, - get_channel_info: function () { - return { res_id: self.res_id, res_model: self.model }; - }, + }); + this.composer.on('input_focused', this, function () { + this.composer.mention_set_prefetched_partners(this.mention_suggestions || []); }); this.composer.insertBefore(this.$('.o_mail_thread')).then(function () { // destroy existing composer diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index 8acbb679b2b..89afd003126 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -160,11 +160,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.searchview = new SearchView(this, dataset, view_id, {}, options); this.searchview.on('search_data', this, this.on_search); - this.composer = new ChatComposer(this, { - get_channel_info: function () { - return { channel_id: self.channel.id }; - }, - }); + this.composer = new ChatComposer(this); this.thread = new ChatThread(this, { display_help: true }); @@ -200,6 +196,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { chat_manager.toggle_star_status(message_id); }); this.composer.on('post_message', this, this.on_post_message); + this.composer.on('input_focused', this, this.on_composer_input_focused); var def1 = this.thread.prependTo(this.$('.o_mail_chat_content')); var def2 = this.composer.appendTo(this.$('.o_mail_chat_content')); @@ -581,6 +578,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.set_channel(channel); } }, + on_composer_input_focused: function () { + var suggestions = chat_manager.get_mention_partner_suggestions(this.channel); + this.composer.mention_set_prefetched_partners(suggestions); + }, on_click_button_invite: function () { var title = _.str.sprintf(_t('Invite people to %s'), this.channel.name); diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index bfbf3618a98..24257d1a72a 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -12,6 +12,19 @@ var Widget = require('web.Widget'); var QWeb = core.qweb; var _t = core._t; +var accented_letters_mapping = { + 'a': '[àáâãäå]', + 'ae': 'æ', + 'c': 'ç', + 'e': '[èéêë]', + 'i': '[ìíîï]', + 'n': 'ñ', + 'o': '[òóôõö]', + 'oe': 'œ', + 'u': '[ùúûűü]', + 'y': '[ýÿ]', +}; + // The MentionManager allows the Composer to register listeners. For each // listener, it detects if the user is currently typing a mention (starting by a // given delimiter). If so, if fetches mention suggestions and renders them. On @@ -63,16 +76,14 @@ var MentionManager = Widget.extend({ }; // add the selected suggestion to the list - var selection = this.active_listener.selection; - if (selection.length && this.active_listener.sort_selection) { + if (this.active_listener.selection.length) { // get mention matches (ordered by index in the text) var matches = this._get_match(text_input, this.active_listener); var index = get_mention_index(matches, this._get_selection_positions().start); - selection.splice(index, 0, selected_suggestion); + this.active_listener.selection.splice(index, 0, selected_suggestion); } else { - selection = selection.concat([selected_suggestion]); + this.active_listener.selection.push(selected_suggestion); } - this._update_listener_selection(this.active_listener, selection); // update input text, and reset dropdown var cursor_position = this._get_selection_positions().start; @@ -90,10 +101,7 @@ var MentionManager = Widget.extend({ * @param {char} [delimiter] the mention delimiter * @param {function} [fetch_callback] the callback to fetch mention suggestions * @param {string} [model] the model used for redirection - * @param {boolean} [notify_selection_changed] true to trigger an event when the selection changes * @param {string} [redirect_classname] the classname of the wrapping the mention - * @param {boolean} [sort_selection] true if the selection must be sorted - * @param {string} [suggestions_template] the QWeb template to render suggestions */ register: function (listener) { this.listeners.push(_.extend(listener, { @@ -137,7 +145,6 @@ var MentionManager = Widget.extend({ */ detect_delimiter: function () { var self = this; - var options = this.options; var text_val = this.composer.$input.val(); var cursor_position = this._get_selection_positions().start; var left_string = text_val.substring(0, cursor_position); @@ -146,7 +153,7 @@ var MentionManager = Widget.extend({ var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))"; var regex_start = new RegExp(pattern, "g"); search_str = search_str.replace(/^\s\s*|^[\n\r]/g, ''); - if (regex_start.test(search_str) && search_str.length > options.min_length) { + if (regex_start.test(search_str) && search_str.length > self.options.min_length) { search_str = search_str.replace(pattern, ''); return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false; } @@ -164,19 +171,19 @@ var MentionManager = Widget.extend({ } } - if (this.active_listener) { - // start a timeout to fetch data with the current 'mention word'. The timer avoid to start - // an RPC for each pushed key when the user is still typing. - // The 'typing_speed' option should approach the time for a human to type a letter. - clearTimeout(this.mention_fetch_timer); - this.mention_fetch_timer = setTimeout(function () { - $.when(listener.fetch_callback(self.mention_word, self.active_listener)).then(function (suggestions) { + // start a timeout to fetch data with the current 'mention word'. The timer avoid to start + // an RPC for each pushed key when the user is still typing. + // The 'typing_speed' option should approach the time for a human to type a letter. + clearTimeout(this.mention_fetch_timer); + this.mention_fetch_timer = setTimeout(function () { + if (self.active_listener) { + $.when(listener.fetch_callback(self.mention_word)).then(function (suggestions) { self.set('mention_suggestions', suggestions); }); - }, this.options.typing_speed); - } else { - this.set('mention_suggestions', []); // close the dropdown - } + } else { + self.set('mention_suggestions', []); // close the dropdown + } + }, this.options.typing_speed); }, /** @@ -201,7 +208,7 @@ var MentionManager = Widget.extend({ } } if (to_remove.length) { - self._update_listener_selection(listener, _.difference(mention_selection, to_remove)); + listener.selection = _.difference(mention_selection, to_remove); } }); }, @@ -235,9 +242,8 @@ var MentionManager = Widget.extend({ }, reset_selections: function () { - var self = this; _.each(this.listeners, function (listener) { - self._update_listener_selection(listener, []); + listener.selection = []; }); }, @@ -269,7 +275,7 @@ var MentionManager = Widget.extend({ }, _render_suggestions: function () { if (_.flatten(this.get('mention_suggestions')).length) { - this.$el.html(QWeb.render(this.active_listener.suggestions_template, { + this.$el.html(QWeb.render('mail.ChatComposer.MentionSuggestions', { suggestions: this.get('mention_suggestions'), })); this.$el @@ -282,12 +288,6 @@ var MentionManager = Widget.extend({ this.open = false; } }, - _update_listener_selection: function (listener, selection) { - listener.selection = selection; - if (listener.notify_selection_changed) { - this.trigger('selection_changed', listener); - } - }, // Cursor position and selection utils _get_selection_positions: function () { @@ -327,10 +327,8 @@ var Composer = Widget.extend({ input_max_height: 150, input_min_height: 28, mention_fetch_limit: 8, - get_channel_info: function () {}, }); this.context = this.options.context; - this.get_channel_info = this.options.get_channel_info; // Attachments this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context); @@ -343,19 +341,14 @@ var Composer = Widget.extend({ delimiter: '@', fetch_callback: this.mention_fetch_partners.bind(this), model: 'res.partner', - notify_selection_changed: true, redirect_classname: 'o_mail_redirect', - sort_selection: true, - suggestions_template: 'mail.ChatComposer.MentionPartnerSuggestions', }); this.mention_manager.register({ delimiter: '#', fetch_callback: this.mention_fetch_channels.bind(this), model: 'mail.channel', redirect_classname: 'o_channel_redirect', - suggestions_template: 'mail.ChatComposer.MentionChannelSuggestions' }); - this.mention_manager.on('selection_changed', this, this.mention_render_selection); this.PartnerModel = new Model('res.partner'); this.ChannelModel = new Model('mail.channel'); }, @@ -365,8 +358,10 @@ var Composer = Widget.extend({ this.$attachment_button = this.$(".o_composer_button_add_attachment"); this.$attachments_list = this.$('.o_composer_attachments_list'); - this.$mention_partner_tags = this.$('.o_composer_mentioned_partners'); this.$input = this.$('.o_composer_input'); + this.$input.focus(function () { + self.trigger('input_focused'); + }); this.resize_input(); // Attachments @@ -403,7 +398,7 @@ var Composer = Widget.extend({ return $.when({ content: this.mention_manager.generate_links(value), attachment_ids: _.pluck(this.get('attachment_ids'), 'id'), - partner_ids: _.pluck(this.mention_manager.get_listener_selection('@'), 'id'), + partner_ids: _.uniq(_.pluck(this.mention_manager.get_listener_selection('@'), 'id')), }); }, @@ -607,19 +602,38 @@ var Composer = Widget.extend({ }); }); }, - mention_fetch_partners: function (search, listener) { - var kwargs = { - channel: this.get_channel_info(), - exclude: listener ? _.pluck(listener.selection, 'id') : [], - limit: this.options.mention_fetch_limit, - search: search, - }; - return this.PartnerModel.call('get_mention_suggestions', kwargs); + mention_fetch_partners: function (search) { + var self = this; + return $.when(this.mention_prefetched_partners).then(function (prefetched_partners) { + // filter prefetched partners with the given search string + var suggestions = []; + var limit = self.options.mention_fetch_limit; + var search_regexp = new RegExp(self.unaccent(search), 'i'); + _.each(prefetched_partners, function (partners) { + if (limit > 0) { + var filtered_partners = _.filter(partners, function (partner) { + return partner.email && partner.email.search(search_regexp) !== -1 || + partner.name && self.unaccent(partner.name).search(search_regexp) !== -1; + }); + if (filtered_partners.length) { + suggestions.push(filtered_partners.slice(0, limit)); + limit -= filtered_partners.length; + } + } + }); + if (!suggestions.length) { + // no result found among prefetched partners, fetch other suggestions + var kwargs = { + limit: limit, + search: search, + }; + suggestions = self.PartnerModel.call('get_mention_suggestions', kwargs); + } + return suggestions; + }); }, - mention_render_selection: function (listener) { - this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', { - partners: listener.selection, - })); + mention_set_prefetched_partners: function (prefetched_partners) { + this.mention_prefetched_partners = prefetched_partners; }, // Others @@ -629,6 +643,12 @@ var Composer = Widget.extend({ focus: function () { this.$input.focus(); }, + unaccent: function (str) { + _.each(accented_letters_mapping, function (value, key) { + str = str.replace(new RegExp(value, 'g'), key); + }); + return str; + }, }); return Composer; diff --git a/addons/mail/static/src/less/client_action.less b/addons/mail/static/src/less/client_action.less index 64f0afe507a..60128b41b50 100644 --- a/addons/mail/static/src/less/client_action.less +++ b/addons/mail/static/src/less/client_action.less @@ -214,16 +214,6 @@ color: white; } } - - .o_composer_mentioned_partners { - color: white; - - > .badge { - color: white; - border: 1px solid white; - background-color: transparent; - } - } } } } diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less index 11cfbae04d3..37e3b7d0e97 100644 --- a/addons/mail/static/src/less/composer.less +++ b/addons/mail/static/src/less/composer.less @@ -2,8 +2,8 @@ resize: none; } -.o_composer_mentioned_partners, .o_composer_attachments_list { - margin-top: 5px; +.o_composer_attachments_list { + margin-bottom: 5px; text-align: justify; } diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml index 6cc8b86b19e..f8ce3057e14 100644 --- a/addons/mail/static/src/xml/chatter.xml +++ b/addons/mail/static/src/xml/chatter.xml @@ -6,7 +6,7 @@ --> - + diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index e47363c07bf..9a1e679c4ba 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -2,7 +2,6 @@
-