summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorMartin Trigaux <mat@odoo.com>2015-12-01 10:54:51 +0100
committerMartin Trigaux <mat@odoo.com>2015-12-01 10:54:51 +0100
commit87f96c942ab4381b489db5b6bf5de959f0ec3d7f (patch)
treee9302d09e1fce1d4544e7fa92d4465105ded0140 /addons
parentd025251c7e4ee4c26462e61b5d63e5c5d58689f8 (diff)
parent90254beedbb6a9bdf055ae582af866073eab4f2c (diff)
[MERGE] forward port of branch saas-7 up to 90254bee
Diffstat (limited to 'addons')
-rw-r--r--addons/account/demo/account_invoice_demo.yml10
-rw-r--r--addons/account/models/account_invoice.py17
-rw-r--r--addons/account/models/partner.py2
-rw-r--r--addons/account/static/src/js/account_reconciliation_widgets.js1
-rw-r--r--addons/account_analytic_default/account_analytic_default.py14
-rw-r--r--addons/account_bank_statement_import/account_bank_statement_import.py6
-rw-r--r--addons/analytic/__openerp__.py2
-rw-r--r--addons/auth_signup/res_users.py4
-rw-r--r--addons/base_action_rule/base_action_rule.py200
-rw-r--r--addons/base_action_rule/base_action_rule_demo.xml7
-rw-r--r--addons/base_action_rule/security/ir.model.access.csv1
-rw-r--r--addons/base_action_rule/test_models.py10
-rw-r--r--addons/base_action_rule/tests/test_base_action_rule.py19
-rw-r--r--addons/calendar/controllers/main.py8
-rw-r--r--addons/calendar/i18n/calendar.pot31
-rw-r--r--addons/calendar/static/src/js/base_calendar.js15
-rw-r--r--addons/calendar/static/src/less/calendar.less41
-rw-r--r--addons/calendar/static/src/xml/base_calendar.xml37
-rw-r--r--addons/crm/crm_lead.py8
-rw-r--r--addons/crm/crm_lead_view.xml5
-rw-r--r--addons/hr/__init__.py1
-rw-r--r--addons/hr/res_partner.py22
-rw-r--r--addons/hr_expense/models/hr_expense.py87
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml2
-rw-r--r--addons/im_livechat/static/src/js/im_livechat.js19
-rw-r--r--addons/l10n_be/i18n_extra/nl_BE.po22
-rw-r--r--addons/l10n_eu_service/wizard/wizard.xml1
-rw-r--r--addons/mail/controllers/main.py4
-rw-r--r--addons/mail/models/mail_channel.py19
-rw-r--r--addons/mail/models/res_partner.py62
-rw-r--r--addons/mail/static/src/js/chat_manager.js130
-rw-r--r--addons/mail/static/src/js/chatter.js39
-rw-r--r--addons/mail/static/src/js/client_action.js14
-rw-r--r--addons/mail/static/src/js/composer.js124
-rw-r--r--addons/mail/static/src/js/thread.js38
-rw-r--r--addons/mail/static/src/less/client_action.less12
-rw-r--r--addons/mail/static/src/less/composer.less4
-rw-r--r--addons/mail/static/src/xml/chatter.xml4
-rw-r--r--addons/mail/static/src/xml/composer.xml37
-rw-r--r--addons/mail/static/src/xml/thread.xml23
-rw-r--r--addons/payment_sips/__openerp__.py2
-rw-r--r--addons/point_of_sale/static/src/js/models.js7
-rw-r--r--addons/portal_sale/portal_sale_view.xml3
-rw-r--r--addons/product/pricelist.py25
-rw-r--r--addons/product/product.py7
-rw-r--r--addons/project/project.py2
-rw-r--r--addons/project/project_data.xml2
-rw-r--r--addons/purchase/purchase.py8
-rw-r--r--addons/purchase/purchase_view.xml2
-rw-r--r--addons/sale/res_config_view.xml4
-rw-r--r--addons/sale/sale.py4
-rw-r--r--addons/sale/sale_view.xml4
-rw-r--r--addons/sale/wizard/sale_make_invoice_advance.xml1
-rw-r--r--addons/sale_layout/models/sale_layout.py8
-rw-r--r--addons/sale_stock/sale_stock.py6
-rw-r--r--addons/stock/stock.py2
-rw-r--r--addons/stock/stock_view.xml1
-rw-r--r--addons/stock_account/stock_account.py13
-rw-r--r--addons/web/controllers/main.py2
-rw-r--r--addons/web/static/src/js/views/form_common.js2
-rw-r--r--addons/web/static/src/js/views/list_view.js2
-rw-r--r--addons/web/static/src/less/modal.less1
-rw-r--r--addons/web_editor/static/src/js/backend.js2
-rw-r--r--addons/web_editor/static/src/js/summernote.js6
-rw-r--r--addons/web_settings_dashboard/static/src/xml/dashboard.xml1
-rw-r--r--addons/web_view_editor/static/src/js/view_editor.js2
-rw-r--r--addons/website/controllers/main.py4
-rw-r--r--addons/website/views/website_templates.xml2
-rw-r--r--addons/website_crm/data/website_crm_data.xml16
-rw-r--r--addons/website_form/controllers/main.py6
-rw-r--r--addons/website_form/models/models.py72
-rw-r--r--addons/website_forum/i18n/website_forum.pot23
-rw-r--r--addons/website_forum/views/website_forum.xml2
-rw-r--r--addons/website_hr_recruitment/data/config_data.xml13
-rw-r--r--addons/website_issue/data/website_issue_data.xml7
-rw-r--r--addons/website_mass_mailing/static/src/js/website_mass_mailing.editor.js4
-rw-r--r--addons/website_quote/static/src/js/website_quotation.js2
-rw-r--r--addons/website_sale/static/src/js/website_sale.js2
-rw-r--r--addons/website_sale_stock/views/website_sale_stock.xml5
79 files changed, 901 insertions, 478 deletions
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
-
- def _process(self, cr, uid, action, record_ids, context=None):
- """ process the given action on the records """
- model = self.pool[action.model_id.model]
+ @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
+
# 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)
-
- # call original method with a modified context
- context = dict(context or {}, action=True)
- new_id = 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'])
- # 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)
-
- # 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)
+ actions = self.env['base.action.rule']._get_actions(self, ['on_write', 'on_create_or_write'])
+ records = self.with_env(actions.env)
+
+ # 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 @@
<field name="act_user_id" ref="base.user_demo"/>
<field name="filter_domain">[('customer', '=', True)]</field>
</record>
+
+ <record id="test_rule_on_line" model="base.action.rule">
+ <field name="name">Test rule on secondary record</field>
+ <field name="kind">on_create</field>
+ <field name="model_id" ref="base_action_rule.model_base_action_rule_line_test"/>
+ <field name="act_user_id" ref="base.user_demo"/>
+ </record>
</data>
</openerp>
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 "<span> hours</span>"
-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 "<span> hours</span>"
+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 ""
@@ -527,13 +527,6 @@ 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"
msgstr ""
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 @@
</t>
</t>
</t>
+
+ <div t-name="CalendarInvitation" class="o_calendar_invitation_page">
+ <div class="pull-right">
+ <t t-esc="attendee_data['current_attendee'].cn"/> (<t t-esc="attendee_data['current_attendee'].email"/>)
+ <div t-if="attendee_data['current_attendee'].state !== 'needsAction'" t-attf-class="o_#{attendee_data['current_attendee'].state}">
+ <t t-if="attendee_data['current_attendee'].state === 'accepted'">Yes I'm going.</t>
+ <t t-if="attendee_data['current_attendee'].state === 'declined'">No I'm not going.</t>
+ </div>
+ </div>
+ <div class="pull-left o_logo">
+ <img class="img img-responsive" src='/web/binary/company_logo'/>
+ </div>
+ <div class="o_event_title">
+ <h2>Calendar Invitation</h2>
+ <h3><t t-esc="attendee_data['meeting'].event"/></h3>
+ </div>
+ <table class="o_event_table table table-striped">
+ <tr>
+ <th>When</th>
+ <td><t t-esc="attendee_data['meeting'].when"/></td>
+ </tr>
+ <tr>
+ <th>Where</th>
+ <td><t t-esc="attendee_data['meeting'].where or '-'"/></td>
+ </tr>
+ <tr>
+ <th>Who</th>
+ <td>
+ <ul>
+ <li t-foreach="attendee_data['attendee']" t-as="attendee" t-attf-class="o_#{attendee.status}">
+ <t t-esc="attendee.name"/>
+ </li>
+ </ul>
+ </td>
+ </tr>
+ </table>
+ </div>
</template>
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index ef52be93934..ac22a1a7bd9 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -291,7 +291,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 367d8fc9484..790cbed583f 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -113,7 +113,8 @@
<field name="fax"/>
</group>
<group>
- <field name="user_id" on_change="on_change_user(user_id, context)" widget="selection"/>
+ <field name="user_id" on_change="on_change_user(user_id, context)"
+ context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_sale_salesman_all_leads'], 'team_id': team_id}"/>
<field name="team_id" widget="selection" domain="[('use_leads','=',True)]"/>
<field name="type" invisible="1"/>
</group>
@@ -434,7 +435,7 @@
</group>
<group>
- <field name="user_id" on_change="on_change_user(user_id, context)" widget="selection"/>
+ <field name="user_id" on_change="on_change_user(user_id, context)" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_sale_salesman_all_leads'], 'team_id': team_id}"/>
<field name="team_id" widget="selection"/>
</group>
<group>
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()
-
- #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
-
- 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()
- return move.post()
+ if expense.journal_id not in journal_dict:
+ journal_dict[expense.journal_id] = []
+ journal_dict[expense.journal_id].append(expense)
+
+ 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()
+
+ #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
+
+ 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 @@
</record>
<record id="hr_expense_entry_action_server" model="ir.actions.server">
- <field name="name">Generate Single Accounting Entry</field>
+ <field name="name">Generate Accounting Entries</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_hr_expense"/>
<field name="state">code</field>
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 @@
<record id="open_l10n_eu_service" model="ir.actions.todo">
<field name="action_id" ref="action_eu_service"/>
<field name="type">automatic</field>
- <field name="state">open</field>
</record>
<record id="view_account_config_settings_inherit" model="ir.ui.view">
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 af7fe9fb756..98310715767 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -429,6 +429,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 = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (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)
@@ -531,7 +535,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 = _('<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (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)
@@ -544,10 +548,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)
-
- 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]
+ # 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']])]
+
+ 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 20078af06a2..b96e9af6777 100644
--- a/addons/mail/static/src/js/client_action.js
+++ b/addons/mail/static/src/js/client_action.js
@@ -171,11 +171,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
});
@@ -211,6 +207,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'));
@@ -436,6 +433,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",
};
},
@@ -592,6 +592,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 <a> 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');
-
- // 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 ||
- 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 if (msg.message_type === 'email') {
- msg.avatar_src = "/mail/static/src/img/email_icon.png";
+ var date = msg.date.format('YYYY-MM-DD');
+
+ 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.avatar_src = "/mail/static/src/img/smiley/avatar.jpg";
+ msg.day = msg.date.format('LL');
+ msg.hour = msg.date.format('hh:mm');
}
- // 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 6b3df329cce..c87f5a4e6bc 100644
--- a/addons/mail/static/src/less/client_action.less
+++ b/addons/mail/static/src/less/client_action.less
@@ -131,7 +131,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 {
@@ -220,16 +220,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 @@
-->
<t t-name="mail.chatter.ChatComposer" t-extend="mail.ChatComposer">
<!-- Insert information before the composer -->
- <t t-jquery=".o_composer_mentioned_partners" t-operation="before">
+ <t t-jquery=".o_composer_attachments_list" t-operation="before">
<!-- Internal note composer -->
<t t-if="widget.options.is_log">
<span class="o_chatter_composer_info">
@@ -53,7 +53,7 @@
</t>
<!-- Add a button to open the full composer dialog -->
- <t t-jquery=".o_composer_buttons" t-operation="append">
+ <t t-jquery=".o_composer_button_add_attachment" t-operation="after">
<button class="btn btn-sm btn-icon fa fa-pencil-square-o o_composer_button_full_composer" type="button"/>
</t>
</t>
diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml
index e47363c07bf..d199c0d5fdf 100644
--- a/addons/mail/static/src/xml/composer.xml
+++ b/addons/mail/static/src/xml/composer.xml
@@ -2,14 +2,13 @@
<templates xml:space="preserve">
<div t-name="mail.ChatComposer" class="o_chat_composer">
- <div class="o_composer_mentioned_partners"/>
<div class="o_composer_attachments_list"/>
<div class="o_composer">
- <textarea class="o_composer_input" placeholder="Write something"/>
+ <textarea class="o_composer_input" placeholder="Write something..."/>
<div class="btn-group o_composer_buttons">
- <button class="btn btn-sm btn-primary o_composer_button_send" type="button">Send</button>
<button class="btn btn-sm btn-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/>
<button class="btn btn-sm btn-icon fa fa-paperclip o_composer_button_add_attachment" type="button"/>
+ <button class="btn btn-sm btn-primary o_composer_button_send" type="button">Send</button>
</div>
</div>
@@ -33,16 +32,16 @@
</t>
</t>
- <t t-name="mail.ChatComposer.MentionPartnerSuggestions">
+ <t t-name="mail.ChatComposer.MentionSuggestions">
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
<t t-set="show_divider" t-value="false"/>
<t t-foreach="suggestions" t-as="group">
- <t t-foreach="group" t-as="partner">
- <li t-if="show_divider &amp;&amp; (partner_index === 0)" class="divider"/>
+ <t t-foreach="group" t-as="suggestion">
+ <li t-if="show_divider &amp;&amp; (suggestion_index === 0)" class="divider"/>
<t t-set="show_divider" t-value="true"/>
- <li class="o_mention_proposition" t-att-data-id="partner.id">
+ <li class="o_mention_proposition" t-att-data-id="suggestion.id">
<a href="#">
- <t t-esc="partner.name"/> <t t-if="partner.email">(<t t-esc="partner.email"/>)</t>
+ <t t-esc="suggestion.name"/> <t t-if="suggestion.email">(<t t-esc="suggestion.email"/>)</t>
</a>
</li>
</t>
@@ -50,26 +49,4 @@
</ul>
</t>
- <t t-name="mail.ChatComposer.MentionChannelSuggestions">
- <ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
- <t t-set="show_divider" t-value="false"/>
- <t t-foreach="suggestions" t-as="group">
- <t t-foreach="group" t-as="channel">
- <li t-if="show_divider &amp;&amp; (channel_index === 0)" class="divider"/>
- <t t-set="show_divider" t-value="true"/>
- <li class="o_mention_proposition" t-att-data-id="channel.id">
- <a href="#"><t t-esc="channel.name"/></a>
- </li>
- </t>
- </t>
- </ul>
- </t>
-
- <t t-name="mail.ChatComposer.MentionTags">
- <t t-if="partners.length">Mention(s): </t>
- <t t-foreach="partners" t-as="partner">
- <span class="badge"><t t-esc="partner.name"/></span>
- </t>
- </t>
-
</templates>
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index 394627940cb..ebf6687c1a3 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -9,9 +9,13 @@
<t t-if="options.display_load_more &amp;&amp; options.display_order == ORDER.DESC"
t-call="mail.ChatThread.LoadMore"/>
</t>
- <t t-if="!messages.length &amp;&amp; options.display_help">
+ <t t-if="options.display_empty_channel">
<t t-call="mail.EmptyChannel" />
</t>
+ <t t-if="options.display_no_match">
+ <div class="o_thread_title">No matches found</div>
+ <div>No message matches your search. Try to change your search filters.</div>
+ </t>
</t>
<!-- When a channel is empty-->
@@ -30,11 +34,11 @@
<t t-if="options.messages_separator_position == 'top'" t-call="mail.MessagesSeparator"/>
<t t-set="current_day" t-value="0"/>
<t t-foreach="messages" t-as="message">
- <div t-if="current_day != message.date.split(' ')[0]" class="o_thread_date_separator">
+ <div t-if="current_day != message.day" class="o_thread_date_separator">
<span class="o_thread_date">
- <t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LL')"/>
+ <t t-esc="message.day"/>
</span>
- <t t-set="current_day" t-value="message.date.split(' ')[0]"/>
+ <t t-set="current_day" t-value="message.day"/>
</div>
<t t-call="mail.ChatThread.Message"/>
@@ -56,8 +60,8 @@
t-att-src="message.avatar_src"
class="o_thread_message_avatar"/>
</t>
- <span t-if="!message.display_author" t-att-title="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LLL')" class="o_thread_message_side_date">
- <t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm')"/>
+ <span t-if="!message.display_author" t-att-title="message.date" class="o_thread_message_side_date">
+ <t t-esc="message.date.format('hh:mm')"/>
</span>
<i t-if="!message.display_author &amp;&amp; options.display_stars &amp;&amp; message.message_type != 'notification'"
t-att-class="'fa o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')"
@@ -83,8 +87,8 @@
<t t-esc="message.displayed_author"/>
</strong>
- <small t-att-title="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LLL')">
- - <t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm A')"/>
+ <small t-att-title="message.date">
+ - <t t-esc="message.hour"/>
</small>
<t t-if="message.model &amp;&amp; (message.model != 'mail.channel') &amp;&amp; options.display_document_link">
on <a class="o_mail_redirect" href="#" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id"><t t-esc="message.record_name"/></a>
@@ -107,7 +111,7 @@
</t>
<t t-call="mail.ChatThread.MessageTracking"/>
</t>
- <p t-if="message.subject" class="o_mail_subject">Subject: <t t-esc="message.subject"/></p>
+ <p t-if="options.display_subject &amp;&amp; message.subject &amp;&amp; message.message_type != 'notification'" class="o_mail_subject">Subject: <t t-esc="message.subject"/></p>
<t t-if="!(message.tracking_value_ids &amp;&amp; message.tracking_value_ids.length &gt; 0)">
<t t-if="message.body_short">
<div class="o_mail_body_short">
@@ -115,7 +119,6 @@
</div>
<div class="o_mail_body_long">
<t t-raw="message.body"/>
- <span class="oe_mail_expand"><a href="#">... read less</a></span>
</div>
</t>
<t t-if="! message.body_short">
diff --git a/addons/payment_sips/__openerp__.py b/addons/payment_sips/__openerp__.py
index 89c36a757e9..9fc4fb26324 100644
--- a/addons/payment_sips/__openerp__.py
+++ b/addons/payment_sips/__openerp__.py
@@ -18,7 +18,7 @@
#
##############################################################################
{
- 'name': 'Worldline SIPS Payment Acquiring for online payments',
+ 'name': 'Worldline SIPS',
'version': '1.0',
'author': 'Eezee-It',
'category': 'Hidden',
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 3f892bb9b53..7eea6cb2725 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -6,6 +6,7 @@ var PosDB = require('point_of_sale.DB');
var devices = require('point_of_sale.devices');
var core = require('web.core');
var Model = require('web.DataModel');
+var formats = require('web.formats');
var session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
@@ -1070,8 +1071,7 @@ exports.Orderline = Backbone.Model.extend({
}
this.product = options.product;
this.price = options.product.price;
- this.quantity = 1;
- this.quantityStr = '1';
+ this.set_quantity(1);
this.discount = 0;
this.discountStr = '0';
this.type = 'unit';
@@ -1134,7 +1134,8 @@ exports.Orderline = Backbone.Model.extend({
if(unit){
if (unit.rounding) {
this.quantity = round_pr(quant, unit.rounding);
- this.quantityStr = this.quantity.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10)));
+ var decimals = Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10));
+ this.quantityStr = formats.format_value(round_di(this.quantity, decimals), { type: 'float', digits: [69, decimals]});
} else {
this.quantity = round_pr(quant, 1);
this.quantityStr = this.quantity.toFixed(0);
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 @@
<field name="groups_id" eval="[(4, ref('base.group_portal'))]"/>
<field name="arch" type="xml">
<data>
+ <header position="replace">
+ <field name="state" invisible="1"/>
+ </header>
<xpath expr='//field[@name="order_line"]/form//field[@name="procurement_ids"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//field[@name="product_id"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_delivered_updateable"]' position="replace"/>
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)
diff --git a/addons/product/product.py b/addons/product/product.py
index a6260b3c9cf..f28f6aa9797 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -898,7 +898,12 @@ class product_product(osv.osv):
def _get_image_variant(self, cr, uid, ids, name, args, context=None):
result = dict.fromkeys(ids, False)
for obj in self.browse(cr, uid, ids, context=context):
- result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
+ if context.get('bin_size'):
+ result[obj.id] = obj.image_variant
+ else:
+ result[obj.id] = tools.image_get_resized_images(obj.image_variant, return_big=True, avoid_resize_medium=True)[name]
+ if not result[obj.id]:
+ result[obj.id] = getattr(obj.product_tmpl_id, name)
return result
def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
diff --git a/addons/project/project.py b/addons/project/project.py
index 8f9c0c598af..e9dc6c06508 100644
--- a/addons/project/project.py
+++ b/addons/project/project.py
@@ -389,7 +389,7 @@ class task(osv.osv):
project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
if project and project.partner_id:
return {'value': {'partner_id': project.partner_id.id}}
- return {'value': {'partner_id': False}}
+ return {}
def onchange_user_id(self, cr, uid, ids, user_id, context=None):
vals = {}
diff --git a/addons/project/project_data.xml b/addons/project/project_data.xml
index 626a8c8253a..654d2557dbf 100644
--- a/addons/project/project_data.xml
+++ b/addons/project/project_data.xml
@@ -296,7 +296,7 @@
<field name="project_id" ref="project.project_project_data"/>
<field name="name">You can set a deadline on a task</field>
<field name="stage_id" ref="project_stage_data_1"/>
- <field name="date_deadline" eval="'2015-%s-01' %(datetime.now().month+1)"/>
+ <field name="date_deadline" eval="datetime.now()+timedelta(days=30)"/>
</record>
<record id="project_task_data_14" model="project.task">
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py
index da13310e77b..bc91dfbb54b 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', string='Destination Location Type',\
+ 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):
'''
@@ -853,6 +860,7 @@ class ProcurementOrder(models.Model):
'price_unit': price_unit,
'procurement_ids': [(4, procurement.id)]
})
+ break
if not po_line:
vals = procurement._prepare_purchase_order_line(po, supplier)
self.env['purchase.order.line'].create(vals)
diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml
index 62985bc322f..e7635066ab3 100644
--- a/addons/purchase/purchase_view.xml
+++ b/addons/purchase/purchase_view.xml
@@ -251,6 +251,8 @@
<group>
<field name="date_planned"/>
<field name="picking_type_id" domain="[('code','=','incoming')]" options="{'no_create': True}" context="{'special_shortened_wh_name': True}" groups="stock.group_locations"/>
+ <field name="dest_address_id" groups="stock.group_locations" attrs="{'invisible': [('default_location_dest_id_usage', '!=', 'customer')], 'required': [('default_location_dest_id_usage', '=', 'customer')]}"/>
+ <field name="default_location_dest_id_usage" invisible="1"/>
<field name="incoterm_id"/>
</group>
<group>
diff --git a/addons/sale/res_config_view.xml b/addons/sale/res_config_view.xml
index da33b5eab0a..0785bef09b3 100644
--- a/addons/sale/res_config_view.xml
+++ b/addons/sale/res_config_view.xml
@@ -35,11 +35,11 @@
<label for="id" string="Customer portal"/>
<div>
<div>
- <field name="module_website_portal" class="oe_inline" widget="upgrade_boolean"/>
+ <field name="module_website_portal" class="oe_inline" />
<label for="module_website_portal"/>
</div>
<div>
- <field name="module_website_sale_digital" class="oe_inline" widget="upgrade_boolean"/>
+ <field name="module_website_sale_digital" class="oe_inline" />
<label for="module_website_sale_digital"/>
</div>
</div>
diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index 355f3e03342..483670b2fea 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -485,7 +485,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/sale_view.xml b/addons/sale/sale_view.xml
index b2722e1b027..308c0d1370c 100644
--- a/addons/sale/sale_view.xml
+++ b/addons/sale/sale_view.xml
@@ -167,7 +167,7 @@
<field name="partner_id" domain="[('customer','=',True)]" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/>
<field name="partner_invoice_id" groups="sale.group_delivery_invoice_address" context="{'default_type':'invoice'}"/>
<field name="partner_shipping_id" groups="sale.group_delivery_invoice_address" context="{'default_type':'delivery'}"/>
- <field name="project_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" groups="sale.group_analytic_accounting"/>
+ <field name="project_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" groups="analytic.group_analytic_accounting"/>
</group>
<group>
<field name="date_order" invisible="1"/>
@@ -361,7 +361,7 @@
<filter string="My Orders" domain="[('user_id','=',uid)]" name="my_sale_orders_filter"/>
<separator/>
<filter string="Quotations" name="draft" domain="[('state','in',('draft','sent'))]" help="Sales Order that haven't yet been confirmed"/>
- <filter string="Sales" name="sales" domain="[('state','in',('progress','Done'))]"/>
+ <filter string="Sales" name="sales" domain="[('state','in',('sale','done'))]"/>
<separator/>
<filter string="To Invoice" domain="[('invoice_status','=','to invoice')]"/>
<filter string="Upselling" domain="[('invoice_status','=','upselling')]"/>
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 @@
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
+ <field name="groups_id" eval="[(4,ref('base.group_sale_salesman'))]"/>
</record>
<!-- TODO: check if we need this -->
diff --git a/addons/sale_layout/models/sale_layout.py b/addons/sale_layout/models/sale_layout.py
index 7d282c15ce2..a0867007fa8 100644
--- a/addons/sale_layout/models/sale_layout.py
+++ b/addons/sale_layout/models/sale_layout.py
@@ -67,10 +67,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'
@@ -100,10 +96,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):
diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py
index 72bf6b0843d..10cd705b253 100644
--- a/addons/sale_stock/sale_stock.py
+++ b/addons/sale_stock/sale_stock.py
@@ -134,7 +134,7 @@ class SaleOrderLine(models.Model):
@api.onchange('product_uom_qty')
def _onchange_product_uom_qty(self):
- if self.state == 'sale' and self.product_id.type != 'service' and self.product_uom_qty < self._origin.product_uom_qty:
+ if self.state == 'sale' and self.product_id.type in ['product', 'consu'] and self.product_uom_qty < self._origin.product_uom_qty:
warning_mess = {
'title': _('Ordered quantity decreased!'),
'message' : _('You are decreasing the ordered quantity! Do not forget to manually update the delivery order if needed.'),
@@ -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
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index 17db57dc25c..d74db0b5af4 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -1920,7 +1920,7 @@ class stock_move(osv.osv):
auto_join=True, help="Location where the system will stock the finished products."),
'partner_id': fields.many2one('res.partner', 'Destination Address ', states={'done': [('readonly', True)]}, help="Optional address where goods are to be delivered, specifically used for allotment"),
-
+ 'picking_partner_id': fields.related('picking_id', 'partner_id', type='many2one', relation='res.partner', string='Transfer Destination Address'),
'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True, copy=False),
'move_orig_ids': fields.one2many('stock.move', 'move_dest_id', 'Original Move', help="Optional: previous stock move when chaining them", select=True),
diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml
index 36b5301613e..4ac81da397f 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -1338,6 +1338,7 @@
<group name="destination_grp" string="Destination" groups="stock.group_locations">
<field name="location_dest_id" />
<field name="partner_id" context="{'contact_display':'partner'}" />
+ <field name="picking_partner_id" context="{'contact_display':'partner'}"/>
<field name="move_dest_id" groups="base.group_no_one" readonly="1"/>
</group>
<group name="quants_grp" string="Reserved Quants" colspan="2" groups="base.group_no_one" attrs="{'invisible': [('state', '=', 'done')]}">
diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py
index fe891865905..bfc8fd1a6b9 100644
--- a/addons/stock_account/stock_account.py
+++ b/addons/stock_account/stock_account.py
@@ -270,7 +270,7 @@ class stock_quant(osv.osv):
valuation_amount = context.get('force_valuation_amount')
else:
if move.product_id.cost_method == 'average':
- valuation_amount = cost if move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal' else move.product_id.standard_price
+ valuation_amount = cost if move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'internal' else move.product_id.standard_price
else:
valuation_amount = cost if move.product_id.cost_method == 'real' else move.product_id.standard_price
#the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
@@ -281,12 +281,17 @@ class stock_quant(osv.osv):
raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (move.product_id.name,))
credit_value = debit_value
- if move.product_id.cost_method == 'average' and move.location_dest_id.usage == 'supplier' and move.company_id.anglo_saxon_accounting:
+ if move.product_id.cost_method == 'average' and move.company_id.anglo_saxon_accounting:
#in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input
#account books the real purchase price, while the stock account books the average price. The difference is
#booked in the dedicated price difference account.
- if move.origin_returned_move_id and move.origin_returned_move_id.purchase_line_id:
- debit_value = move.origin_returned_move_id.price_unit
+ if move.location_dest_id.usage == 'supplier' and move.origin_returned_move_id and move.origin_returned_move_id.purchase_line_id:
+ debit_value = move.origin_returned_move_id.price_unit * qty
+ #in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation
+ #is made using the original average price to negate the delivery effect.
+ if move.location_id.usage == 'customer' and move.origin_returned_move_id:
+ debit_value = move.origin_returned_move_id.price_unit * qty
+ credit_value = debit_value
partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False
debit_line_vals = {
'name': move.name,
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index b2f35500ab4..3d68c05c2e0 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1411,7 +1411,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']
diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js
index aaa39b225f6..80342f42ee8 100644
--- a/addons/web/static/src/js/views/form_common.js
+++ b/addons/web/static/src/js/views/form_common.js
@@ -992,8 +992,8 @@ var SelectCreateDialog = ViewDialog.extend({
this.searchview.destroy();
}
var $header = $('<div/>').addClass('o_modal_header').appendTo(this.$el);
- var $buttons = $('<div/>').addClass('o_search_options').appendTo($header);
var $pager = $('<div/>').addClass('o_pager').appendTo($header);
+ var $buttons = $('<div/>').addClass('o_search_options').appendTo($header);
this.searchview = new SearchView(this, this.dataset, false, search_defaults, {$buttons: $buttons});
this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
if (self.initial_ids) {
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(', '));
diff --git a/addons/web/static/src/less/modal.less b/addons/web/static/src/less/modal.less
index 6635dbaf1f5..9ff0453c4dd 100644
--- a/addons/web/static/src/less/modal.less
+++ b/addons/web/static/src/less/modal.less
@@ -10,6 +10,7 @@
.o_modal_header {
.o-webclient-padding(@top: 10px, @bottom: 10px);
+ .clearfix();
.o_search_options {
display: inline-block;
diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js
index e7c6d93853d..2c7c86466c1 100644
--- a/addons/web_editor/static/src/js/backend.js
+++ b/addons/web_editor/static/src/js/backend.js
@@ -122,8 +122,8 @@ var FieldTextHtmlSimple = widget.extend({
if (this.options['style-inline']) {
transcoder.class_to_style(this.$content);
transcoder.font_to_img(this.$content);
- this.internal_set_value(this.$content.html());
}
+ this.internal_set_value(this.$content.html());
},
destroy_content: function () {
$(".oe-view-manager-content").off("scroll");
diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js
index c5a9b3ebbef..ff509800c50 100644
--- a/addons/web_editor/static/src/js/summernote.js
+++ b/addons/web_editor/static/src/js/summernote.js
@@ -2320,6 +2320,12 @@ function summernote_paste (event) {
var html = clipboardData.getData("text/html");
var $node = $('<div/>').html(html);
+ // if copying source did not provide html, default to plain text
+ if(!html) {
+ $node.text(clipboardData.getData("text/plain")).html(function(_, html){
+ return html.replace(/\r?\n/g,'<br>');
+ });
+ }
/*
remove undesirable tag
diff --git a/addons/web_settings_dashboard/static/src/xml/dashboard.xml b/addons/web_settings_dashboard/static/src/xml/dashboard.xml
index 50c50e4a8eb..5f9f9eee309 100644
--- a/addons/web_settings_dashboard/static/src/xml/dashboard.xml
+++ b/addons/web_settings_dashboard/static/src/xml/dashboard.xml
@@ -25,6 +25,7 @@
<div class="text-center o_web_settings_dashboard_apps">
<i class="fa fa-cog fa-4x text-muted o_browse_apps" style="cursor: pointer;"></i>
<div class="o_web_settings_dashboard_header">
+ <t t-set="installed_apps" t-value="widget.data.installed_apps"/>
<t t-if="installed_apps">
<t t-esc="installed_apps"></t>
<t t-if="installed_apps == 1">Installed App</t>
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 29d4b642920..350a02441f6 100644
--- a/addons/web_view_editor/static/src/js/view_editor.js
+++ b/addons/web_view_editor/static/src/js/view_editor.js
@@ -1032,7 +1032,7 @@ var ViewEditor = 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/addons/website/controllers/main.py b/addons/website/controllers/main.py
index c274a14ac8d..182d05c3c51 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -38,9 +38,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)
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index ea5b4618f4c..2fb0c1c89a9 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -6,7 +6,7 @@
<template id="assets_common" inherit_id="web.assets_common" name="Website assets">
<xpath expr="." position="inside">
- <script groups="base.group_website_designer" type="text/javascript" src="/website/static/src/js/website.tour.rte.js"></script>
+ <script type="text/javascript" src="/website/static/src/js/website.tour.rte.js"></script>
</xpath>
</template>
diff --git a/addons/website_crm/data/website_crm_data.xml b/addons/website_crm/data/website_crm_data.xml
index e74672c71b1..5c454fc942c 100644
--- a/addons/website_crm/data/website_crm_data.xml
+++ b/addons/website_crm/data/website_crm_data.xml
@@ -1,12 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
- <data noupdate="1">
+ <data>
<record id="crm.model_crm_lead" model="ir.model">
<field name="website_form_default_field_id" ref="crm.field_crm_lead_description" />
<field name="website_form_access">True</field>
<field name="website_form_label">Create a lead</field>
</record>
-
+ <function model="ir.model.fields" name="formbuilder_whitelist">
+ <value>crm.lead</value>
+ <value eval="[
+ 'contact_name',
+ 'description',
+ 'email_from',
+ 'name',
+ 'partner_name',
+ 'phone',
+ ]"/>
+ </function>
+ </data>
+ <data noupdate="1">
<record id="action_open_website" model="ir.actions.act_url">
<field name="name">Website Contact Form</field>
<field name="target">self</field>
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index f7f5f0d07ef..e5cb84823d0 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -15,13 +15,11 @@ class WebsiteForm(http.Controller):
@http.route('/website_form/<string:model_name>', type='http', auth="public", methods=['POST'], website=True)
def website_form(self, model_name, **kwargs):
model_record = request.env['ir.model'].search([('model', '=', model_name), ('website_form_access', '=', True)])
-
if not model_record:
return json.dumps(False)
try:
data = self.extract_data(model_record, ** kwargs)
-
# If we encounter an issue while extracting data
except ValidationError, e:
# I couldn't find a cleaner way to pass data to an exception
@@ -96,7 +94,7 @@ class WebsiteForm(http.Controller):
'custom': '', # Custom fields values
}
- authorized_fields = model.sudo().get_authorized_fields();
+ authorized_fields = model.sudo()._get_form_writable_fields()
error_fields = []
for field_name, field_value in kwargs.items():
@@ -183,7 +181,7 @@ class WebsiteForm(http.Controller):
def insert_attachment(self, model, id_record, files):
orphan_attachment_ids = []
record = model.env[model.model].browse(id_record)
- authorized_fields = model.sudo().get_authorized_fields()
+ authorized_fields = model.sudo()._get_form_writable_fields()
for file in files:
custom_field = file.field_name not in authorized_fields
attachment_value = {
diff --git a/addons/website_form/models/models.py b/addons/website_form/models/models.py
index 24d308bd64b..9a6799b6e5b 100644
--- a/addons/website_form/models/models.py
+++ b/addons/website_form/models/models.py
@@ -1,3 +1,5 @@
+import itertools
+
from openerp import tools
from openerp import models, fields, api
@@ -16,18 +18,36 @@ class website_form_model(models.Model):
website_form_default_field_id = fields.Many2one('ir.model.fields', 'Field for custom form data', domain="[('model', '=', model), ('ttype', '=', 'text')]", help="Specify the field which will contain meta and custom form fields datas.")
website_form_label = fields.Char("Label for form action", help="Form action label. Ex: crm.lead could be 'Send an e-mail' and project.issue could be 'Create an Issue'.")
+ def _all_inherited_model_ids(self):
+ return list(itertools.chain(
+ [self.id],
+ *(m._all_inherited_model_ids() for m in self.inherited_model_ids)
+ ))
- def all_inherited_model_ids(self):
- return [self.id] + [m.all_inherited_model_ids() for m in self.inherited_model_ids]
+ def _get_form_writable_fields(self):
+ """
+ Restriction of "authorized fields" (fields which can be used in the
+ form builders) to fields which have actually been opted into form
+ builders and are writable. By default no field is writable by the
+ form builder.
+ """
+ excluded = {
+ field.name
+ for field in self.env['ir.model.fields'].sudo().search([
+ ('model_id', 'in', self._all_inherited_model_ids()),
+ ('website_form_blacklisted', '=', True)
+ ])
+ }
+ return {
+ k: v for k, v in self.get_authorized_fields().iteritems()
+ if k not in excluded
+ }
@api.multi
def get_authorized_fields(self):
model = self.env[self.model]
fields_get = model.fields_get()
- for elem in self.env['ir.model.fields'].search([('model_id', 'in', self.all_inherited_model_ids())]):
- if elem.website_form_blacklisted:
- fields_get.pop(elem.name, None)
for key, val in model._inherits.iteritems():
fields_get.pop(val,None)
@@ -49,4 +69,44 @@ class website_form_model_fields(models.Model):
_name = 'ir.model.fields'
_inherit = 'ir.model.fields'
- website_form_blacklisted = fields.Boolean('Blacklisted in web forms', help='Blacklist this field for web forms')
+ def init(self, cr):
+ # set all existing unset website_form_blacklisted fields to ``true``
+ # (so that we can use it as a whitelist rather than a blacklist)
+ cr.execute('UPDATE ir_model_fields'
+ ' SET website_form_blacklisted=true'
+ ' WHERE website_form_blacklisted IS NULL')
+ # add an SQL-level default value on website_form_blacklisted to that
+ # pure-SQL ir.model.field creations (e.g. in _field_create) generate
+ # the right default value for a whitelist (aka fields should be
+ # blacklisted by default)
+ cr.execute('ALTER TABLE ir_model_fields '
+ ' ALTER COLUMN website_form_blacklisted SET DEFAULT true')
+
+ @api.model
+ def formbuilder_whitelist(self, model, fields):
+ """
+ :param str model: name of the model on which to whitelist fields
+ :param list(str) fields: list of fields to whitelist on the model
+ :return: nothing of import
+ """
+ # postgres does *not* like ``in [EMPTY TUPLE]`` queries
+ if not fields: return False
+
+ # only allow users who can change the website structure
+ if not self.env['res.users'].has_group('base.group_website_designer'):
+ return False
+
+ # the ORM only allows writing on custom fields and will trigger a
+ # registry reload once that's happened. We want to be able to
+ # whitelist non-custom fields and the registry reload absolutely
+ # isn't desirable, so go with a method and raw SQL
+ self.env.cr.execute(
+ "UPDATE ir_model_fields"
+ " SET website_form_blacklisted=false"
+ " WHERE model=%s AND name in %s", (model, tuple(fields)))
+ return True
+
+ website_form_blacklisted = fields.Boolean(
+ 'Blacklisted in web forms', default=True, select=True, # required=True,
+ help='Blacklist this field for web forms'
+ )
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 "<i class=\"text-muted\">awarded users</i>"
msgstr ""
#. module: website_forum
-#: code:addons/website_forum/models/forum.py:419
+#: code:addons/website_forum/models/forum.py:483
#, python-format
msgid "<p>A new answer for <i>%s</i> has been posted. <a href=\"%s/forum/%s/question/%s\">Click here to access the post.</a></p>"
msgstr ""
#. module: website_forum
-#: code:addons/website_forum/models/forum.py:425
+#: code:addons/website_forum/models/forum.py:499
+#, python-format
+msgid "<p>A new question <i>%s</i> has been asked on %s and require your validation. <a href=\"%s/forum/%s/question/%s\">Click here to access the question.</a></p>"
+msgstr ""
+
+#. module: website_forum
+#: code:addons/website_forum/models/forum.py:489
#, python-format
msgid "<p>A new question <i>%s</i> has been asked on %s. <a href=\"%s/forum/%s/question/%s\">Click here to access the question.</a></p>"
msgstr ""
@@ -1622,11 +1628,6 @@ 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:"
msgstr ""
@@ -2058,7 +2059,7 @@ msgstr ""
#. module: website_forum
#: model:ir.ui.view,arch_db:website_forum.header_footer_custom
-msgid "Q&amp;A"
+msgid "Q&amp;amp;A"
msgstr ""
#. module: website_forum
diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml
index 5e1f6bc3e1e..4b9093e8447 100644
--- a/addons/website_forum/views/website_forum.xml
+++ b/addons/website_forum/views/website_forum.xml
@@ -25,7 +25,7 @@
<template id="header_footer_custom" inherit_id="website.footer_default"
name="Footer Questions Link">
<xpath expr="//ul[@id='products']" position="inside">
- <li><a t-attf-href="/forum/%(website_forum.forum_help)d">Q&amp;A</a></li>
+ <li><a t-attf-href="/forum/%(website_forum.forum_help)d">Q&amp;amp;A</a></li>
<li><a href="/forum">Forums</a></li>
</xpath>
</template>
diff --git a/addons/website_hr_recruitment/data/config_data.xml b/addons/website_hr_recruitment/data/config_data.xml
index ee934bb3ca4..24e4ed8b30f 100644
--- a/addons/website_hr_recruitment/data/config_data.xml
+++ b/addons/website_hr_recruitment/data/config_data.xml
@@ -16,10 +16,23 @@
<field name="action_id" ref="action_open_website"/>
<field name="state">open</field>
</record>
+ </data>
+ <data>
<record id="hr_recruitment.model_hr_applicant" model="ir.model">
<field name="website_form_default_field_id" ref="hr_recruitment.field_hr_applicant_description" />
<field name="website_form_access">True</field>
<field name="website_form_label">Apply for a Job</field>
</record>
+ <function model="ir.model.fields" name="formbuilder_whitelist">
+ <value>hr.applicant</value>
+ <value eval="[
+ 'description',
+ 'email_from',
+ 'partner_name',
+ 'partner_phone',
+ 'job_id',
+ 'department_id',
+ ]"/>
+ </function>
</data>
</openerp>
diff --git a/addons/website_issue/data/website_issue_data.xml b/addons/website_issue/data/website_issue_data.xml
index a588449d30d..83f65b516e1 100644
--- a/addons/website_issue/data/website_issue_data.xml
+++ b/addons/website_issue/data/website_issue_data.xml
@@ -1,15 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
- <data noupdate="1">
+ <data>
<record id="project_issue.model_project_issue" model="ir.model">
<field name="website_form_default_field_id" ref="project_issue.field_project_issue_description" />
<field name="website_form_access">True</field>
<field name="website_form_label">Create an issue</field>
</record>
-<!-- Template to blacklist field
- <record id="project_issue.field_project_issue_{field_name}" model="ir.model.fields">
- <field name="website_form_blacklisted">True</field>
- </record>
--->
</data>
</openerp>
diff --git a/addons/website_mass_mailing/static/src/js/website_mass_mailing.editor.js b/addons/website_mass_mailing/static/src/js/website_mass_mailing.editor.js
index e5ff84677c7..10f5771139f 100644
--- a/addons/website_mass_mailing/static/src/js/website_mass_mailing.editor.js
+++ b/addons/website_mass_mailing/static/src/js/website_mass_mailing.editor.js
@@ -68,9 +68,9 @@ options.registry.newsletter_popup = mass_mailing_common.extend({
});
web_editor.Class.include({
- edit: function () {
- this._super();
+ start: function () {
$('body').on('click','#edit_dialog',_.bind(this.edit_dialog, this.rte.editor));
+ return this._super();
},
save : function() {
var $target = $('#wrapwrap').find('#o_newsletter_popup');
diff --git a/addons/website_quote/static/src/js/website_quotation.js b/addons/website_quote/static/src/js/website_quotation.js
index 2539dc79365..f4e17c92bed 100644
--- a/addons/website_quote/static/src/js/website_quotation.js
+++ b/addons/website_quote/static/src/js/website_quotation.js
@@ -53,7 +53,7 @@ if(!$('.o_website_quote').length) {
'submit #accept': 'submitForm',
},
initSignature: function(ev){
- this.$("#signature").empty().jSignature({'decor-color' : '#D1D0CE'});
+ this.$("#signature").empty().jSignature({'decor-color' : '#D1D0CE', 'color': '#000', 'background-color': '#fff'});
this.empty_sign = this.$("#signature").jSignature("getData",'image');
},
clearSignature: function(ev){
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 676ccba1e2f..c316f69d53c 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -146,9 +146,9 @@ $('.oe_website_sale').each(function () {
clickwatch(function(){
$dom_optional.each(function(){
+ $(this).find('.js_quantity').text(value);
product_ids.push($(this).find('span[data-product-id]').data('product-id'));
});
- if (isNaN(value)) value = 0;
$input.data('update_change', true);
ajax.jsonRpc("/shop/cart/update_json", 'call', {
diff --git a/addons/website_sale_stock/views/website_sale_stock.xml b/addons/website_sale_stock/views/website_sale_stock.xml
index b624c72bf72..32a1729123b 100644
--- a/addons/website_sale_stock/views/website_sale_stock.xml
+++ b/addons/website_sale_stock/views/website_sale_stock.xml
@@ -31,4 +31,9 @@
</t>
</xpath>
</template>
+ <template id="report_picking_portal" inherit_id="stock.report_picking" groups="base.group_portal">
+ <xpath expr="//tr[@t-foreach='o.move_lines']" position="attributes">
+ <attribute name="t-foreach">o.move_lines.sudo()</attribute>
+ </xpath>
+ </template>
</odoo> \ No newline at end of file