diff --git a/addons/account/demo/account_invoice_demo.yml b/addons/account/demo/account_invoice_demo.yml index 1b04d0f6550..5c4c0032b24 100644 --- a/addons/account/demo/account_invoice_demo.yml +++ b/addons/account/demo/account_invoice_demo.yml @@ -9,7 +9,7 @@ reference_type: none payment_term_id: account.account_payment_term type: 'out_invoice' - date_invoice: !eval time.strftime('%Y')+'-01-01' + date_invoice: !eval time.strftime('%Y-%m')+'-01' invoice_line_ids: - name: 'Dell Inspiron Laptop without OS' product_id: product.product_product_49 @@ -27,7 +27,7 @@ partner_id: base.res_partner_2 currency_id: base.EUR type: 'out_invoice' - date_invoice: !eval time.strftime('%Y')+'-01-08' + date_invoice: !eval time.strftime('%Y-%m')+'-08' invoice_line_ids: - name: '8-port Switch' product_id: product.product_product_51 @@ -94,7 +94,7 @@ reference_type: none payment_term_id: account.account_payment_term type: 'in_invoice' - date_invoice: !eval time.strftime('%Y')+'-01-01' + date_invoice: !eval time.strftime('%Y-%m')+'-01' invoice_line_ids: - name: 'Toner Cartridge' price_unit: 10.0 @@ -112,7 +112,7 @@ partner_id: base.res_partner_12 currency_id: base.EUR type: 'in_invoice' - date_invoice: !eval time.strftime('%Y')+'-01-01' + date_invoice: !eval time.strftime('%Y-%m')+'-01' - !python {model: ir.model.data} : | line_vals = { @@ -133,7 +133,7 @@ partner_id: base.res_partner_1 currency_id: base.EUR type: 'in_invoice' - date_invoice: !eval time.strftime('%Y')+'-01-15' + date_invoice: !eval time.strftime('%Y-%m')+'-15' - !python {model: ir.model.data} : | line_vals = { diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index a75211665a3..5bcb778f53b 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -116,16 +116,21 @@ class AccountInvoice(models.Model): if len(lines) != 0: for line in lines: # get the outstanding residual value in invoice currency + # get the outstanding residual value in its currency. We don't want to show it + # in the invoice currency since the exchange rate between the invoice date and + # the payment date might have changed. if line.currency_id: - amount_to_show = line.currency_id.compute(abs(line.amount_residual_currency), self.currency_id) + currency_id = line.currency_id + amount_to_show = abs(line.amount_residual_currency) else: - amount_to_show = line.company_id.currency_id.compute(abs(line.amount_residual), self.currency_id) + currency_id = line.company_id.currency_id + amount_to_show = abs(line.amount_residual) info['content'].append({ 'journal_name': line.ref or line.move_id.name, 'amount': amount_to_show, - 'currency': self.currency_id.symbol, + 'currency': currency_id.symbol, 'id': line.id, - 'position': self.currency_id.position, + 'position': currency_id.position, 'digits': [69, self.currency_id.decimal_places], }) info['title'] = type_payment @@ -148,7 +153,9 @@ class AccountInvoice(models.Model): elif self.type in ('in_invoice', 'out_refund'): amount = sum([p.amount for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids]) amount_currency = sum([p.amount_currency for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids]) - # get the payment value in invoice currency + # Get the payment value in its currency. We don't want to show it in the invoice + # currency since the exchange rate between the invoice date and the payment date + # might have changed. if payment.currency_id and amount_currency != 0: currency_id = payment.currency_id amount_to_show = -amount_currency 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) 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; diff --git a/addons/account_analytic_default/account_analytic_default.py b/addons/account_analytic_default/account_analytic_default.py index 7fa23b1b0f6..5b38ad413c1 100644 --- a/addons/account_analytic_default/account_analytic_default.py +++ b/addons/account_analytic_default/account_analytic_default.py @@ -131,3 +131,17 @@ class product_template(osv.Model): # Remove context so it is not going to filter on product_id with active_id of template 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 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)) diff --git a/addons/analytic/__openerp__.py b/addons/analytic/__openerp__.py index 6ab909f864b..4c12224ebfd 100644 --- a/addons/analytic/__openerp__.py +++ b/addons/analytic/__openerp__.py @@ -6,7 +6,7 @@ 'version': '1.1', '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. =============================================== diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py index 4b99259a8a9..92ebcb093b0 100644 --- a/addons/auth_signup/res_users.py +++ b/addons/auth_signup/res_users.py @@ -265,8 +265,7 @@ class res_users(osv.Model): res_partner.signup_prepare(cr, uid, partner_ids, signup_type="reset", expiration=expiration, context=context) - if not context: - context = {} + context = dict(context or {}) # send email to users with their signup url template = False @@ -283,6 +282,7 @@ class res_users(osv.Model): for user in self.browse(cr, uid, ids, context): if not user.email: raise UserError(_("Cannot send email: user %s has no email address.") % user.name) + context['lang'] = user.lang self.pool.get('mail.template').send_mail(cr, uid, template.id, user.id, force_send=True, raise_exception=True, context=context) def create(self, cr, uid, values, context=None): diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py index 3bc238b7fa7..3201d9b9381 100644 --- a/addons/base_action_rule/base_action_rule.py +++ b/addons/base_action_rule/base_action_rule.py @@ -104,11 +104,11 @@ class base_action_rule(osv.osv): def onchange_kind(self, cr, uid, ids, kind, context=None): clear_fields = [] if kind in ['on_create', 'on_create_or_write', 'on_unlink']: - clear_fields = ['filter_pre_id', 'trg_date_id', 'trg_date_range', 'trg_date_range_type'] + clear_fields = ['filter_pre_id', 'filter_pre_domain', 'trg_date_id', 'trg_date_range', 'trg_date_range_type'] elif kind in ['on_write', 'on_create_or_write']: clear_fields = ['trg_date_id', 'trg_date_range', 'trg_date_range_type'] elif kind == 'on_time': - clear_fields = ['filter_pre_id'] + clear_fields = ['filter_pre_id', 'filter_pre_domain'] return {'value': dict.fromkeys(clear_fields, False)} def onchange_filter_pre_id(self, cr, uid, ids, filter_pre_id, context=None): @@ -119,57 +119,88 @@ class base_action_rule(osv.osv): ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context) return {'value': {'filter_domain': ir_filter.domain}} - def _get_eval_context(self, cr, uid, context=None): + @openerp.api.model + def _get_actions(self, records, kinds): + """ Return the actions of the given kinds for records' model. The + returned actions' context contain an object to manage processing. + """ + if '__action_done' not in self._context: + self = self.with_context(__action_done={}) + domain = [('model', '=', records._name), ('kind', 'in', kinds)] + actions = self.with_context(active_test=True).search(domain) + return actions.with_env(self.env) + + @openerp.api.model + def _get_eval_context(self): """ Prepare the context used when evaluating python code :returns: dict -- evaluation context given to (safe_)eval """ return { 'datetime': DT, 'dateutil': dateutil, 'time': time, - 'uid': uid, - 'user': self.pool['res.users'].browse(cr, uid, uid, context=context), + 'uid': self.env.uid, + 'user': self.env.user, } - def _filter(self, cr, uid, action, action_filter, record_ids, domain=False, context=None): - """ Filter the list record_ids that satisfy the domain or the action filter. """ - if record_ids and (domain is not False or action_filter): - eval_context = self._get_eval_context(cr, uid, context=context) - if domain is not False: - new_domain = [('id', 'in', record_ids)] + eval(domain, eval_context) - ctx = context - elif action_filter: - assert action.model == action_filter.model_id, "Filter model different from action rule model" - new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain, eval_context) - ctx = dict(context or {}) - ctx.update(eval(action_filter.context)) - record_ids = self.pool[action.model].search(cr, uid, new_domain, context=ctx) - return record_ids + @openerp.api.model + def _filter_pre(self, records): + """ Filter the records that satisfy the precondition of action ``self``. """ + if self.filter_pre_id and records: + eval_context = self._get_eval_context() + domain = [('id', 'in', records.ids)] + eval(self.filter_pre_id.domain, eval_context) + ctx = eval(self.filter_pre_id.context) + return records.with_context(**ctx).search(domain).with_env(records.env) + elif self.filter_pre_domain and records: + eval_context = self._get_eval_context() + domain = [('id', 'in', records.ids)] + eval(self.filter_pre_domain, eval_context) + return records.search(domain) + else: + return records + + @openerp.api.model + def _filter_post(self, records): + """ Filter the records that satisfy the postcondition of action ``self``. """ + if self.filter_id and records: + eval_context = self._get_eval_context() + domain = [('id', 'in', records.ids)] + eval(self.filter_id.domain, eval_context) + ctx = eval(self.filter_id.context) + return records.with_context(**ctx).search(domain).with_env(records.env) + elif self.filter_domain and records: + eval_context = self._get_eval_context() + domain = [('id', 'in', records.ids)] + eval(self.filter_domain, eval_context) + return records.search(domain) + else: + return records + + @openerp.api.multi + def _process(self, records): + """ Process action ``self`` on the ``records`` that have not been done yet. """ + # filter out the records on which self has already been done, then mark + # remaining records as done (to avoid recursive processing) + action_done = self._context['__action_done'] + records -= action_done.setdefault(self, records.browse()) + if not records: + return + action_done[self] |= records - def _process(self, cr, uid, action, record_ids, context=None): - """ process the given action on the records """ - model = self.pool[action.model_id.model] # modify records values = {} - if 'date_action_last' in model._fields: - values['date_action_last'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - if action.act_user_id and 'user_id' in model._fields: - values['user_id'] = action.act_user_id.id + if 'date_action_last' in records._fields: + values['date_action_last'] = openerp.fields.Datetime.now() + if self.act_user_id and 'user_id' in records._fields: + values['user_id'] = self.act_user_id.id if values: - model.write(cr, uid, record_ids, values, context=context) + records.write(values) - if action.act_followers and hasattr(model, 'message_subscribe'): - follower_ids = map(int, action.act_followers) - model.message_subscribe(cr, uid, record_ids, follower_ids, context=context) + # subscribe followers + if self.act_followers and hasattr(records, 'message_subscribe'): + records.message_subscribe(self.act_followers.ids) # execute server actions - if action.server_action_ids: - server_action_ids = map(int, action.server_action_ids) - for record in model.browse(cr, uid, record_ids, context): - action_server_obj = self.pool.get('ir.actions.server') - ctx = dict(context, active_model=model._name, active_ids=[record.id], active_id=record.id) - action_server_obj.run(cr, uid, server_action_ids, context=ctx) - - return True + if self.server_action_ids: + for record in records: + ctx = {'active_model': record._name, 'active_ids': record.ids, 'active_id': record.id} + self.server_action_ids.with_context(**ctx).run() def _register_hook(self, cr): """ Patch models that should trigger action rules based on creation, @@ -186,26 +217,19 @@ class base_action_rule(osv.osv): def make_create(): """ Instanciate a create method that processes action rules. """ - def create(self, cr, uid, vals, context=None): - # avoid loops or cascading actions - if context and context.get('action'): - return create.origin(self, cr, uid, vals, context=context) + @openerp.api.model + def create(self, vals): + # retrieve the action rules to possibly execute + actions = self.env['base.action.rule']._get_actions(self, ['on_create', 'on_create_or_write']) - # call original method with a modified context - context = dict(context or {}, action=True) - new_id = create.origin(self, cr, uid, vals, context=context) - - # as it is a new record, we do not consider the actions that have a prefilter - action_model = self.pool.get('base.action.rule') - action_dom = [('model', '=', self._name), - ('kind', 'in', ['on_create', 'on_create_or_write'])] - action_ids = action_model.search(cr, uid, action_dom, context=dict(context, active_test=True)) + # call original method + record = create.origin(self.with_env(actions.env), vals) # check postconditions, and execute actions on the records that satisfy them - for action in action_model.browse(cr, uid, action_ids, context=context): - if action_model._filter(cr, uid, action, action.filter_id, [new_id], domain=action.filter_domain, context=context): - action_model._process(cr, uid, action, [new_id], context=context) - return new_id + for action in actions.with_context(old_values=None): + action._process(action._filter_post(record)) + + return record.with_env(self.env) return create @@ -215,69 +239,45 @@ class base_action_rule(osv.osv): # Note: we patch method _write() instead of write() in order to # catch updates made by field recomputations. # - def _write(self, cr, uid, ids, vals, context=None): - # avoid loops or cascading actions - if context and context.get('action'): - return _write.origin(self, cr, uid, ids, vals, context=context) - - # modify context - context = dict(context or {}, action=True) - ids = [ids] if isinstance(ids, (int, long, str)) else ids - + @openerp.api.multi + def _write(self, vals): # retrieve the action rules to possibly execute - action_model = self.pool.get('base.action.rule') - action_dom = [('model', '=', self._name), - ('kind', 'in', ['on_write', 'on_create_or_write'])] - action_ids = action_model.search(cr, uid, action_dom, context=context) - actions = action_model.browse(cr, uid, action_ids, context=context) + actions = self.env['base.action.rule']._get_actions(self, ['on_write', 'on_create_or_write']) + records = self.with_env(actions.env) - # check preconditions - pre_ids = {} - for action in actions: - pre_ids[action] = action_model._filter(cr, uid, action, action.filter_pre_id, ids, domain=action.filter_pre_domain, context=context) + # check preconditions on records + pre = {action: action._filter_pre(records) for action in actions} # read old values before the update - old_values = {} - for old_vals in self.read(cr, uid, ids, list(vals), context=context): - old_values[old_vals.pop('id')] = old_vals + old_values = { + old_vals.pop('id'): old_vals + for old_vals in records.read(list(vals)) + } # call original method - _write.origin(self, cr, uid, ids, vals, context=context) + _write.origin(records, vals) # check postconditions, and execute actions on the records that satisfy them - for action in actions: - post_ids = action_model._filter(cr, uid, action, action.filter_id, pre_ids[action], domain=action.filter_domain, context=context) - if post_ids: - action_model._process(cr, uid, action, post_ids, context=dict(context, old_values=old_values)) + for action in actions.with_context(old_values=old_values): + action._process(action._filter_post(pre[action])) return True return _write def make_unlink(): """ Instanciate an unlink method that processes action rules. """ - def unlink(self, cr, uid, ids, context=None, **kwargs): - if context and context.get('action'): - return unlink.origin(self, cr, uid, ids, context=context) - - # modify context - context = dict(context or {}, action=True) - ids = [ids] if isinstance(ids, (int, long, str)) else ids - + @openerp.api.multi + def unlink(self, **kwargs): # retrieve the action rules to possibly execute - action_model = self.pool.get('base.action.rule') - action_dom = [('model', '=', self._name), - ('kind', '=', 'on_unlink')] - action_ids = action_model.search(cr, uid, action_dom, context=context) - actions = action_model.browse(cr, uid, action_ids, context=context) + actions = self.env['base.action.rule']._get_actions(self, ['on_unlink']) + records = self.with_env(actions.env) # check conditions, and execute actions on the records that satisfy them for action in actions: - pre_ids = action_model._filter(cr, uid, action, action.filter_id, ids, domain=action.filter_domain, context=context) - if pre_ids: - action_model._process(cr, uid, action, pre_ids, context=context) + action._process(action._filter_post(pre[action])) # call original method - return unlink.origin(self, cr, uid, ids, context=context, **kwargs) + return unlink.origin(self, **kwargs) return unlink diff --git a/addons/base_action_rule/base_action_rule_demo.xml b/addons/base_action_rule/base_action_rule_demo.xml index 4c4a5e841ad..c626cd41a3a 100644 --- a/addons/base_action_rule/base_action_rule_demo.xml +++ b/addons/base_action_rule/base_action_rule_demo.xml @@ -43,5 +43,12 @@ [('customer', '=', True)] + + + Test rule on secondary record + on_create + + + diff --git a/addons/base_action_rule/security/ir.model.access.csv b/addons/base_action_rule/security/ir.model.access.csv index b509ec62944..cec3f676e0b 100644 --- a/addons/base_action_rule/security/ir.model.access.csv +++ b/addons/base_action_rule/security/ir.model.access.csv @@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_base_action_rule,base.action.rule,model_base_action_rule,,1,0,0,0 access_base_action_rule_config,base.action.rule config,model_base_action_rule,base.group_system,1,1,1,1 access_base_action_rule_lead_test,access_base_action_rule_lead_test,model_base_action_rule_lead_test,base.group_system,1,1,1,1 +access_base_action_rule_line_test,access_base_action_rule_line_test,model_base_action_rule_line_test,base.group_system,1,1,1,1 diff --git a/addons/base_action_rule/test_models.py b/addons/base_action_rule/test_models.py index 5c0ae12886e..2bfb568d762 100644 --- a/addons/base_action_rule/test_models.py +++ b/addons/base_action_rule/test_models.py @@ -24,6 +24,7 @@ class lead_test(osv.Model): 'active': fields.boolean('Active', required=False), 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null'), 'date_action_last': fields.datetime('Last Action', readonly=1), + 'line_ids': fields.one2many('base.action.rule.line.test', 'lead_id'), } _defaults = { @@ -39,3 +40,12 @@ class lead_test(osv.Model): def message_subscribe(self, cr, uid, ids, partner_ids=None, channel_ids=None, subtype_ids=None, force=True, context=None): pass + + +class line_test(osv.Model): + _name = "base.action.rule.line.test" + _description = "Action Rule Line Test" + + name = openerp.fields.Char() + lead_id = openerp.fields.Many2one('base.action.rule.lead.test', ondelete='cascade') + user_id = openerp.fields.Many2one('res.users') diff --git a/addons/base_action_rule/tests/test_base_action_rule.py b/addons/base_action_rule/tests/test_base_action_rule.py index ccc33404d6f..b6e7d880246 100644 --- a/addons/base_action_rule/tests/test_base_action_rule.py +++ b/addons/base_action_rule/tests/test_base_action_rule.py @@ -77,7 +77,7 @@ class base_action_rule_test(common.TransactionCase): self.assertEqual(lead.state, 'done') self.assertEqual(lead.user_id, self.user_demo) - def test_04_recomputed_field(self): + def test_10_recomputed_field(self): """ Check that a rule is executed whenever a field is recomputed after a change on another model. @@ -91,3 +91,20 @@ class base_action_rule_test(common.TransactionCase): partner.write({'customer': True}) self.assertTrue(lead.customer) self.assertEqual(lead.user_id, self.user_demo) + + def test_20_direct_line(self): + """ + Check that a rule is executed after creating a line record. + """ + line = self.env['base.action.rule.line.test'].create({'name': "Line"}) + self.assertEqual(line.user_id, self.user_demo) + + def test_20_indirect_line(self): + """ + Check that creating a lead with a line executes rules on both records. + """ + lead = self.create_lead(line_ids=[(0, 0, {'name': "Line"})]) + self.assertEqual(lead.state, 'draft') + self.assertEqual(lead.user_id, self.user_demo) + self.assertEqual(len(lead.line_ids), 1) + self.assertEqual(lead.line_ids.user_id, self.user_demo) diff --git a/addons/calendar/controllers/main.py b/addons/calendar/controllers/main.py index 770c73bd41c..afe85209f03 100644 --- a/addons/calendar/controllers/main.py +++ b/addons/calendar/controllers/main.py @@ -48,7 +48,13 @@ class meeting_invitation(http.Controller): if attendee: attendee_data['current_attendee'] = attendee[0] - values = dict(init="s.calendar.event('%s', '%s', '%s', '%s' , '%s');" % (db, action, id, 'form', json.dumps(attendee_data))) + values = dict( + init = """ + odoo.define('calendar.invitation_page', function (require) { + require('base_calendar.base_calendar').showCalendarInvitation('%s', '%s', '%s', '%s', '%s'); + }); + """ % (db, action, id, 'form', json.dumps(attendee_data)) + ) return request.render('web.webclient_bootstrap', values) # Function used, in RPC to check every 5 minutes, if notification to do for an event or not 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" diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index 7139a0d61d1..b28f6308328 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -8,6 +8,7 @@ var Dialog = require('web.Dialog'); var form_common = require('web.form_common'); var Model = require('web.DataModel'); var Notification = require('web.notification').Notification; +var session = require('web.session'); var WebClient = require('web.WebClient'); var widgets = require('web_calendar.widgets'); @@ -252,6 +253,20 @@ var Many2ManyAttendee = FieldMany2ManyTags.extend({ } }); +function showCalendarInvitation(db, action, id, view, attendee_data) { + session.session_bind(session.origin).then(function () { + if (session.session_is_valid(db) && session.username !== "anonymous") { + window.location.href = _.str.sprintf('/web?db=%s#id=%s&view_type=form&model=calendar.event', db, id); + } else { + $("body").prepend(QWeb.render('CalendarInvitation', {attendee_data: JSON.parse(attendee_data)})); + } + }); +} + core.form_widget_registry.add('many2manyattendee', Many2ManyAttendee); +return { + showCalendarInvitation: showCalendarInvitation, +}; + }); diff --git a/addons/calendar/static/src/less/calendar.less b/addons/calendar/static/src/less/calendar.less index b531a583145..e3c15136a00 100644 --- a/addons/calendar/static/src/less/calendar.less +++ b/addons/calendar/static/src/less/calendar.less @@ -35,6 +35,10 @@ &.needsAction { background-color: @odoo-brand-secondary; } + + &.declined { + background-color: @brand-danger; + } } .o_calendar_all_responsibles { @@ -60,3 +64,40 @@ margin-top: 10px; position: relative; } + +.o_calendar_invitation_page { + .o-flex(0, 0, auto); + width: 50%; + margin: 30px auto 0; + .o-webclient-padding(@top: 10px, @bottom: 10px); + background-color: @odoo-view-background-color; + + .o_logo { + width: 15%; + } + .o_event_title { + margin-left: 20%; + + h2 { + margin-top: 0; + } + } + .o_event_table { + clear: both; + margin: 15px 0 0; + + th { + padding-right: 15px; + } + ul { + padding-left: 0; + } + } + + .o_accepted { + color: @brand-success; + } + .o_declined { + color: @brand-danger; + } +} diff --git a/addons/calendar/static/src/xml/base_calendar.xml b/addons/calendar/static/src/xml/base_calendar.xml index e4dd5eca511..d01994de2e5 100644 --- a/addons/calendar/static/src/xml/base_calendar.xml +++ b/addons/calendar/static/src/xml/base_calendar.xml @@ -27,4 +27,41 @@ + +
+
+ () +
+ Yes I'm going. + No I'm not going. +
+
+ +
+

Calendar Invitation

+

+
+ + + + + + + + + + + + + +
When
Where
Who +
    +
  • + +
  • +
+
+
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 9c014d971c6..f83030eecf6 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -290,7 +290,15 @@ class crm_lead(format_address, osv.osv): def on_change_user(self, cr, uid, ids, user_id, context=None): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ + if user_id and context.get('team_id'): + team = self.pool['crm.team'].browse(cr, uid, context['team_id'], context=context) + if user_id in team.member_ids.ids: + return {} team_id = self.pool['crm.team']._get_default_team_id(cr, uid, context=context, user_id=user_id) + if user_id and not team_id and self.pool['res.users'].has_group(cr, uid, 'base.group_multi_salesteams'): + team_ids = self.pool.get('crm.team').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context) + if team_ids: + team_id = team_ids[0] return {'value': {'team_id': team_id}} def stage_find(self, cr, uid, cases, team_id, domain=None, order='sequence', context=None): diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index 427cb54533b..2dacfa3baa1 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -158,7 +158,7 @@ + context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_sale_salesman_all_leads'], 'team_id': team_id}"/> @@ -496,7 +496,7 @@ - + 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/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index b02ea5acba7..2633c7dc450 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -208,47 +208,54 @@ class HrExpense(models.Model): if any(not expense.journal_id for expense in self): raise UserError(_("Expenses must have an expense journal specified to generate accounting entries.")) - #create the move that will contain the accounting entries - move = self.env['account.move'].create({ - 'journal_id': self.journal_id.id, - 'company_id': self.env.user.company_id.id, - }) - + journal_dict = {} for expense in self: - company_currency = expense.company_id.currency_id - diff_currency_p = expense.currency_id != company_currency - #one account.move.line per expense (+taxes..) - move_lines = expense._move_line_get() + if expense.journal_id not in journal_dict: + journal_dict[expense.journal_id] = [] + journal_dict[expense.journal_id].append(expense) - #create one more move line, a counterline for the total on payable account - total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines) - if expense.payment_mode == 'company_account': - if not expense.bank_journal_id.default_credit_account_id: - raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name)) - emp_account = expense.bank_journal_id.default_credit_account_id.id - else: - if not expense.employee_id.address_home_id: - raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name)) - emp_account = expense.employee_id.address_home_id.property_account_payable_id.id + for journal, expense_list in journal_dict.items(): + #create the move that will contain the accounting entries + move = self.env['account.move'].create({ + 'journal_id': journal.id, + 'company_id': self.env.user.company_id.id, + }) + for expense in expense_list: + company_currency = expense.company_id.currency_id + diff_currency_p = expense.currency_id != company_currency + #one account.move.line per expense (+taxes..) + move_lines = expense._move_line_get() - move_lines.append({ - 'type': 'dest', - 'name': '/', - 'price': total, - 'account_id': emp_account, - 'date_maturity': expense.date, - 'amount_currency': diff_currency_p and total_currency or False, - 'currency_id': diff_currency_p and expense.currency_id.id or False, - 'ref': expense.employee_id.address_home_id.ref or False - }) + #create one more move line, a counterline for the total on payable account + total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines) + if expense.payment_mode == 'company_account': + if not expense.bank_journal_id.default_credit_account_id: + raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name)) + emp_account = expense.bank_journal_id.default_credit_account_id.id + else: + if not expense.employee_id.address_home_id: + raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name)) + emp_account = expense.employee_id.address_home_id.property_account_payable_id.id - #convert eml into an osv-valid format - lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines) - move.write({'line_ids': lines}) - expense.write({'account_move_id': move.id, 'state': 'post'}) - if expense.payment_mode == 'company_account': - expense.paid_expenses() - return move.post() + move_lines.append({ + 'type': 'dest', + 'name': '/', + 'price': total, + 'account_id': emp_account, + 'date_maturity': expense.date, + 'amount_currency': diff_currency_p and total_currency or False, + 'currency_id': diff_currency_p and expense.currency_id.id or False, + 'ref': expense.employee_id.address_home_id.ref or False + }) + + #convert eml into an osv-valid format + lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines) + move.write({'line_ids': lines}) + expense.write({'account_move_id': move.id, 'state': 'post'}) + if expense.payment_mode == 'company_account': + expense.paid_expenses() + move.post() + return True @api.multi def _move_line_get(self): diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index d2dabad3c2d..d4d8e4d13eb 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -325,7 +325,7 @@ - Generate Single Accounting Entry + Generate Accounting Entries ir.actions.server code 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/l10n_be/i18n_extra/nl_BE.po b/addons/l10n_be/i18n_extra/nl_BE.po index c6c64c82899..9be7f3a1b7a 100644 --- a/addons/l10n_be/i18n_extra/nl_BE.po +++ b/addons/l10n_be/i18n_extra/nl_BE.po @@ -20,12 +20,12 @@ msgstr "" #. module: l10n_be #: model:account.tax.template,name:l10n_be.attn_VAT-IN-V83-00 msgid "0% Biens d'investissement" -msgstr "" +msgstr "0% Investeringsgoederen" #. module: l10n_be #: model:account.tax.template,name:l10n_be.attn_VAT-IN-V82-00-G msgid "0% Biens divers" -msgstr "" +msgstr "0% Diverse goederen" #. module: l10n_be #: model:account.tax.template,name:l10n_be.attn_VAT-OUT-00-CC @@ -189,7 +189,7 @@ msgstr "Vooruitbetalingen" #. module: l10n_be #: model:account.account.template,name:l10n_be.a360 msgid "Acomptes versés sur achats pour stocks" -msgstr "" +msgstr "Vooruitbetalingen op aankopen voor aandelen" #. module: l10n_be #: model:account.account.template,name:l10n_be.a4550 @@ -239,7 +239,7 @@ msgstr "Advertenties, publiciteit, propaganda en documentatie" #. module: l10n_be #: model:account.account.template,name:l10n_be.a3100 msgid "Approvisionements et fournitures" -msgstr "" +msgstr "Materialen en benodigdheden" #. module: l10n_be #: model:account.account.template,name:l10n_be.a4160 @@ -280,7 +280,7 @@ msgstr "Overige leningen" #. module: l10n_be #: model:account.account.template,name:l10n_be.a4125 msgid "Autres impôts belges" -msgstr "" +msgstr "Overige Belgische belastingen" #. module: l10n_be #: model:account.account.template,name:l10n_be.a49102 @@ -301,7 +301,7 @@ msgstr "Voorschotten en leningen aan het personeel" #. module: l10n_be #: model:account.chart.template,name:l10n_be.l10nbe_chart_template msgid "Belgian PCMN" -msgstr "" +msgstr "Belgische PCMN" #. module: l10n_be #: model:account.account.template,name:l10n_be.a140 @@ -331,7 +331,7 @@ msgstr "Vrachtwagens" #. module: l10n_be #: model:account.account.template,name:l10n_be.a1000 msgid "Capital" -msgstr "" +msgstr "Kapitaal" #. module: l10n_be #: model:account.account.template,name:l10n_be.a418 @@ -341,7 +341,7 @@ msgstr "Borgtochten betaald in contanten" #. module: l10n_be #: model:account.account.template,name:l10n_be.a6600 msgid "Charges exceptionelles" -msgstr "" +msgstr "Uitzonderlijke kosten" #. module: l10n_be #: model:account.account.template,name:l10n_be.a6101 @@ -371,7 +371,7 @@ msgstr "Klanten" #. module: l10n_be #: model:account.account.template,name:l10n_be.a370 msgid "Commandes en cours d'exécution" -msgstr "" +msgstr "Lopende opdrachten" #. module: l10n_be #: model:account.account.template,name:l10n_be.a49101 @@ -448,7 +448,7 @@ msgstr "Leasingschulden van roerende goederen" #. module: l10n_be #: model:account.account.template,name:l10n_be.a17300 msgid "Dettes en compte \\ Banque A" -msgstr "" +msgstr "Schulden op rekeningen \\Bank A" #. module: l10n_be #: model:account.account.template,name:l10n_be.a4597 @@ -636,7 +636,7 @@ msgstr "Diverse huren" #. module: l10n_be #: model:account.account.template,name:l10n_be.a3400 msgid "Marchandises" -msgstr "" +msgstr "Goederen" #. module: l10n_be #: model:account.account.template,name:l10n_be.a300 diff --git a/addons/l10n_eu_service/wizard/wizard.xml b/addons/l10n_eu_service/wizard/wizard.xml index 9ceac6978db..04f604f0462 100644 --- a/addons/l10n_eu_service/wizard/wizard.xml +++ b/addons/l10n_eu_service/wizard/wizard.xml @@ -55,7 +55,6 @@ automatic - open 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..a2f257a45ca 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -166,7 +166,6 @@ class Channel(models.Model): self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") return result - @api.multi def message_get_email_values(self, notif_mail=None): self.ensure_one() @@ -423,6 +422,10 @@ class Channel(models.Model): for channel in self: partners_to_add = partners - channel.channel_partner_ids channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]}) + for partner in partners_to_add: + notification = _('
joined #%s
') % (self.id, self.name,) + self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id) + # broadcast the channel header to the added partner self._broadcast(partner_ids) @@ -525,7 +528,7 @@ class Channel(models.Model): 'email_send': False, 'channel_partner_ids': [(4, self.env.user.partner_id.id)] }) - channel_info = new_channel.channel_info()[0] + channel_info = new_channel.channel_info('creation')[0] notification = _('
created #%s
') % (new_channel.id, new_channel.name,) new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info) @@ -538,10 +541,21 @@ class Channel(models.Model): user isn't registered to. """ domain = expression.AND([ [('name', 'ilike', search)], - [('channel_type', '!=', 'chat')], + [('channel_type', '=', 'channel')], expression.OR([ [('public', '!=', 'private')], [('channel_partner_ids', 'in', [self.env.user.partner_id.id])] ]) ]) 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 f297a751fa9..e8a2ad3951e 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; @@ -23,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 @@ -39,6 +41,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 +70,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; } @@ -71,7 +82,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, @@ -118,13 +129,38 @@ 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; + } } + + // 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; } @@ -207,7 +243,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 +265,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 +314,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 +386,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 +412,7 @@ function on_channel_notification (message) { } }); add_message(message, { show_notification: true }); + invalidate_caches(message.channel_ids); }); } @@ -382,6 +435,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 +449,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 +474,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'); } }); @@ -433,10 +489,10 @@ function on_mark_as_unread_notification (data) { } function on_chat_session_notification (chat_session) { - if ((chat_session.channel_type === "channel") && (chat_session.public === "private") && (chat_session.state === "open")) { + if ((chat_session.channel_type === "channel") && (chat_session.state === "open")) { add_channel(chat_session, {autoswitch: false}); - if (!chat_session.is_minimized) { - web_client.do_notify(_t("Private Channel"), _t("You have been invited to: ") + chat_session.name); + if (!chat_session.is_minimized && chat_session.info !== 'creation') { + web_client.do_notify(_t("Invitation"), _t("You have been invited to: ") + chat_session.name); } } // partner specific change (open a detached window for example) @@ -460,9 +516,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}); } @@ -531,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; }, @@ -618,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..e4908188122 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')); @@ -425,6 +422,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, { display_needactions: this.channel.display_needactions, messages_separator_position: this.messages_separator_position, squash_close_messages: this.channel.type !== 'static', + display_empty_channel: !messages.length && !this.domain.length, + display_no_match: !messages.length && this.domain.length, + display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox", }; }, @@ -581,6 +581,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/js/thread.js b/addons/mail/static/src/js/thread.js index 6fd5838aa48..a1f8fc8a076 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; @@ -79,7 +77,7 @@ var Thread = Widget.extend({ this.$el.html(QWeb.render('mail.ChatThread', { messages: msgs, - options: _.extend({}, this.options, options), + options: options, ORDER: ORDER, })); }, @@ -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/less/client_action.less b/addons/mail/static/src/less/client_action.less index 64f0afe507a..f6479b9a418 100644 --- a/addons/mail/static/src/less/client_action.less +++ b/addons/mail/static/src/less/client_action.less @@ -128,7 +128,7 @@ .o_mail_chat { .o_mail_chat_sidebar { background-color: darken(@gray, 8%); - color: lighten(@gray, 50%); + color: lighten(@gray, 60%); opacity: 0.9; .o_mail_chat_channel_item { @@ -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..6207cdb3980 100644 --- a/addons/mail/static/src/xml/chatter.xml +++ b/addons/mail/static/src/xml/chatter.xml @@ -6,7 +6,7 @@ --> - + @@ -53,7 +53,7 @@ - +