[MERGE] forward port of branch 9.0 up to 00ee6e7b

This commit is contained in:
Martin Trigaux
2015-12-01 10:30:44 +01:00
89 changed files with 937 additions and 504 deletions
+5 -5
View File
@@ -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 = {
+12 -5
View File
@@ -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
+1 -1
View File
@@ -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)
@@ -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;
@@ -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
@@ -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))
+1 -1
View File
@@ -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.
===============================================
+2 -2
View File
@@ -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):
+98 -98
View File
@@ -104,11 +104,11 @@ class base_action_rule(osv.osv):
def onchange_kind(self, cr, uid, ids, kind, context=None):
clear_fields = []
if kind in ['on_create', 'on_create_or_write', 'on_unlink']:
clear_fields = ['filter_pre_id', 'trg_date_id', 'trg_date_range', 'trg_date_range_type']
clear_fields = ['filter_pre_id', 'filter_pre_domain', 'trg_date_id', 'trg_date_range', 'trg_date_range_type']
elif kind in ['on_write', 'on_create_or_write']:
clear_fields = ['trg_date_id', 'trg_date_range', 'trg_date_range_type']
elif kind == 'on_time':
clear_fields = ['filter_pre_id']
clear_fields = ['filter_pre_id', 'filter_pre_domain']
return {'value': dict.fromkeys(clear_fields, False)}
def onchange_filter_pre_id(self, cr, uid, ids, filter_pre_id, context=None):
@@ -119,57 +119,88 @@ class base_action_rule(osv.osv):
ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context)
return {'value': {'filter_domain': ir_filter.domain}}
def _get_eval_context(self, cr, uid, context=None):
@openerp.api.model
def _get_actions(self, records, kinds):
""" Return the actions of the given kinds for records' model. The
returned actions' context contain an object to manage processing.
"""
if '__action_done' not in self._context:
self = self.with_context(__action_done={})
domain = [('model', '=', records._name), ('kind', 'in', kinds)]
actions = self.with_context(active_test=True).search(domain)
return actions.with_env(self.env)
@openerp.api.model
def _get_eval_context(self):
""" Prepare the context used when evaluating python code
:returns: dict -- evaluation context given to (safe_)eval """
return {
'datetime': DT,
'dateutil': dateutil,
'time': time,
'uid': uid,
'user': self.pool['res.users'].browse(cr, uid, uid, context=context),
'uid': self.env.uid,
'user': self.env.user,
}
def _filter(self, cr, uid, action, action_filter, record_ids, domain=False, context=None):
""" Filter the list record_ids that satisfy the domain or the action filter. """
if record_ids and (domain is not False or action_filter):
eval_context = self._get_eval_context(cr, uid, context=context)
if domain is not False:
new_domain = [('id', 'in', record_ids)] + eval(domain, eval_context)
ctx = context
elif action_filter:
assert action.model == action_filter.model_id, "Filter model different from action rule model"
new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain, eval_context)
ctx = dict(context or {})
ctx.update(eval(action_filter.context))
record_ids = self.pool[action.model].search(cr, uid, new_domain, context=ctx)
return record_ids
@openerp.api.model
def _filter_pre(self, records):
""" Filter the records that satisfy the precondition of action ``self``. """
if self.filter_pre_id and records:
eval_context = self._get_eval_context()
domain = [('id', 'in', records.ids)] + eval(self.filter_pre_id.domain, eval_context)
ctx = eval(self.filter_pre_id.context)
return records.with_context(**ctx).search(domain).with_env(records.env)
elif self.filter_pre_domain and records:
eval_context = self._get_eval_context()
domain = [('id', 'in', records.ids)] + eval(self.filter_pre_domain, eval_context)
return records.search(domain)
else:
return records
@openerp.api.model
def _filter_post(self, records):
""" Filter the records that satisfy the postcondition of action ``self``. """
if self.filter_id and records:
eval_context = self._get_eval_context()
domain = [('id', 'in', records.ids)] + eval(self.filter_id.domain, eval_context)
ctx = eval(self.filter_id.context)
return records.with_context(**ctx).search(domain).with_env(records.env)
elif self.filter_domain and records:
eval_context = self._get_eval_context()
domain = [('id', 'in', records.ids)] + eval(self.filter_domain, eval_context)
return records.search(domain)
else:
return records
@openerp.api.multi
def _process(self, records):
""" Process action ``self`` on the ``records`` that have not been done yet. """
# filter out the records on which self has already been done, then mark
# remaining records as done (to avoid recursive processing)
action_done = self._context['__action_done']
records -= action_done.setdefault(self, records.browse())
if not records:
return
action_done[self] |= records
def _process(self, cr, uid, action, record_ids, context=None):
""" process the given action on the records """
model = self.pool[action.model_id.model]
# modify records
values = {}
if 'date_action_last' in model._fields:
values['date_action_last'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
if action.act_user_id and 'user_id' in model._fields:
values['user_id'] = action.act_user_id.id
if 'date_action_last' in records._fields:
values['date_action_last'] = openerp.fields.Datetime.now()
if self.act_user_id and 'user_id' in records._fields:
values['user_id'] = self.act_user_id.id
if values:
model.write(cr, uid, record_ids, values, context=context)
records.write(values)
if action.act_followers and hasattr(model, 'message_subscribe'):
follower_ids = map(int, action.act_followers)
model.message_subscribe(cr, uid, record_ids, follower_ids, context=context)
# subscribe followers
if self.act_followers and hasattr(records, 'message_subscribe'):
records.message_subscribe(self.act_followers.ids)
# execute server actions
if action.server_action_ids:
server_action_ids = map(int, action.server_action_ids)
for record in model.browse(cr, uid, record_ids, context):
action_server_obj = self.pool.get('ir.actions.server')
ctx = dict(context, active_model=model._name, active_ids=[record.id], active_id=record.id)
action_server_obj.run(cr, uid, server_action_ids, context=ctx)
return True
if self.server_action_ids:
for record in records:
ctx = {'active_model': record._name, 'active_ids': record.ids, 'active_id': record.id}
self.server_action_ids.with_context(**ctx).run()
def _register_hook(self, cr):
""" Patch models that should trigger action rules based on creation,
@@ -186,26 +217,19 @@ class base_action_rule(osv.osv):
def make_create():
""" Instanciate a create method that processes action rules. """
def create(self, cr, uid, vals, context=None):
# avoid loops or cascading actions
if context and context.get('action'):
return create.origin(self, cr, uid, vals, context=context)
@openerp.api.model
def create(self, vals):
# retrieve the action rules to possibly execute
actions = self.env['base.action.rule']._get_actions(self, ['on_create', 'on_create_or_write'])
# call original method with a modified context
context = dict(context or {}, action=True)
new_id = create.origin(self, cr, uid, vals, context=context)
# as it is a new record, we do not consider the actions that have a prefilter
action_model = self.pool.get('base.action.rule')
action_dom = [('model', '=', self._name),
('kind', 'in', ['on_create', 'on_create_or_write'])]
action_ids = action_model.search(cr, uid, action_dom, context=dict(context, active_test=True))
# call original method
record = create.origin(self.with_env(actions.env), vals)
# check postconditions, and execute actions on the records that satisfy them
for action in action_model.browse(cr, uid, action_ids, context=context):
if action_model._filter(cr, uid, action, action.filter_id, [new_id], domain=action.filter_domain, context=context):
action_model._process(cr, uid, action, [new_id], context=context)
return new_id
for action in actions.with_context(old_values=None):
action._process(action._filter_post(record))
return record.with_env(self.env)
return create
@@ -215,69 +239,45 @@ class base_action_rule(osv.osv):
# Note: we patch method _write() instead of write() in order to
# catch updates made by field recomputations.
#
def _write(self, cr, uid, ids, vals, context=None):
# avoid loops or cascading actions
if context and context.get('action'):
return _write.origin(self, cr, uid, ids, vals, context=context)
# modify context
context = dict(context or {}, action=True)
ids = [ids] if isinstance(ids, (int, long, str)) else ids
@openerp.api.multi
def _write(self, vals):
# retrieve the action rules to possibly execute
action_model = self.pool.get('base.action.rule')
action_dom = [('model', '=', self._name),
('kind', 'in', ['on_write', 'on_create_or_write'])]
action_ids = action_model.search(cr, uid, action_dom, context=context)
actions = action_model.browse(cr, uid, action_ids, context=context)
actions = self.env['base.action.rule']._get_actions(self, ['on_write', 'on_create_or_write'])
records = self.with_env(actions.env)
# check preconditions
pre_ids = {}
for action in actions:
pre_ids[action] = action_model._filter(cr, uid, action, action.filter_pre_id, ids, domain=action.filter_pre_domain, context=context)
# check preconditions on records
pre = {action: action._filter_pre(records) for action in actions}
# read old values before the update
old_values = {}
for old_vals in self.read(cr, uid, ids, list(vals), context=context):
old_values[old_vals.pop('id')] = old_vals
old_values = {
old_vals.pop('id'): old_vals
for old_vals in records.read(list(vals))
}
# call original method
_write.origin(self, cr, uid, ids, vals, context=context)
_write.origin(records, vals)
# check postconditions, and execute actions on the records that satisfy them
for action in actions:
post_ids = action_model._filter(cr, uid, action, action.filter_id, pre_ids[action], domain=action.filter_domain, context=context)
if post_ids:
action_model._process(cr, uid, action, post_ids, context=dict(context, old_values=old_values))
for action in actions.with_context(old_values=old_values):
action._process(action._filter_post(pre[action]))
return True
return _write
def make_unlink():
""" Instanciate an unlink method that processes action rules. """
def unlink(self, cr, uid, ids, context=None, **kwargs):
if context and context.get('action'):
return unlink.origin(self, cr, uid, ids, context=context)
# modify context
context = dict(context or {}, action=True)
ids = [ids] if isinstance(ids, (int, long, str)) else ids
@openerp.api.multi
def unlink(self, **kwargs):
# retrieve the action rules to possibly execute
action_model = self.pool.get('base.action.rule')
action_dom = [('model', '=', self._name),
('kind', '=', 'on_unlink')]
action_ids = action_model.search(cr, uid, action_dom, context=context)
actions = action_model.browse(cr, uid, action_ids, context=context)
actions = self.env['base.action.rule']._get_actions(self, ['on_unlink'])
records = self.with_env(actions.env)
# check conditions, and execute actions on the records that satisfy them
for action in actions:
pre_ids = action_model._filter(cr, uid, action, action.filter_id, ids, domain=action.filter_domain, context=context)
if pre_ids:
action_model._process(cr, uid, action, pre_ids, context=context)
action._process(action._filter_post(pre[action]))
# call original method
return unlink.origin(self, cr, uid, ids, context=context, **kwargs)
return unlink.origin(self, **kwargs)
return unlink
@@ -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>
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_base_action_rule base.action.rule model_base_action_rule 1 0 0 0
3 access_base_action_rule_config base.action.rule config model_base_action_rule base.group_system 1 1 1 1
4 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
5 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
+10
View File
@@ -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')
@@ -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)
+7 -1
View File
@@ -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
+12 -19
View File
@@ -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 ""
@@ -526,13 +526,6 @@ msgstr ""
msgid "Click to schedule a new meeting."
msgstr ""
#. module: calendar
#: view:calendar.alarm:calendar.view_calendar_alarm_tree
#: model:ir.actions.act_window,name:calendar.action_calendar_alarm
#: model:ir.ui.menu,name:calendar.menu_calendar_alarm
msgid "Calendar Alarm"
msgstr ""
#. module: calendar
#: model:ir.model.fields,field_description:calendar.field_calendar_event_color_partner_id
msgid "Color index of creator"
@@ -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,
};
});
@@ -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;
}
}
@@ -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>
+8
View File
@@ -290,7 +290,15 @@ class crm_lead(format_address, osv.osv):
def on_change_user(self, cr, uid, ids, user_id, context=None):
""" When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """
if user_id and context.get('team_id'):
team = self.pool['crm.team'].browse(cr, uid, context['team_id'], context=context)
if user_id in team.member_ids.ids:
return {}
team_id = self.pool['crm.team']._get_default_team_id(cr, uid, context=context, user_id=user_id)
if user_id and not team_id and self.pool['res.users'].has_group(cr, uid, 'base.group_multi_salesteams'):
team_ids = self.pool.get('crm.team').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
if team_ids:
team_id = team_ids[0]
return {'value': {'team_id': team_id}}
def stage_find(self, cr, uid, cases, team_id, domain=None, order='sequence', context=None):
+2 -2
View File
@@ -158,7 +158,7 @@
</group>
<group>
<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'] }" widget="selection"/>
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>
@@ -496,7 +496,7 @@
</group>
<group>
<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']}" 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>
+1
View File
@@ -4,3 +4,4 @@
import hr
import res_users
import res_partner
+22
View File
@@ -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
+44 -37
View File
@@ -208,47 +208,54 @@ class HrExpense(models.Model):
if any(not expense.journal_id for expense in self):
raise UserError(_("Expenses must have an expense journal specified to generate accounting entries."))
#create the move that will contain the accounting entries
move = self.env['account.move'].create({
'journal_id': self.journal_id.id,
'company_id': self.env.user.company_id.id,
})
journal_dict = {}
for expense in self:
company_currency = expense.company_id.currency_id
diff_currency_p = expense.currency_id != company_currency
#one account.move.line per expense (+taxes..)
move_lines = expense._move_line_get()
if expense.journal_id not in journal_dict:
journal_dict[expense.journal_id] = []
journal_dict[expense.journal_id].append(expense)
#create one more move line, a counterline for the total on payable account
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines)
if expense.payment_mode == 'company_account':
if not expense.bank_journal_id.default_credit_account_id:
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name))
emp_account = expense.bank_journal_id.default_credit_account_id.id
else:
if not expense.employee_id.address_home_id:
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
emp_account = expense.employee_id.address_home_id.property_account_payable_id.id
for journal, expense_list in journal_dict.items():
#create the move that will contain the accounting entries
move = self.env['account.move'].create({
'journal_id': journal.id,
'company_id': self.env.user.company_id.id,
})
for expense in expense_list:
company_currency = expense.company_id.currency_id
diff_currency_p = expense.currency_id != company_currency
#one account.move.line per expense (+taxes..)
move_lines = expense._move_line_get()
move_lines.append({
'type': 'dest',
'name': '/',
'price': total,
'account_id': emp_account,
'date_maturity': expense.date,
'amount_currency': diff_currency_p and total_currency or False,
'currency_id': diff_currency_p and expense.currency_id.id or False,
'ref': expense.employee_id.address_home_id.ref or False
})
#create one more move line, a counterline for the total on payable account
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines)
if expense.payment_mode == 'company_account':
if not expense.bank_journal_id.default_credit_account_id:
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name))
emp_account = expense.bank_journal_id.default_credit_account_id.id
else:
if not expense.employee_id.address_home_id:
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
emp_account = expense.employee_id.address_home_id.property_account_payable_id.id
#convert eml into an osv-valid format
lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines)
move.write({'line_ids': lines})
expense.write({'account_move_id': move.id, 'state': 'post'})
if expense.payment_mode == 'company_account':
expense.paid_expenses()
return move.post()
move_lines.append({
'type': 'dest',
'name': '/',
'price': total,
'account_id': emp_account,
'date_maturity': expense.date,
'amount_currency': diff_currency_p and total_currency or False,
'currency_id': diff_currency_p and expense.currency_id.id or False,
'ref': expense.employee_id.address_home_id.ref or False
})
#convert eml into an osv-valid format
lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines)
move.write({'line_ids': lines})
expense.write({'account_move_id': move.id, 'state': 'post'})
if expense.payment_mode == 'company_account':
expense.paid_expenses()
move.post()
return True
@api.multi
def _move_line_get(self):
+1 -1
View File
@@ -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>
@@ -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 () {
+11 -11
View File
@@ -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
-1
View File
@@ -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">
+3 -1
View File
@@ -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
+17 -3
View File
@@ -166,7 +166,6 @@ class Channel(models.Model):
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
return result
@api.multi
def message_get_email_values(self, notif_mail=None):
self.ensure_one()
@@ -423,6 +422,10 @@ class Channel(models.Model):
for channel in self:
partners_to_add = partners - channel.channel_partner_ids
channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
for partner in partners_to_add:
notification = _('<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)
@@ -525,7 +528,7 @@ class Channel(models.Model):
'email_send': False,
'channel_partner_ids': [(4, self.env.user.partner_id.id)]
})
channel_info = new_channel.channel_info()[0]
channel_info = new_channel.channel_info('creation')[0]
notification = _('<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)
@@ -538,10 +541,21 @@ class Channel(models.Model):
user isn't registered to. """
domain = expression.AND([
[('name', 'ilike', search)],
[('channel_type', '!=', 'chat')],
[('channel_type', '=', 'channel')],
expression.OR([
[('public', '!=', 'private')],
[('channel_partner_ids', 'in', [self.env.user.partner_id.id])]
])
])
return self.search_read(domain, ['id', 'name', 'public'], limit=limit)
@api.model
def channel_fetch_listeners(self, uuid):
""" Return the id, name and email of partners listening to the given channel """
self._cr.execute("""
SELECT P.id, P.name, P.email
FROM mail_channel_partner CP
INNER JOIN res_partner P ON CP.partner_id = P.id
INNER JOIN mail_channel C ON CP.channel_id = C.id
WHERE C.uuid = %s""", (uuid,))
return self._cr.dictfetchall()
+19 -41
View File
@@ -212,49 +212,27 @@ class Partner(models.Model):
return 0
@api.model
def get_mention_suggestions(self, search, channel, exclude=None, limit=8):
def get_static_mention_suggestions(self):
""" To be overwritten to return the id, name and email of partners used as static mention
suggestions loaded once at webclient initialization and stored client side. """
return []
@api.model
def get_mention_suggestions(self, search, limit=8):
""" Return 'limit'-first partners' id, name and email such that the name or email matches a
'search' string. Prioritize partners registered to channel 'channel[channel_id]' if
given, or partners that are followers of a document identified by 'channel[res_model]'
and 'channel[res_id]' otherwise, then users, and finally extend the research to all
partners. Exclude partners whose id is in 'exclude'. """
if exclude is None:
exclude = []
members = []
users = []
partners = []
search_dom = expression.AND([
expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]]),
[('id', 'not in', exclude)]
])
'search' string. Prioritize users, and then extend the research to all partners. """
search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]])
fields = ['id', 'name', 'email']
def search_partners(domain, fields, limit, exclude):
partners = self.search_read(domain, fields, limit=limit)
limit -= len(partners)
exclude += [partner['id'] for partner in partners]
return partners, limit, exclude
# Search users
domain = expression.AND([[('user_ids.id', '!=', False)], search_dom])
users = self.search_read(domain, fields, limit=limit)
# Search users registered to the channel
if 'channel_id' in channel:
domain = expression.AND([[('channel_ids', 'in', [channel['channel_id']])], search_dom])
members, limit, exclude = search_partners(domain, fields, limit, exclude)
else:
domain = expression.AND([
[('res_model', '=', channel['res_model'])],
[('res_id', '=', channel['res_id'])]
])
followers = self.env['mail.followers'].search(domain)
domain = expression.AND([[('id', 'in', followers.mapped('partner_id').ids)], search_dom])
members, limit, exclude = search_partners(domain, fields, limit, exclude)
# Search partners if less than 'limit' users found
partners = []
if len(users) < limit:
partners = self.search_read(search_dom, fields, limit=limit)
# Remove duplicates
partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])]
if limit > 0:
# Search users
domain = expression.AND([[('user_ids.id', '!=', False)], search_dom])
users, limit, exclude = search_partners(domain, fields, limit, exclude)
if limit > 0:
# Search partners
partners = self.search_read(search_dom, fields, limit=limit)
return [members, users, partners]
return [users, partners]
+103 -27
View File
@@ -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) {
+35 -4
View File
@@ -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
+9 -5
View File
@@ -160,11 +160,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.searchview = new SearchView(this, dataset, view_id, {}, options);
this.searchview.on('search_data', this, this.on_search);
this.composer = new ChatComposer(this, {
get_channel_info: function () {
return { channel_id: self.channel.id };
},
});
this.composer = new ChatComposer(this);
this.thread = new ChatThread(this, {
display_help: true
});
@@ -200,6 +196,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
chat_manager.toggle_star_status(message_id);
});
this.composer.on('post_message', this, this.on_post_message);
this.composer.on('input_focused', this, this.on_composer_input_focused);
var def1 = this.thread.prependTo(this.$('.o_mail_chat_content'));
var def2 = this.composer.appendTo(this.$('.o_mail_chat_content'));
@@ -425,6 +422,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
display_needactions: this.channel.display_needactions,
messages_separator_position: this.messages_separator_position,
squash_close_messages: this.channel.type !== 'static',
display_empty_channel: !messages.length && !this.domain.length,
display_no_match: !messages.length && this.domain.length,
display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox",
};
},
@@ -581,6 +581,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.set_channel(channel);
}
},
on_composer_input_focused: function () {
var suggestions = chat_manager.get_mention_partner_suggestions(this.channel);
this.composer.mention_set_prefetched_partners(suggestions);
},
on_click_button_invite: function () {
var title = _.str.sprintf(_t('Invite people to %s'), this.channel.name);
+72 -52
View File
@@ -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;
+11 -25
View File
@@ -2,7 +2,6 @@ odoo.define('mail.ChatThread', function (require) {
"use strict";
var core = require('web.core');
var time = require('web.time');
var Widget = require('web.Widget');
var QWeb = core.qweb;
@@ -49,7 +48,6 @@ var Thread = Widget.extend({
display_order: ORDER.ASC,
display_needactions: true,
display_stars: true,
default_username: _t('Anonymous'),
display_document_link: true,
display_avatar: true,
squash_close_messages: true,
@@ -67,7 +65,7 @@ var Thread = Widget.extend({
// one are both comments wrote by the same author at the same minute
var prev_msg;
_.each(msgs, function (msg) {
if (!prev_msg || (Math.abs(moment(msg.date).diff(prev_msg.date)) > 60000) ||
if (!prev_msg || (Math.abs(msg.date.diff(prev_msg.date)) > 60000) ||
prev_msg.message_type !== 'comment' || msg.message_type !== 'comment' ||
(prev_msg.author_id[0] !== msg.author_id[0])) {
msg.display_author = true;
@@ -79,7 +77,7 @@ var Thread = Widget.extend({
this.$el.html(QWeb.render('mail.ChatThread', {
messages: msgs,
options: _.extend({}, this.options, options),
options: options,
ORDER: ORDER,
}));
},
@@ -113,31 +111,19 @@ var Thread = Widget.extend({
var msg = _.extend({}, message);
// Set the date in the browser timezone
msg.date = moment(time.str_to_datetime(msg.date)).format('YYYY-MM-DD HH:mm:ss');
var date = msg.date.format('YYYY-MM-DD');
// Compute displayed author name or email
if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) {
msg.mailto = msg.email_from;
if (date === moment().format('YYYY-MM-DD')) {
msg.day = _t("Today");
msg.hour = msg.date.fromNow();
} else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) {
msg.day = _t("Yesterday");
msg.hour = msg.date.format('hh:mm');
} else {
msg.displayed_author = msg.author_id && msg.author_id[1] ||
msg.email_from ||
this.options.default_username;
msg.day = msg.date.format('LL');
msg.hour = msg.date.format('hh:mm');
}
// Compute the avatar_url
if (msg.author_id && msg.author_id[0]) {
msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
} else if (msg.message_type === 'email') {
msg.avatar_src = "/mail/static/src/img/email_icon.png";
} else {
msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg";
}
// Compute url of attachments
_.each(msg.attachment_ids, function(a) {
a.url = '/web/content/' + a.id + '?download=true';
});
return msg;
},
+1 -11
View File
@@ -128,7 +128,7 @@
.o_mail_chat {
.o_mail_chat_sidebar {
background-color: darken(@gray, 8%);
color: lighten(@gray, 50%);
color: lighten(@gray, 60%);
opacity: 0.9;
.o_mail_chat_channel_item {
@@ -214,16 +214,6 @@
color: white;
}
}
.o_composer_mentioned_partners {
color: white;
> .badge {
color: white;
border: 1px solid white;
background-color: transparent;
}
}
}
}
}
+2 -2
View File
@@ -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;
}
+2 -2
View File
@@ -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>
+7 -30
View File
@@ -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>
+13 -10
View File
@@ -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">
+1 -1
View File
@@ -18,7 +18,7 @@
#
##############################################################################
{
'name': 'Worldline SIPS Payment Acquiring for online payments',
'name': 'Worldline SIPS',
'version': '1.0',
'author': 'Eezee-It',
'category': 'Hidden',
+4 -3
View File
@@ -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);
+3
View File
@@ -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"/>
+25
View File
@@ -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)
+6 -1
View File
@@ -894,7 +894,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):
+1 -1
View File
@@ -388,7 +388,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 = {}
+1 -1
View File
@@ -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">
+8
View File
@@ -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)
+2
View File
@@ -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>
+2 -2
View File
@@ -34,11 +34,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>
+3 -1
View File
@@ -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
+2 -2
View File
@@ -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')]"/>
@@ -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 -->
-8
View File
@@ -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):
+5 -1
View File
@@ -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
+1 -1
View File
@@ -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),
+1
View File
@@ -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')]}">
+1 -1
View File
@@ -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']
@@ -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) {
+1 -1
View File
@@ -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(', '));
+1
View File
@@ -10,6 +10,7 @@
.o_modal_header {
.o-webclient-padding(@top: 10px, @bottom: 10px);
.clearfix();
.o_search_options {
display: inline-block;
+1 -1
View File
@@ -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");
@@ -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
@@ -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>
@@ -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',
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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>
+14 -2
View File
@@ -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>
+2 -4
View File
@@ -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 = {
+66 -6
View File
@@ -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'
)
+1 -1
View File
@@ -384,7 +384,7 @@ class WebsiteForum(http.Controller):
def post_save(self, forum, post, **kwargs):
if 'post_name' in kwargs and not kwargs.get('post_name').strip():
return request.website.render('website.http_error', {'status_code': _('Bad Request'), 'status_message': _('Title should not be empty.')})
post_tags = forum._tag_to_write_vals(kwargs.get('post_tag', ''))
post_tags = forum._tag_to_write_vals(kwargs.get('post_tags', ''))
vals = {
'tag_ids': post_tags,
'name': kwargs.get('post_name'),
+12 -11
View File
@@ -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 ""
@@ -1621,11 +1627,6 @@ msgstr ""
msgid "Last activity date"
msgstr ""
#. module: website_forum
#: model:ir.model.fields,field_description:website_forum.field_res_users_calendar_last_notif_ack
msgid "Last notification marked as read from base Calendar"
msgstr ""
#. module: website_forum
#: model:ir.ui.view,arch_db:website_forum.header
msgid "Last updated:"
@@ -2058,7 +2059,7 @@ msgstr ""
#. module: website_forum
#: model:ir.ui.view,arch_db:website_forum.header_footer_custom
msgid "Q&amp;A"
msgid "Q&amp;amp;A"
msgstr ""
#. module: website_forum
+1 -1
View File
@@ -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>
@@ -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>
@@ -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>
@@ -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');
@@ -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){
@@ -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', {
@@ -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>
+15
View File
@@ -0,0 +1,15 @@
French Polynesia (Tahiti), 2015-10-22
Invitu agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Cyril Vinh-Tung contact@invitu.com https://github.com/invitu
List of contributors:
Cyril Vinh-Tung contact@invitu.com https://github.com/invitu
+1 -1
View File
@@ -16749,7 +16749,7 @@ msgstr ""
#. module: base
#: model:ir.module.module,shortdesc:base.module_payment_sips
msgid "Worldline SIPS Payment Acquiring for online payments"
msgid "Worldline SIPS"
msgstr ""
#. module: base
+5 -11
View File
@@ -93,7 +93,7 @@ class ir_model(osv.osv):
_defaults = {
'model': 'x_',
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
'state': 'manual',
}
def _check_model_name(self, cr, uid, ids, context=None):
@@ -177,10 +177,8 @@ class ir_model(osv.osv):
def create(self, cr, user, vals, context=None):
if context is None:
context = {}
if context and context.get('manual'):
vals['state']='manual'
res = super(ir_model,self).create(cr, user, vals, context)
if vals.get('state','base')=='manual':
if vals.get('state','manual')=='manual':
# setup models; this automatically adds model in registry
self.pool.setup_models(cr, partial=(not self.pool.ready))
# update database schema
@@ -264,7 +262,7 @@ class ir_model_fields(osv.osv):
'selection': "",
'domain': "[]",
'name': 'x_',
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
'state': 'manual',
'on_delete': 'set null',
'field_description': '',
'selectable': 1,
@@ -444,14 +442,12 @@ class ir_model_fields(osv.osv):
vals['model'] = model_data.model
if context is None:
context = {}
if context and context.get('manual',False):
vals['state'] = 'manual'
if vals.get('ttype', False) == 'selection':
if not vals.get('selection',False):
raise UserError(_('For selection fields, the Selection Options must be given!'))
self._check_selection(cr, user, vals['selection'], context=context)
res = super(ir_model_fields,self).create(cr, user, vals, context)
if vals.get('state','base') == 'manual':
if vals.get('state','manual') == 'manual':
if not vals['name'].startswith('x_'):
raise UserError(_("Custom fields must have a name that starts with 'x_' !"))
@@ -479,8 +475,6 @@ class ir_model_fields(osv.osv):
def write(self, cr, user, ids, vals, context=None):
if context is None:
context = {}
if context and context.get('manual',False):
vals['state'] = 'manual'
#For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
if 'serialization_field_id' in vals or 'name' in vals:
@@ -523,7 +517,7 @@ class ir_model_fields(osv.osv):
raise UserError(_('Can only rename one field at a time!'))
if vals['name'] in obj._fields:
raise UserError(_('Cannot rename field to %s, because that field already exists!') % vals['name'])
if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
if vals.get('state', 'manual') == 'manual' and not vals['name'].startswith('x_'):
raise UserError(_('New field name must still start with x_ , because it is a custom field!'))
if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
raise ValueError('Invalid character in column name')
+3 -3
View File
@@ -41,7 +41,7 @@
</group>
<notebook>
<page string="Fields">
<field context="{'manual':True}" name="field_id">
<field name="field_id">
<tree string="Fields Description">
<field name="name"/>
<field name="field_description"/>
@@ -203,7 +203,7 @@
<field name="name">Models</field>
<field name="res_model">ir.model</field>
<field name="view_type">form</field>
<field name="context">{'manual':True}</field>
<field name="context">{}</field>
<field name="view_id" ref="view_model_tree"/>
</record>
<menuitem action="action_model_model" id="ir_model_model_menu" parent="next_id_9"/>
@@ -351,7 +351,7 @@
<field name="name">Fields</field>
<field name="res_model">ir.model.fields</field>
<field name="view_type">form</field>
<field name="context">{'manual':True}</field>
<field name="context">{}</field>
<field name="view_id" ref="view_model_fields_tree"/>
</record>
<menuitem action="action_model_fields" id="ir_model_model_fields" parent="base.next_id_9"/>
+19 -12
View File
@@ -130,11 +130,13 @@ class ir_translation_import_cursor(object):
AND irt.type = ti.type
AND irt.module = ti.module
AND irt.name = ti.name
AND (ti.type IN ('field', 'help') OR irt.src = ti.src)
AND ( ti.type NOT IN ('model', 'view')
OR (ti.type = 'model' AND ti.res_id = irt.res_id)
OR (ti.type = 'view' AND (irt.res_id IS NULL OR ti.res_id = irt.res_id))
)
AND (
(ti.type = 'model' AND ti.res_id = irt.res_id AND irt.src = ti.src)
OR (ti.type = 'view' AND (irt.res_id IS NULL OR ti.res_id = irt.res_id) AND irt.src = ti.src)
OR (ti.type = 'field')
OR (ti.type = 'help')
OR (ti.type NOT IN ('model', 'view', 'field', 'help') AND irt.src = ti.src)
)
"""
# Step 2: update existing (matching) translations
@@ -536,41 +538,46 @@ class ir_translation(osv.osv):
def insert_missing(self, field, records):
""" Insert missing translations for `field` on `records`. """
records = records.with_context(lang=None)
external_ids = records.get_external_id() # if no xml_id, empty string
if callable(field.translate):
# insert missing translations for each term in src
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value)
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value, module)
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s, %(module)s
FROM res_lang l
WHERE l.active AND NOT EXISTS (
SELECT 1 FROM ir_translation
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND src=%(src)s
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND src=%(src)s AND module=%(module)s
);
"""
for record in records:
module = external_ids[record.id].split('.')[0]
src = record[field.name] or None
for term in set(field.get_trans_terms(src)):
self._cr.execute(query, {
'name': "%s,%s" % (field.model_name, field.name),
'res_id': record.id,
'src': term,
'module': module
})
else:
# insert missing translations for src
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value)
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value, module)
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s, %(module)s
FROM res_lang l
WHERE l.active AND l.code != 'en_US' AND NOT EXISTS (
SELECT 1 FROM ir_translation
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND module=%(module)s
);
UPDATE ir_translation SET src=%(src)s
WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s;
WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s AND module=%(module)s;
"""
for record in records:
module = external_ids[record.id].split('.')[0]
self._cr.execute(query, {
'name': "%s,%s" % (field.model_name, field.name),
'res_id': record.id,
'src': record[field.name] or None,
'module': module
})
self.clear_caches()
+1 -1
View File
@@ -259,7 +259,7 @@ class res_partner(osv.Model, format_address):
'use_parent_address': fields.boolean('Use Company Address', help="Select this if you want to set company's address information for this contact"),
'company_id': fields.many2one('res.company', 'Company', select=1),
'color': fields.integer('Color Index'),
'user_ids': fields.one2many('res.users', 'partner_id', 'Users'),
'user_ids': fields.one2many('res.users', 'partner_id', 'Users', auto_join=True),
'contact_address': fields.function(_address_display, type='char', string='Complete Address'),
# technical field used for managing commercial fields
+1 -1
View File
@@ -119,7 +119,7 @@ class TranslationToolsTestCase(unittest.TestCase):
terms = []
source = """<div>
stuff before
<div translation="off">Do not translate this</div>
<div t-translation="off">Do not translate this</div>
stuff after
</div>"""
result = xml_translate(terms.append, source)
+5 -5
View File
@@ -61,9 +61,9 @@ def report_configuration():
user = config['db_user'] or os.environ.get('PGUSER', 'default')
_logger.info('database: %s@%s:%s', user, host, port)
def rm_pid_file():
def rm_pid_file(main_pid):
config = openerp.tools.config
if not openerp.evented and config['pidfile']:
if config['pidfile'] and main_pid == os.getpid():
try:
os.unlink(config['pidfile'])
except OSError:
@@ -76,10 +76,10 @@ def setup_pid_file():
"""
config = openerp.tools.config
if not openerp.evented and config['pidfile']:
pid = os.getpid()
with open(config['pidfile'], 'w') as fd:
pidtext = "%d" % (os.getpid())
fd.write(pidtext)
atexit.register(rm_pid_file)
fd.write(str(pid))
atexit.register(rm_pid_file, pid)
def export_translation():
config = openerp.tools.config
+1
View File
@@ -305,6 +305,7 @@ class WebRequest(object):
# case, the request cursor is unusable. Rollback transaction to create a new one.
if self._cr:
self._cr.rollback()
self.env.clear()
result = self.endpoint(*a, **kw)
if isinstance(result, Response) and result.is_qweb:
# Early rendering of lazy responses to benefit from @service_model.check protection
+1 -1
View File
@@ -215,7 +215,7 @@ class XMLTranslator(object):
if (
isinstance(node, SKIPPED_ELEMENT_TYPES) or
node.tag in SKIPPED_ELEMENTS or
node.get("translation", "").strip() == "off" or
node.get("t-translation", "").strip() == "off" or
node.tag == "attribute" and node.get("name") not in TRANSLATED_ATTRS
):
# do not translate the contents of the node