[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 reference_type: none
payment_term_id: account.account_payment_term payment_term_id: account.account_payment_term
type: 'out_invoice' type: 'out_invoice'
date_invoice: !eval time.strftime('%Y')+'-01-01' date_invoice: !eval time.strftime('%Y-%m')+'-01'
invoice_line_ids: invoice_line_ids:
- name: 'Dell Inspiron Laptop without OS' - name: 'Dell Inspiron Laptop without OS'
product_id: product.product_product_49 product_id: product.product_product_49
@@ -27,7 +27,7 @@
partner_id: base.res_partner_2 partner_id: base.res_partner_2
currency_id: base.EUR currency_id: base.EUR
type: 'out_invoice' type: 'out_invoice'
date_invoice: !eval time.strftime('%Y')+'-01-08' date_invoice: !eval time.strftime('%Y-%m')+'-08'
invoice_line_ids: invoice_line_ids:
- name: '8-port Switch' - name: '8-port Switch'
product_id: product.product_product_51 product_id: product.product_product_51
@@ -94,7 +94,7 @@
reference_type: none reference_type: none
payment_term_id: account.account_payment_term payment_term_id: account.account_payment_term
type: 'in_invoice' type: 'in_invoice'
date_invoice: !eval time.strftime('%Y')+'-01-01' date_invoice: !eval time.strftime('%Y-%m')+'-01'
invoice_line_ids: invoice_line_ids:
- name: 'Toner Cartridge' - name: 'Toner Cartridge'
price_unit: 10.0 price_unit: 10.0
@@ -112,7 +112,7 @@
partner_id: base.res_partner_12 partner_id: base.res_partner_12
currency_id: base.EUR currency_id: base.EUR
type: 'in_invoice' type: 'in_invoice'
date_invoice: !eval time.strftime('%Y')+'-01-01' date_invoice: !eval time.strftime('%Y-%m')+'-01'
- -
!python {model: ir.model.data} : | !python {model: ir.model.data} : |
line_vals = { line_vals = {
@@ -133,7 +133,7 @@
partner_id: base.res_partner_1 partner_id: base.res_partner_1
currency_id: base.EUR currency_id: base.EUR
type: 'in_invoice' type: 'in_invoice'
date_invoice: !eval time.strftime('%Y')+'-01-15' date_invoice: !eval time.strftime('%Y-%m')+'-15'
- -
!python {model: ir.model.data} : | !python {model: ir.model.data} : |
line_vals = { line_vals = {
+12 -5
View File
@@ -116,16 +116,21 @@ class AccountInvoice(models.Model):
if len(lines) != 0: if len(lines) != 0:
for line in lines: for line in lines:
# get the outstanding residual value in invoice currency # 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: 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: 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({ info['content'].append({
'journal_name': line.ref or line.move_id.name, 'journal_name': line.ref or line.move_id.name,
'amount': amount_to_show, 'amount': amount_to_show,
'currency': self.currency_id.symbol, 'currency': currency_id.symbol,
'id': line.id, 'id': line.id,
'position': self.currency_id.position, 'position': currency_id.position,
'digits': [69, self.currency_id.decimal_places], 'digits': [69, self.currency_id.decimal_places],
}) })
info['title'] = type_payment info['title'] = type_payment
@@ -148,7 +153,9 @@ class AccountInvoice(models.Model):
elif self.type in ('in_invoice', 'out_refund'): 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 = 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]) 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: if payment.currency_id and amount_currency != 0:
currency_id = payment.currency_id currency_id = payment.currency_id
amount_to_show = -amount_currency amount_to_show = -amount_currency
+1 -1
View File
@@ -232,7 +232,7 @@ class ResPartner(models.Model):
def _credit_debit_get(self): def _credit_debit_get(self):
tables, where_clause, where_params = self.env['account.move.line']._query_get() tables, where_clause, where_params = self.env['account.move.line']._query_get()
where_params = [tuple(self.ids)] + where_params 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 FROM account_move_line l
LEFT JOIN account_account a ON (l.account_id=a.id) LEFT JOIN account_account a ON (l.account_id=a.id)
LEFT JOIN account_account_type act ON (a.user_type_id=act.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 var propose_partial = self.getCreatedLines().length === 0
&& balance_type === "greater" && balance_type === "greater"
&& Math.abs(self.get("balance")) < Math.abs(last_line.debit - last_line.credit) && 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.partial_reconcile
&& ! last_line.already_paid; && ! last_line.already_paid;
last_line.propose_partial_reconcile = propose_partial; 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 # Remove context so it is not going to filter on product_id with active_id of template
result['context'] = "{}" result['context'] = "{}"
return result 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: if currency_code:
currency = self.env['res.currency'].search([('name', '=ilike', currency_code)], limit=1) currency = self.env['res.currency'].search([('name', '=ilike', currency_code)], limit=1)
if not currency: 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: if currency == company_currency:
currency = False 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 importing into an existing journal, its currency must be the same as the bank statement
if journal: if journal:
journal_currency = journal.currency_id journal_currency = journal.currency_id
if currency == None: if currency is None:
currency = journal_currency currency = journal_currency
if currency and 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 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)) 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', 'version': '1.1',
'website' : 'https://www.odoo.com/page/accounting', 'website' : 'https://www.odoo.com/page/accounting',
'category': 'Hidden/Dependency', 'category': 'Hidden/Dependency',
'depends' : ['base', 'decimal_precision', 'mail'], 'depends' : ['base', 'decimal_precision', 'mail', 'report'],
'description': """ 'description': """
Module for defining analytic accounting object. 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) res_partner.signup_prepare(cr, uid, partner_ids, signup_type="reset", expiration=expiration, context=context)
if not context: context = dict(context or {})
context = {}
# send email to users with their signup url # send email to users with their signup url
template = False template = False
@@ -283,6 +282,7 @@ class res_users(osv.Model):
for user in self.browse(cr, uid, ids, context): for user in self.browse(cr, uid, ids, context):
if not user.email: if not user.email:
raise UserError(_("Cannot send email: user %s has no email address.") % user.name) 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) 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): 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): def onchange_kind(self, cr, uid, ids, kind, context=None):
clear_fields = [] clear_fields = []
if kind in ['on_create', 'on_create_or_write', 'on_unlink']: 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']: elif kind in ['on_write', 'on_create_or_write']:
clear_fields = ['trg_date_id', 'trg_date_range', 'trg_date_range_type'] clear_fields = ['trg_date_id', 'trg_date_range', 'trg_date_range_type']
elif kind == 'on_time': elif kind == 'on_time':
clear_fields = ['filter_pre_id'] clear_fields = ['filter_pre_id', 'filter_pre_domain']
return {'value': dict.fromkeys(clear_fields, False)} return {'value': dict.fromkeys(clear_fields, False)}
def onchange_filter_pre_id(self, cr, uid, ids, filter_pre_id, context=None): 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) ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context)
return {'value': {'filter_domain': ir_filter.domain}} 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 """ Prepare the context used when evaluating python code
:returns: dict -- evaluation context given to (safe_)eval """ :returns: dict -- evaluation context given to (safe_)eval """
return { return {
'datetime': DT, 'datetime': DT,
'dateutil': dateutil, 'dateutil': dateutil,
'time': time, 'time': time,
'uid': uid, 'uid': self.env.uid,
'user': self.pool['res.users'].browse(cr, uid, uid, context=context), 'user': self.env.user,
} }
def _filter(self, cr, uid, action, action_filter, record_ids, domain=False, context=None): @openerp.api.model
""" Filter the list record_ids that satisfy the domain or the action filter. """ def _filter_pre(self, records):
if record_ids and (domain is not False or action_filter): """ Filter the records that satisfy the precondition of action ``self``. """
eval_context = self._get_eval_context(cr, uid, context=context) if self.filter_pre_id and records:
if domain is not False: eval_context = self._get_eval_context()
new_domain = [('id', 'in', record_ids)] + eval(domain, eval_context) domain = [('id', 'in', records.ids)] + eval(self.filter_pre_id.domain, eval_context)
ctx = context ctx = eval(self.filter_pre_id.context)
elif action_filter: return records.with_context(**ctx).search(domain).with_env(records.env)
assert action.model == action_filter.model_id, "Filter model different from action rule model" elif self.filter_pre_domain and records:
new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain, eval_context) eval_context = self._get_eval_context()
ctx = dict(context or {}) domain = [('id', 'in', records.ids)] + eval(self.filter_pre_domain, eval_context)
ctx.update(eval(action_filter.context)) return records.search(domain)
record_ids = self.pool[action.model].search(cr, uid, new_domain, context=ctx) else:
return record_ids 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 # modify records
values = {} values = {}
if 'date_action_last' in model._fields: if 'date_action_last' in records._fields:
values['date_action_last'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) values['date_action_last'] = openerp.fields.Datetime.now()
if action.act_user_id and 'user_id' in model._fields: if self.act_user_id and 'user_id' in records._fields:
values['user_id'] = action.act_user_id.id values['user_id'] = self.act_user_id.id
if values: if values:
model.write(cr, uid, record_ids, values, context=context) records.write(values)
if action.act_followers and hasattr(model, 'message_subscribe'): # subscribe followers
follower_ids = map(int, action.act_followers) if self.act_followers and hasattr(records, 'message_subscribe'):
model.message_subscribe(cr, uid, record_ids, follower_ids, context=context) records.message_subscribe(self.act_followers.ids)
# execute server actions # execute server actions
if action.server_action_ids: if self.server_action_ids:
server_action_ids = map(int, action.server_action_ids) for record in records:
for record in model.browse(cr, uid, record_ids, context): ctx = {'active_model': record._name, 'active_ids': record.ids, 'active_id': record.id}
action_server_obj = self.pool.get('ir.actions.server') self.server_action_ids.with_context(**ctx).run()
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
def _register_hook(self, cr): def _register_hook(self, cr):
""" Patch models that should trigger action rules based on creation, """ Patch models that should trigger action rules based on creation,
@@ -186,26 +217,19 @@ class base_action_rule(osv.osv):
def make_create(): def make_create():
""" Instanciate a create method that processes action rules. """ """ Instanciate a create method that processes action rules. """
def create(self, cr, uid, vals, context=None): @openerp.api.model
# avoid loops or cascading actions def create(self, vals):
if context and context.get('action'): # retrieve the action rules to possibly execute
return create.origin(self, cr, uid, vals, context=context) actions = self.env['base.action.rule']._get_actions(self, ['on_create', 'on_create_or_write'])
# call original method with a modified context # call original method
context = dict(context or {}, action=True) record = create.origin(self.with_env(actions.env), vals)
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))
# check postconditions, and execute actions on the records that satisfy them # check postconditions, and execute actions on the records that satisfy them
for action in action_model.browse(cr, uid, action_ids, context=context): for action in actions.with_context(old_values=None):
if action_model._filter(cr, uid, action, action.filter_id, [new_id], domain=action.filter_domain, context=context): action._process(action._filter_post(record))
action_model._process(cr, uid, action, [new_id], context=context)
return new_id return record.with_env(self.env)
return create return create
@@ -215,69 +239,45 @@ class base_action_rule(osv.osv):
# Note: we patch method _write() instead of write() in order to # Note: we patch method _write() instead of write() in order to
# catch updates made by field recomputations. # catch updates made by field recomputations.
# #
def _write(self, cr, uid, ids, vals, context=None): @openerp.api.multi
# avoid loops or cascading actions def _write(self, vals):
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
# retrieve the action rules to possibly execute # retrieve the action rules to possibly execute
action_model = self.pool.get('base.action.rule') actions = self.env['base.action.rule']._get_actions(self, ['on_write', 'on_create_or_write'])
action_dom = [('model', '=', self._name), records = self.with_env(actions.env)
('kind', 'in', ['on_write', 'on_create_or_write'])]
action_ids = action_model.search(cr, uid, action_dom, context=context)
actions = action_model.browse(cr, uid, action_ids, context=context)
# check preconditions # check preconditions on records
pre_ids = {} pre = {action: action._filter_pre(records) for action in actions}
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)
# read old values before the update # read old values before the update
old_values = {} old_values = {
for old_vals in self.read(cr, uid, ids, list(vals), context=context): 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 # 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 # check postconditions, and execute actions on the records that satisfy them
for action in actions: for action in actions.with_context(old_values=old_values):
post_ids = action_model._filter(cr, uid, action, action.filter_id, pre_ids[action], domain=action.filter_domain, context=context) action._process(action._filter_post(pre[action]))
if post_ids:
action_model._process(cr, uid, action, post_ids, context=dict(context, old_values=old_values))
return True return True
return _write return _write
def make_unlink(): def make_unlink():
""" Instanciate an unlink method that processes action rules. """ """ Instanciate an unlink method that processes action rules. """
def unlink(self, cr, uid, ids, context=None, **kwargs): @openerp.api.multi
if context and context.get('action'): def unlink(self, **kwargs):
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
# retrieve the action rules to possibly execute # retrieve the action rules to possibly execute
action_model = self.pool.get('base.action.rule') actions = self.env['base.action.rule']._get_actions(self, ['on_unlink'])
action_dom = [('model', '=', self._name), records = self.with_env(actions.env)
('kind', '=', 'on_unlink')]
action_ids = action_model.search(cr, uid, action_dom, context=context)
actions = action_model.browse(cr, uid, action_ids, context=context)
# check conditions, and execute actions on the records that satisfy them # check conditions, and execute actions on the records that satisfy them
for action in actions: for action in actions:
pre_ids = action_model._filter(cr, uid, action, action.filter_id, ids, domain=action.filter_domain, context=context) action._process(action._filter_post(pre[action]))
if pre_ids:
action_model._process(cr, uid, action, pre_ids, context=context)
# call original method # call original method
return unlink.origin(self, cr, uid, ids, context=context, **kwargs) return unlink.origin(self, **kwargs)
return unlink return unlink
@@ -43,5 +43,12 @@
<field name="act_user_id" ref="base.user_demo"/> <field name="act_user_id" ref="base.user_demo"/>
<field name="filter_domain">[('customer', '=', True)]</field> <field name="filter_domain">[('customer', '=', True)]</field>
</record> </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> </data>
</openerp> </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,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_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_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), 'active': fields.boolean('Active', required=False),
'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null'), 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null'),
'date_action_last': fields.datetime('Last Action', readonly=1), 'date_action_last': fields.datetime('Last Action', readonly=1),
'line_ids': fields.one2many('base.action.rule.line.test', 'lead_id'),
} }
_defaults = { _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): def message_subscribe(self, cr, uid, ids, partner_ids=None, channel_ids=None, subtype_ids=None, force=True, context=None):
pass 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.state, 'done')
self.assertEqual(lead.user_id, self.user_demo) 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 Check that a rule is executed whenever a field is recomputed after a
change on another model. change on another model.
@@ -91,3 +91,20 @@ class base_action_rule_test(common.TransactionCase):
partner.write({'customer': True}) partner.write({'customer': True})
self.assertTrue(lead.customer) self.assertTrue(lead.customer)
self.assertEqual(lead.user_id, self.user_demo) 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: if attendee:
attendee_data['current_attendee'] = attendee[0] 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) 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 # 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 9.0\n" "Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-11-25 13:28+0000\n" "POT-Creation-Date: 2015-11-26 09:05+0000\n"
"PO-Revision-Date: 2015-11-25 13:28+0000\n" "PO-Revision-Date: 2015-11-26 09:05+0000\n"
"Last-Translator: <>\n" "Last-Translator: <>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -381,26 +381,26 @@ msgid "${object.event_id.name} - Reminder"
msgstr "" msgstr ""
#. module: calendar #. module: calendar
#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form #: code:addons/calendar/calendar.py:775
#: 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
#, python-format #, python-format
msgid "%s at %s To\n" msgid "%s at %s To\n"
" %s at %s (%s)" " %s at %s (%s)"
msgstr "" msgstr ""
#. module: calendar #. module: calendar
#: code:addons/calendar/calendar.py:800 #: code:addons/calendar/calendar.py:773
#, python-format #, python-format
msgid "%s at (%s To %s) (%s)" msgid "%s at (%s To %s) (%s)"
msgstr "" msgstr ""
#. module: calendar #. 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 #, python-format
msgid "A email has been send to specify that the date has been changed !" msgid "A email has been send to specify that the date has been changed !"
msgstr "" msgstr ""
@@ -526,13 +526,6 @@ msgstr ""
msgid "Click to schedule a new meeting." msgid "Click to schedule a new meeting."
msgstr "" 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 #. module: calendar
#: model:ir.model.fields,field_description:calendar.field_calendar_event_color_partner_id #: model:ir.model.fields,field_description:calendar.field_calendar_event_color_partner_id
msgid "Color index of creator" msgid "Color index of creator"
@@ -8,6 +8,7 @@ var Dialog = require('web.Dialog');
var form_common = require('web.form_common'); var form_common = require('web.form_common');
var Model = require('web.DataModel'); var Model = require('web.DataModel');
var Notification = require('web.notification').Notification; var Notification = require('web.notification').Notification;
var session = require('web.session');
var WebClient = require('web.WebClient'); var WebClient = require('web.WebClient');
var widgets = require('web_calendar.widgets'); 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); core.form_widget_registry.add('many2manyattendee', Many2ManyAttendee);
return {
showCalendarInvitation: showCalendarInvitation,
};
}); });
@@ -35,6 +35,10 @@
&.needsAction { &.needsAction {
background-color: @odoo-brand-secondary; background-color: @odoo-brand-secondary;
} }
&.declined {
background-color: @brand-danger;
}
} }
.o_calendar_all_responsibles { .o_calendar_all_responsibles {
@@ -60,3 +64,40 @@
margin-top: 10px; margin-top: 10px;
position: relative; 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> </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> </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): 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 """ When changing the user, also set a team_id or restrict team id
to the ones user_id is member of. """ 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) 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}} return {'value': {'team_id': team_id}}
def stage_find(self, cr, uid, cases, team_id, domain=None, order='sequence', context=None): 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>
<group> <group>
<field name="user_id" on_change="on_change_user(user_id, context)" <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="team_id" widget="selection" domain="[('use_leads','=',True)]"/>
<field name="type" invisible="1"/> <field name="type" invisible="1"/>
</group> </group>
@@ -496,7 +496,7 @@
</group> </group>
<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"/> <field name="team_id" widget="selection"/>
</group> </group>
<group> <group>
+1
View File
@@ -4,3 +4,4 @@
import hr import hr
import res_users 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): if any(not expense.journal_id for expense in self):
raise UserError(_("Expenses must have an expense journal specified to generate accounting entries.")) raise UserError(_("Expenses must have an expense journal specified to generate accounting entries."))
#create the move that will contain the accounting entries journal_dict = {}
move = self.env['account.move'].create({
'journal_id': self.journal_id.id,
'company_id': self.env.user.company_id.id,
})
for expense in self: for expense in self:
company_currency = expense.company_id.currency_id if expense.journal_id not in journal_dict:
diff_currency_p = expense.currency_id != company_currency journal_dict[expense.journal_id] = []
#one account.move.line per expense (+taxes..) journal_dict[expense.journal_id].append(expense)
move_lines = expense._move_line_get()
#create one more move line, a counterline for the total on payable account for journal, expense_list in journal_dict.items():
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines) #create the move that will contain the accounting entries
if expense.payment_mode == 'company_account': move = self.env['account.move'].create({
if not expense.bank_journal_id.default_credit_account_id: 'journal_id': journal.id,
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name)) 'company_id': self.env.user.company_id.id,
emp_account = expense.bank_journal_id.default_credit_account_id.id })
else: for expense in expense_list:
if not expense.employee_id.address_home_id: company_currency = expense.company_id.currency_id
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name)) diff_currency_p = expense.currency_id != company_currency
emp_account = expense.employee_id.address_home_id.property_account_payable_id.id #one account.move.line per expense (+taxes..)
move_lines = expense._move_line_get()
move_lines.append({ #create one more move line, a counterline for the total on payable account
'type': 'dest', total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines)
'name': '/', if expense.payment_mode == 'company_account':
'price': total, if not expense.bank_journal_id.default_credit_account_id:
'account_id': emp_account, raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name))
'date_maturity': expense.date, emp_account = expense.bank_journal_id.default_credit_account_id.id
'amount_currency': diff_currency_p and total_currency or False, else:
'currency_id': diff_currency_p and expense.currency_id.id or False, if not expense.employee_id.address_home_id:
'ref': expense.employee_id.address_home_id.ref or False 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 move_lines.append({
lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines) 'type': 'dest',
move.write({'line_ids': lines}) 'name': '/',
expense.write({'account_move_id': move.id, 'state': 'post'}) 'price': total,
if expense.payment_mode == 'company_account': 'account_id': emp_account,
expense.paid_expenses() 'date_maturity': expense.date,
return move.post() '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 @api.multi
def _move_line_get(self): def _move_line_get(self):
+1 -1
View File
@@ -325,7 +325,7 @@
</record> </record>
<record id="hr_expense_entry_action_server" model="ir.actions.server"> <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="type">ir.actions.server</field>
<field name="model_id" ref="model_hr_expense"/> <field name="model_id" ref="model_hr_expense"/>
<field name="state">code</field> <field name="state">code</field>
@@ -128,15 +128,28 @@ var LivechatButton = Widget.extend({
}, },
add_message: function (data) { add_message: function (data) {
this.messages.push({ var msg = {
id: data.id, id: data.id,
attachment_ids: data.attachment_ids, attachment_ids: data.attachment_ids,
author_id: data.author_id, author_id: data.author_id,
body: data.body, body: data.body,
date: data.date, date: moment(time.str_to_datetime(data.date)),
is_needaction: false, is_needaction: false,
is_note: data.is_note, 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 () { render_messages: function () {
+11 -11
View File
@@ -20,12 +20,12 @@ msgstr ""
#. module: l10n_be #. module: l10n_be
#: model:account.tax.template,name:l10n_be.attn_VAT-IN-V83-00 #: model:account.tax.template,name:l10n_be.attn_VAT-IN-V83-00
msgid "0% Biens d'investissement" msgid "0% Biens d'investissement"
msgstr "" msgstr "0% Investeringsgoederen"
#. module: l10n_be #. module: l10n_be
#: model:account.tax.template,name:l10n_be.attn_VAT-IN-V82-00-G #: model:account.tax.template,name:l10n_be.attn_VAT-IN-V82-00-G
msgid "0% Biens divers" msgid "0% Biens divers"
msgstr "" msgstr "0% Diverse goederen"
#. module: l10n_be #. module: l10n_be
#: model:account.tax.template,name:l10n_be.attn_VAT-OUT-00-CC #: model:account.tax.template,name:l10n_be.attn_VAT-OUT-00-CC
@@ -189,7 +189,7 @@ msgstr "Vooruitbetalingen"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a360 #: model:account.account.template,name:l10n_be.a360
msgid "Acomptes versés sur achats pour stocks" msgid "Acomptes versés sur achats pour stocks"
msgstr "" msgstr "Vooruitbetalingen op aankopen voor aandelen"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a4550 #: model:account.account.template,name:l10n_be.a4550
@@ -239,7 +239,7 @@ msgstr "Advertenties, publiciteit, propaganda en documentatie"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a3100 #: model:account.account.template,name:l10n_be.a3100
msgid "Approvisionements et fournitures" msgid "Approvisionements et fournitures"
msgstr "" msgstr "Materialen en benodigdheden"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a4160 #: model:account.account.template,name:l10n_be.a4160
@@ -280,7 +280,7 @@ msgstr "Overige leningen"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a4125 #: model:account.account.template,name:l10n_be.a4125
msgid "Autres impôts belges" msgid "Autres impôts belges"
msgstr "" msgstr "Overige Belgische belastingen"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a49102 #: model:account.account.template,name:l10n_be.a49102
@@ -301,7 +301,7 @@ msgstr "Voorschotten en leningen aan het personeel"
#. module: l10n_be #. module: l10n_be
#: model:account.chart.template,name:l10n_be.l10nbe_chart_template #: model:account.chart.template,name:l10n_be.l10nbe_chart_template
msgid "Belgian PCMN" msgid "Belgian PCMN"
msgstr "" msgstr "Belgische PCMN"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a140 #: model:account.account.template,name:l10n_be.a140
@@ -331,7 +331,7 @@ msgstr "Vrachtwagens"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a1000 #: model:account.account.template,name:l10n_be.a1000
msgid "Capital" msgid "Capital"
msgstr "" msgstr "Kapitaal"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a418 #: model:account.account.template,name:l10n_be.a418
@@ -341,7 +341,7 @@ msgstr "Borgtochten betaald in contanten"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a6600 #: model:account.account.template,name:l10n_be.a6600
msgid "Charges exceptionelles" msgid "Charges exceptionelles"
msgstr "" msgstr "Uitzonderlijke kosten"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a6101 #: model:account.account.template,name:l10n_be.a6101
@@ -371,7 +371,7 @@ msgstr "Klanten"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a370 #: model:account.account.template,name:l10n_be.a370
msgid "Commandes en cours d'exécution" msgid "Commandes en cours d'exécution"
msgstr "" msgstr "Lopende opdrachten"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a49101 #: model:account.account.template,name:l10n_be.a49101
@@ -448,7 +448,7 @@ msgstr "Leasingschulden van roerende goederen"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a17300 #: model:account.account.template,name:l10n_be.a17300
msgid "Dettes en compte \\ Banque A" msgid "Dettes en compte \\ Banque A"
msgstr "" msgstr "Schulden op rekeningen \\Bank A"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a4597 #: model:account.account.template,name:l10n_be.a4597
@@ -636,7 +636,7 @@ msgstr "Diverse huren"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a3400 #: model:account.account.template,name:l10n_be.a3400
msgid "Marchandises" msgid "Marchandises"
msgstr "" msgstr "Goederen"
#. module: l10n_be #. module: l10n_be
#: model:account.account.template,name:l10n_be.a300 #: 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"> <record id="open_l10n_eu_service" model="ir.actions.todo">
<field name="action_id" ref="action_eu_service"/> <field name="action_id" ref="action_eu_service"/>
<field name="type">automatic</field> <field name="type">automatic</field>
<field name="state">open</field>
</record> </record>
<record id="view_account_config_settings_inherit" model="ir.ui.view"> <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({ result.append({
'id': follower.id, 'id': follower.id,
'name': follower.partner_id.name or follower.channel_id.name, '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_model': 'res.partner' if follower.partner_id else 'mail.channel',
'res_id': follower.partner_id.id or follower.channel_id.id, 'res_id': follower.partner_id.id or follower.channel_id.id,
'is_editable': is_editable, 'is_editable': is_editable,
@@ -241,6 +242,7 @@ class MailController(http.Controller):
values = { values = {
'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(), 'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(),
'chatter_needaction_auto': request.env.user.chatter_needaction_auto, '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 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") self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
return result return result
@api.multi @api.multi
def message_get_email_values(self, notif_mail=None): def message_get_email_values(self, notif_mail=None):
self.ensure_one() self.ensure_one()
@@ -423,6 +422,10 @@ class Channel(models.Model):
for channel in self: for channel in self:
partners_to_add = partners - channel.channel_partner_ids 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]}) 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 # broadcast the channel header to the added partner
self._broadcast(partner_ids) self._broadcast(partner_ids)
@@ -525,7 +528,7 @@ class Channel(models.Model):
'email_send': False, 'email_send': False,
'channel_partner_ids': [(4, self.env.user.partner_id.id)] '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,) 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") 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) 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. """ user isn't registered to. """
domain = expression.AND([ domain = expression.AND([
[('name', 'ilike', search)], [('name', 'ilike', search)],
[('channel_type', '!=', 'chat')], [('channel_type', '=', 'channel')],
expression.OR([ expression.OR([
[('public', '!=', 'private')], [('public', '!=', 'private')],
[('channel_partner_ids', 'in', [self.env.user.partner_id.id])] [('channel_partner_ids', 'in', [self.env.user.partner_id.id])]
]) ])
]) ])
return self.search_read(domain, ['id', 'name', 'public'], limit=limit) 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 return 0
@api.model @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 """ 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 'search' string. Prioritize users, and then extend the research to all partners. """
given, or partners that are followers of a document identified by 'channel[res_model]' search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]])
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)]
])
fields = ['id', 'name', 'email'] fields = ['id', 'name', 'email']
def search_partners(domain, fields, limit, exclude): # Search users
partners = self.search_read(domain, fields, limit=limit) domain = expression.AND([[('user_ids.id', '!=', False)], search_dom])
limit -= len(partners) users = self.search_read(domain, fields, limit=limit)
exclude += [partner['id'] for partner in partners]
return partners, limit, exclude
# Search users registered to the channel # Search partners if less than 'limit' users found
if 'channel_id' in channel: partners = []
domain = expression.AND([[('channel_ids', 'in', [channel['channel_id']])], search_dom]) if len(users) < limit:
members, limit, exclude = search_partners(domain, fields, limit, exclude) partners = self.search_read(search_dom, fields, limit=limit)
else: # Remove duplicates
domain = expression.AND([ partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])]
[('res_model', '=', channel['res_model'])],
[('res_id', '=', channel['res_id'])]
])
followers = self.env['mail.followers'].search(domain)
domain = expression.AND([[('id', 'in', followers.mapped('partner_id').ids)], search_dom])
members, limit, exclude = search_partners(domain, fields, limit, exclude)
if limit > 0: return [users, partners]
# 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]
+103 -27
View File
@@ -6,6 +6,7 @@ var core = require('web.core');
var data = require('web.data'); var data = require('web.data');
var Model = require('web.Model'); var Model = require('web.Model');
var session = require('web.session'); var session = require('web.session');
var time = require('web.time');
var web_client = require('web.web_client'); var web_client = require('web.web_client');
var _t = core._t; var _t = core._t;
@@ -23,6 +24,7 @@ var channel_defs = {};
var emojis = []; var emojis = [];
var emoji_substitutions = {}; var emoji_substitutions = {};
var needaction_counter = 0; var needaction_counter = 0;
var mention_partner_suggestions = [];
var discuss_ids = {}; var discuss_ids = {};
// Message and channel manipulation helpers // Message and channel manipulation helpers
@@ -39,6 +41,13 @@ function add_message (data, options) {
messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg); messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
_.each(msg.channel_ids, function (channel_id) { _.each(msg.channel_ids, function (channel_id) {
var channel = chat_manager.get_channel(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) { if (channel && channel.hidden) {
channel.hidden = false; channel.hidden = false;
chat_manager.bus.trigger('new_channel', channel); chat_manager.bus.trigger('new_channel', channel);
@@ -61,6 +70,8 @@ function add_message (data, options) {
if (!options.silent) { if (!options.silent) {
chat_manager.bus.trigger('new_message', msg); chat_manager.bus.trigger('new_message', msg);
} }
} else if (options.domain && options.domain !== []) {
add_to_cache(msg, options.domain);
} }
return msg; return msg;
} }
@@ -71,7 +82,7 @@ function make_message (data) {
author_id: data.author_id, author_id: data.author_id,
body_short: data.body_short || "", body_short: data.body_short || "",
body: data.body || "", body: data.body || "",
date: data.date, date: moment(time.str_to_datetime(data.date)),
message_type: data.message_type, message_type: data.message_type,
subtype_description: data.subtype_description, subtype_description: data.subtype_description,
is_note: data.is_note, is_note: data.is_note,
@@ -118,13 +129,38 @@ function make_message (data) {
if (_.contains(data.starred_partner_ids, session.partner_id)) { if (_.contains(data.starred_partner_ids, session.partner_id)) {
msg.is_starred = true; msg.is_starred = true;
} }
var real_channels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred'); if (msg.model === 'mail.channel') {
var origin = real_channels.length ? real_channels[0] : undefined; var real_channels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred');
var channel = origin && chat_manager.get_channel(origin); var origin = real_channels.length === 1 ? real_channels[0] : undefined;
if (channel) { var channel = origin && chat_manager.get_channel(origin);
msg.origin_id = origin; if (channel) {
msg.origin_name = channel.name; 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; return msg;
} }
@@ -207,7 +243,7 @@ function make_channel (data, options) {
cache: {'[]': { cache: {'[]': {
all_history_loaded: false, all_history_loaded: false,
loaded: false, loaded: false,
message_ids: [], messages: [],
}}, }},
}; };
if (channel.type === "channel" && data.public !== "private") { if (channel.type === "channel" && data.public !== "private") {
@@ -229,17 +265,39 @@ function get_channel_cache (channel, domain) {
channel.cache[stringified_domain] = { channel.cache[stringified_domain] = {
all_history_loaded: false, all_history_loaded: false,
loaded: false, loaded: false,
message_ids: [], messages: [],
}; };
} }
return channel.cache[stringified_domain]; 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) { function remove_message_from_channel (channel_id, message) {
message.channel_ids = _.without(message.channel_ids, channel_id); message.channel_ids = _.without(message.channel_ids, channel_id);
var channel = _.findWhere(channels, { id: channel_id }); var channel = _.findWhere(channels, { id: channel_id });
_.each(channel.cache, function (cache) { _.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 || []); domain = new data.CompoundDomain(domain, options.domain || []);
} }
if (options.load_more) { if (options.load_more) {
var min_message_id = _.chain(messages) var min_message_id = cache.messages[0].id;
.filter(function (msg) { return _.contains(cache.message_ids, msg.id); })
.pluck("id")
.min()
.value();
domain = new data.CompoundDomain([['id', '<', min_message_id]], domain); domain = new data.CompoundDomain([['id', '<', min_message_id]], domain);
} }
return MessageModel.call('message_fetch', [domain], {limit: LIMIT}).then(function (msgs) { 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) { if (!cache.all_history_loaded) {
cache.all_history_loaded = msgs.length < LIMIT; cache.all_history_loaded = msgs.length < LIMIT;
} }
cache.loaded = true; cache.loaded = true;
_.each(msgs, function (msg) { _.each(msgs, function (msg) {
add_message(msg, {channel_id: channel.id, silent: true}); add_message(msg, {channel_id: channel.id, silent: true, domain: options.domain});
});
return _.filter(messages, function (m) {
return _.contains(cache.message_ids, m.id);
}); });
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) { function on_needaction_notification (message) {
message = add_message(message, { channel_id: 'channel_inbox', show_notification: true} ); message = add_message(message, { channel_id: 'channel_inbox', show_notification: true} );
invalidate_caches(message.channel_ids);
needaction_counter++; needaction_counter++;
_.each(message.channel_ids, function (channel_id) { _.each(message.channel_ids, function (channel_id) {
var channel = chat_manager.get_channel(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 }); 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) { _.each(data.message_ids, function (msg_id) {
var message = _.findWhere(messages, { id: msg_id }); var message = _.findWhere(messages, { id: msg_id });
if (message) { if (message) {
invalidate_caches(message.channel_ids);
message.is_starred = data.starred; message.is_starred = data.starred;
if (!message.is_starred) { if (!message.is_starred) {
remove_message_from_channel("channel_starred", message); 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) { _.each(data.message_ids, function (msg_id) {
var message = _.findWhere(messages, { id: msg_id }); var message = _.findWhere(messages, { id: msg_id });
if (message) { if (message) {
invalidate_caches(message.channel_ids);
remove_message_from_channel("channel_inbox", message); remove_message_from_channel("channel_inbox", message);
chat_manager.bus.trigger('update_message', 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) { _.each(data.message_ids, function (message_id) {
var message = _.findWhere(messages, { id: message_id }); var message = _.findWhere(messages, { id: message_id });
if (message) { if (message) {
invalidate_caches(message.channel_ids);
add_channel_to_message(message, 'channel_inbox'); 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) { 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}); add_channel(chat_session, {autoswitch: false});
if (!chat_session.is_minimized) { if (!chat_session.is_minimized && chat_session.info !== 'creation') {
web_client.do_notify(_t("Private Channel"), _t("You have been invited to: ") + chat_session.name); web_client.do_notify(_t("Invitation"), _t("You have been invited to: ") + chat_session.name);
} }
} }
// partner specific change (open a detached window for example) // 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 = this.get_channel(options.channel_id);
var channel_cache = get_channel_cache(channel, options.domain); var channel_cache = get_channel_cache(channel, options.domain);
if (channel_cache.loaded) { if (channel_cache.loaded) {
return $.when(_.filter(messages, function (message) { return $.when(channel_cache.messages);
return _.contains(message.channel_ids, options.channel_id);
}));
} else { } else {
return fetch_from_channel(channel, {domain: options.domain}); return fetch_from_channel(channel, {domain: options.domain});
} }
@@ -531,6 +585,27 @@ var chat_manager = {
return get_channel_cache(channel, domain).all_history_loaded; 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() { get_emojis: function() {
return emojis; return emojis;
}, },
@@ -618,6 +693,7 @@ function init () {
_.each(channels, add_channel); _.each(channels, add_channel);
}); });
needaction_counter = result.needaction_inbox_counter; needaction_counter = result.needaction_inbox_counter;
mention_partner_suggestions = result.mention_partner_suggestions;
}); });
var load_emojis = session.rpc("/mail/chat_init").then(function (result) { 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.value = [];
this.followers = []; this.followers = [];
this.followers_fetched = $.Deferred();
this.data_subtype = {}; this.data_subtype = {};
this.view_is_editable = this.__parentedParent.is_action_enabled('edit'); this.view_is_editable = this.__parentedParent.is_action_enabled('edit');
@@ -188,9 +189,14 @@ var Followers = form_common.AbstractField.extend({
}, },
fetch_followers: function (value_) { fetch_followers: function (value_) {
var self = this;
this.value = value_ || []; this.value = value_ || [];
return ajax.jsonRpc('/mail/read_followers', 'call', {'follower_ids': this.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('display_buttons'))
.then(this.proxy('fetch_subtypes')); .then(this.proxy('fetch_subtypes'));
}, },
@@ -706,6 +712,7 @@ var Chatter = form_common.AbstractField.extend({
if (this.followers) { if (this.followers) {
this.$('.o_chatter_topbar').append(this.followers.$el); this.$('.o_chatter_topbar').append(this.followers.$el);
this.followers.on('redirect', this, this.on_redirect); this.followers.on('redirect', this, this.on_redirect);
this.followers.on('followers_update', this, this.on_followers_update);
} }
this.thread = new ChatThread(this, { 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 () { load_more_messages: function () {
this.fetch_and_render_thread(this.msg_ids, {force_fetch: true}); 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, internal_subtypes: this.options.internal_subtypes,
is_log: options && options.is_log, is_log: options && options.is_log,
record_name: this.record_name, 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 () { this.composer.insertBefore(this.$('.o_mail_thread')).then(function () {
// destroy existing composer // 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 = new SearchView(this, dataset, view_id, {}, options);
this.searchview.on('search_data', this, this.on_search); this.searchview.on('search_data', this, this.on_search);
this.composer = new ChatComposer(this, { this.composer = new ChatComposer(this);
get_channel_info: function () {
return { channel_id: self.channel.id };
},
});
this.thread = new ChatThread(this, { this.thread = new ChatThread(this, {
display_help: true display_help: true
}); });
@@ -200,6 +196,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
chat_manager.toggle_star_status(message_id); chat_manager.toggle_star_status(message_id);
}); });
this.composer.on('post_message', this, this.on_post_message); 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 def1 = this.thread.prependTo(this.$('.o_mail_chat_content'));
var def2 = this.composer.appendTo(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, display_needactions: this.channel.display_needactions,
messages_separator_position: this.messages_separator_position, messages_separator_position: this.messages_separator_position,
squash_close_messages: this.channel.type !== 'static', 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); 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 () { on_click_button_invite: function () {
var title = _.str.sprintf(_t('Invite people to %s'), this.channel.name); 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 QWeb = core.qweb;
var _t = core._t; 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 // The MentionManager allows the Composer to register listeners. For each
// listener, it detects if the user is currently typing a mention (starting by a // 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 // 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 // add the selected suggestion to the list
var selection = this.active_listener.selection; if (this.active_listener.selection.length) {
if (selection.length && this.active_listener.sort_selection) {
// get mention matches (ordered by index in the text) // get mention matches (ordered by index in the text)
var matches = this._get_match(text_input, this.active_listener); var matches = this._get_match(text_input, this.active_listener);
var index = get_mention_index(matches, this._get_selection_positions().start); 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 { } 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 // update input text, and reset dropdown
var cursor_position = this._get_selection_positions().start; var cursor_position = this._get_selection_positions().start;
@@ -90,10 +101,7 @@ var MentionManager = Widget.extend({
* @param {char} [delimiter] the mention delimiter * @param {char} [delimiter] the mention delimiter
* @param {function} [fetch_callback] the callback to fetch mention suggestions * @param {function} [fetch_callback] the callback to fetch mention suggestions
* @param {string} [model] the model used for redirection * @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 {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) { register: function (listener) {
this.listeners.push(_.extend(listener, { this.listeners.push(_.extend(listener, {
@@ -137,7 +145,6 @@ var MentionManager = Widget.extend({
*/ */
detect_delimiter: function () { detect_delimiter: function () {
var self = this; var self = this;
var options = this.options;
var text_val = this.composer.$input.val(); var text_val = this.composer.$input.val();
var cursor_position = this._get_selection_positions().start; var cursor_position = this._get_selection_positions().start;
var left_string = text_val.substring(0, cursor_position); var left_string = text_val.substring(0, cursor_position);
@@ -146,7 +153,7 @@ var MentionManager = Widget.extend({
var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))"; var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))";
var regex_start = new RegExp(pattern, "g"); var regex_start = new RegExp(pattern, "g");
search_str = search_str.replace(/^\s\s*|^[\n\r]/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, ''); search_str = search_str.replace(pattern, '');
return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false; 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
// 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.
// 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.
// The 'typing_speed' option should approach the time for a human to type a letter. clearTimeout(this.mention_fetch_timer);
clearTimeout(this.mention_fetch_timer); this.mention_fetch_timer = setTimeout(function () {
this.mention_fetch_timer = setTimeout(function () { if (self.active_listener) {
$.when(listener.fetch_callback(self.mention_word, self.active_listener)).then(function (suggestions) { $.when(listener.fetch_callback(self.mention_word)).then(function (suggestions) {
self.set('mention_suggestions', suggestions); self.set('mention_suggestions', suggestions);
}); });
}, this.options.typing_speed); } else {
} else { self.set('mention_suggestions', []); // close the dropdown
this.set('mention_suggestions', []); // close the dropdown }
} }, this.options.typing_speed);
}, },
/** /**
@@ -201,7 +208,7 @@ var MentionManager = Widget.extend({
} }
} }
if (to_remove.length) { 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 () { reset_selections: function () {
var self = this;
_.each(this.listeners, function (listener) { _.each(this.listeners, function (listener) {
self._update_listener_selection(listener, []); listener.selection = [];
}); });
}, },
@@ -269,7 +275,7 @@ var MentionManager = Widget.extend({
}, },
_render_suggestions: function () { _render_suggestions: function () {
if (_.flatten(this.get('mention_suggestions')).length) { 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'), suggestions: this.get('mention_suggestions'),
})); }));
this.$el this.$el
@@ -282,12 +288,6 @@ var MentionManager = Widget.extend({
this.open = false; 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 // Cursor position and selection utils
_get_selection_positions: function () { _get_selection_positions: function () {
@@ -327,10 +327,8 @@ var Composer = Widget.extend({
input_max_height: 150, input_max_height: 150,
input_min_height: 28, input_min_height: 28,
mention_fetch_limit: 8, mention_fetch_limit: 8,
get_channel_info: function () {},
}); });
this.context = this.options.context; this.context = this.options.context;
this.get_channel_info = this.options.get_channel_info;
// Attachments // Attachments
this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context); this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
@@ -343,19 +341,14 @@ var Composer = Widget.extend({
delimiter: '@', delimiter: '@',
fetch_callback: this.mention_fetch_partners.bind(this), fetch_callback: this.mention_fetch_partners.bind(this),
model: 'res.partner', model: 'res.partner',
notify_selection_changed: true,
redirect_classname: 'o_mail_redirect', redirect_classname: 'o_mail_redirect',
sort_selection: true,
suggestions_template: 'mail.ChatComposer.MentionPartnerSuggestions',
}); });
this.mention_manager.register({ this.mention_manager.register({
delimiter: '#', delimiter: '#',
fetch_callback: this.mention_fetch_channels.bind(this), fetch_callback: this.mention_fetch_channels.bind(this),
model: 'mail.channel', model: 'mail.channel',
redirect_classname: 'o_channel_redirect', 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.PartnerModel = new Model('res.partner');
this.ChannelModel = new Model('mail.channel'); this.ChannelModel = new Model('mail.channel');
}, },
@@ -365,8 +358,10 @@ var Composer = Widget.extend({
this.$attachment_button = this.$(".o_composer_button_add_attachment"); this.$attachment_button = this.$(".o_composer_button_add_attachment");
this.$attachments_list = this.$('.o_composer_attachments_list'); 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 = this.$('.o_composer_input');
this.$input.focus(function () {
self.trigger('input_focused');
});
this.resize_input(); this.resize_input();
// Attachments // Attachments
@@ -403,7 +398,7 @@ var Composer = Widget.extend({
return $.when({ return $.when({
content: this.mention_manager.generate_links(value), content: this.mention_manager.generate_links(value),
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'), 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) { mention_fetch_partners: function (search) {
var kwargs = { var self = this;
channel: this.get_channel_info(), return $.when(this.mention_prefetched_partners).then(function (prefetched_partners) {
exclude: listener ? _.pluck(listener.selection, 'id') : [], // filter prefetched partners with the given search string
limit: this.options.mention_fetch_limit, var suggestions = [];
search: search, var limit = self.options.mention_fetch_limit;
}; var search_regexp = new RegExp(self.unaccent(search), 'i');
return this.PartnerModel.call('get_mention_suggestions', kwargs); _.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) { mention_set_prefetched_partners: function (prefetched_partners) {
this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', { this.mention_prefetched_partners = prefetched_partners;
partners: listener.selection,
}));
}, },
// Others // Others
@@ -629,6 +643,12 @@ var Composer = Widget.extend({
focus: function () { focus: function () {
this.$input.focus(); 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; return Composer;
+11 -25
View File
@@ -2,7 +2,6 @@ odoo.define('mail.ChatThread', function (require) {
"use strict"; "use strict";
var core = require('web.core'); var core = require('web.core');
var time = require('web.time');
var Widget = require('web.Widget'); var Widget = require('web.Widget');
var QWeb = core.qweb; var QWeb = core.qweb;
@@ -49,7 +48,6 @@ var Thread = Widget.extend({
display_order: ORDER.ASC, display_order: ORDER.ASC,
display_needactions: true, display_needactions: true,
display_stars: true, display_stars: true,
default_username: _t('Anonymous'),
display_document_link: true, display_document_link: true,
display_avatar: true, display_avatar: true,
squash_close_messages: 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 // one are both comments wrote by the same author at the same minute
var prev_msg; var prev_msg;
_.each(msgs, function (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.message_type !== 'comment' || msg.message_type !== 'comment' ||
(prev_msg.author_id[0] !== msg.author_id[0])) { (prev_msg.author_id[0] !== msg.author_id[0])) {
msg.display_author = true; msg.display_author = true;
@@ -79,7 +77,7 @@ var Thread = Widget.extend({
this.$el.html(QWeb.render('mail.ChatThread', { this.$el.html(QWeb.render('mail.ChatThread', {
messages: msgs, messages: msgs,
options: _.extend({}, this.options, options), options: options,
ORDER: ORDER, ORDER: ORDER,
})); }));
}, },
@@ -113,31 +111,19 @@ var Thread = Widget.extend({
var msg = _.extend({}, message); var msg = _.extend({}, message);
// Set the date in the browser timezone // 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 (date === moment().format('YYYY-MM-DD')) {
if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) { msg.day = _t("Today");
msg.mailto = msg.email_from; 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 { } else {
msg.displayed_author = msg.author_id && msg.author_id[1] || msg.day = msg.date.format('LL');
msg.email_from || msg.hour = msg.date.format('hh:mm');
this.options.default_username;
} }
// Compute the avatar_url
if (msg.author_id && msg.author_id[0]) {
msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
} else if (msg.message_type === 'email') {
msg.avatar_src = "/mail/static/src/img/email_icon.png";
} 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; return msg;
}, },
+1 -11
View File
@@ -128,7 +128,7 @@
.o_mail_chat { .o_mail_chat {
.o_mail_chat_sidebar { .o_mail_chat_sidebar {
background-color: darken(@gray, 8%); background-color: darken(@gray, 8%);
color: lighten(@gray, 50%); color: lighten(@gray, 60%);
opacity: 0.9; opacity: 0.9;
.o_mail_chat_channel_item { .o_mail_chat_channel_item {
@@ -214,16 +214,6 @@
color: white; 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; resize: none;
} }
.o_composer_mentioned_partners, .o_composer_attachments_list { .o_composer_attachments_list {
margin-top: 5px; margin-bottom: 5px;
text-align: justify; text-align: justify;
} }
+2 -2
View File
@@ -6,7 +6,7 @@
--> -->
<t t-name="mail.chatter.ChatComposer" t-extend="mail.ChatComposer"> <t t-name="mail.chatter.ChatComposer" t-extend="mail.ChatComposer">
<!-- Insert information before the composer --> <!-- 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 --> <!-- Internal note composer -->
<t t-if="widget.options.is_log"> <t t-if="widget.options.is_log">
<span class="o_chatter_composer_info"> <span class="o_chatter_composer_info">
@@ -53,7 +53,7 @@
</t> </t>
<!-- Add a button to open the full composer dialog --> <!-- 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"/> <button class="btn btn-sm btn-icon fa fa-pencil-square-o o_composer_button_full_composer" type="button"/>
</t> </t>
</t> </t>
+7 -30
View File
@@ -2,14 +2,13 @@
<templates xml:space="preserve"> <templates xml:space="preserve">
<div t-name="mail.ChatComposer" class="o_chat_composer"> <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_attachments_list"/>
<div class="o_composer"> <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"> <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-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-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>
</div> </div>
@@ -33,16 +32,16 @@
</t> </t>
</t> </t>
<t t-name="mail.ChatComposer.MentionPartnerSuggestions"> <t t-name="mail.ChatComposer.MentionSuggestions">
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu"> <ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
<t t-set="show_divider" t-value="false"/> <t t-set="show_divider" t-value="false"/>
<t t-foreach="suggestions" t-as="group"> <t t-foreach="suggestions" t-as="group">
<t t-foreach="group" t-as="partner"> <t t-foreach="group" t-as="suggestion">
<li t-if="show_divider &amp;&amp; (partner_index === 0)" class="divider"/> <li t-if="show_divider &amp;&amp; (suggestion_index === 0)" class="divider"/>
<t t-set="show_divider" t-value="true"/> <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="#"> <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> </a>
</li> </li>
</t> </t>
@@ -50,26 +49,4 @@
</ul> </ul>
</t> </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> </templates>
+13 -10
View File
@@ -9,9 +9,13 @@
<t t-if="options.display_load_more &amp;&amp; options.display_order == ORDER.DESC" <t t-if="options.display_load_more &amp;&amp; options.display_order == ORDER.DESC"
t-call="mail.ChatThread.LoadMore"/> t-call="mail.ChatThread.LoadMore"/>
</t> </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-call="mail.EmptyChannel" />
</t> </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> </t>
<!-- When a channel is empty--> <!-- When a channel is empty-->
@@ -30,11 +34,11 @@
<t t-if="options.messages_separator_position == 'top'" t-call="mail.MessagesSeparator"/> <t t-if="options.messages_separator_position == 'top'" t-call="mail.MessagesSeparator"/>
<t t-set="current_day" t-value="0"/> <t t-set="current_day" t-value="0"/>
<t t-foreach="messages" t-as="message"> <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"> <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> </span>
<t t-set="current_day" t-value="message.date.split(' ')[0]"/> <t t-set="current_day" t-value="message.day"/>
</div> </div>
<t t-call="mail.ChatThread.Message"/> <t t-call="mail.ChatThread.Message"/>
@@ -56,8 +60,8 @@
t-att-src="message.avatar_src" t-att-src="message.avatar_src"
class="o_thread_message_avatar"/> class="o_thread_message_avatar"/>
</t> </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"> <span t-if="!message.display_author" t-att-title="message.date" class="o_thread_message_side_date">
<t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm')"/> <t t-esc="message.date.format('hh:mm')"/>
</span> </span>
<i t-if="!message.display_author &amp;&amp; options.display_stars &amp;&amp; message.message_type != 'notification'" <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')" 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"/> <t t-esc="message.displayed_author"/>
</strong> </strong>
<small t-att-title="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LLL')"> <small t-att-title="message.date">
- <t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm A')"/> - <t t-esc="message.hour"/>
</small> </small>
<t t-if="message.model &amp;&amp; (message.model != 'mail.channel') &amp;&amp; options.display_document_link"> <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> 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 t-call="mail.ChatThread.MessageTracking"/> <t t-call="mail.ChatThread.MessageTracking"/>
</t> </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.tracking_value_ids &amp;&amp; message.tracking_value_ids.length &gt; 0)">
<t t-if="message.body_short"> <t t-if="message.body_short">
<div class="o_mail_body_short"> <div class="o_mail_body_short">
@@ -115,7 +119,6 @@
</div> </div>
<div class="o_mail_body_long"> <div class="o_mail_body_long">
<t t-raw="message.body"/> <t t-raw="message.body"/>
<span class="oe_mail_expand"><a href="#">... read less</a></span>
</div> </div>
</t> </t>
<t t-if="! message.body_short"> <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', 'version': '1.0',
'author': 'Eezee-It', 'author': 'Eezee-It',
'category': 'Hidden', '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 devices = require('point_of_sale.devices');
var core = require('web.core'); var core = require('web.core');
var Model = require('web.DataModel'); var Model = require('web.DataModel');
var formats = require('web.formats');
var session = require('web.session'); var session = require('web.session');
var time = require('web.time'); var time = require('web.time');
var utils = require('web.utils'); var utils = require('web.utils');
@@ -1070,8 +1071,7 @@ exports.Orderline = Backbone.Model.extend({
} }
this.product = options.product; this.product = options.product;
this.price = options.product.price; this.price = options.product.price;
this.quantity = 1; this.set_quantity(1);
this.quantityStr = '1';
this.discount = 0; this.discount = 0;
this.discountStr = '0'; this.discountStr = '0';
this.type = 'unit'; this.type = 'unit';
@@ -1134,7 +1134,8 @@ exports.Orderline = Backbone.Model.extend({
if(unit){ if(unit){
if (unit.rounding) { if (unit.rounding) {
this.quantity = round_pr(quant, 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 { } else {
this.quantity = round_pr(quant, 1); this.quantity = round_pr(quant, 1);
this.quantityStr = this.quantity.toFixed(0); 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="groups_id" eval="[(4, ref('base.group_portal'))]"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<data> <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="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="product_id"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_delivered_updateable"]' 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): class product_pricelist_item_new(models.Model):
_inherit = "product.pricelist.item" _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.one
@api.depends('categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \ @api.depends('categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \
'pricelist_id', 'percent_price', 'price_discount', 'price_surcharge') '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 #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.") 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.") 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): def _get_image_variant(self, cr, uid, ids, name, args, context=None):
result = dict.fromkeys(ids, False) result = dict.fromkeys(ids, False)
for obj in self.browse(cr, uid, ids, context=context): 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 return result
def _set_image_variant(self, cr, uid, id, name, value, args, context=None): 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) project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
if project and project.partner_id: if project and project.partner_id:
return {'value': {'partner_id': project.partner_id.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): def onchange_user_id(self, cr, uid, ids, user_id, context=None):
vals = {} vals = {}
+1 -1
View File
@@ -296,7 +296,7 @@
<field name="project_id" ref="project.project_project_data"/> <field name="project_id" ref="project.project_project_data"/>
<field name="name">You can set a deadline on a task</field> <field name="name">You can set a deadline on a task</field>
<field name="stage_id" ref="project_stage_data_1"/> <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>
<record id="project_task_data_14" model="project.task"> <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,\ 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") 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") group_id = fields.Many2one('procurement.group', string="Procurement Group")
@api.model @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 self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id
return {} 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 @api.multi
def action_rfq_send(self): def action_rfq_send(self):
''' '''
@@ -853,6 +860,7 @@ class ProcurementOrder(models.Model):
'price_unit': price_unit, 'price_unit': price_unit,
'procurement_ids': [(4, procurement.id)] 'procurement_ids': [(4, procurement.id)]
}) })
break
if not po_line: if not po_line:
vals = procurement._prepare_purchase_order_line(po, supplier) vals = procurement._prepare_purchase_order_line(po, supplier)
self.env['purchase.order.line'].create(vals) self.env['purchase.order.line'].create(vals)
+2
View File
@@ -251,6 +251,8 @@
<group> <group>
<field name="date_planned"/> <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="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"/> <field name="incoterm_id"/>
</group> </group>
<group> <group>
+2 -2
View File
@@ -34,11 +34,11 @@
<label for="id" string="Customer portal"/> <label for="id" string="Customer portal"/>
<div> <div>
<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"/> <label for="module_website_portal"/>
</div> </div>
<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"/> <label for="module_website_sale_digital"/>
</div> </div>
</div> </div>
+3 -1
View File
@@ -485,7 +485,9 @@ class SaleOrderLine(models.Model):
def _get_invoice_qty(self): def _get_invoice_qty(self):
""" """
Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. Note 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: for line in self:
qty_invoiced = 0.0 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_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_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="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>
<group> <group>
<field name="date_order" invisible="1"/> <field name="date_order" invisible="1"/>
@@ -361,7 +361,7 @@
<filter string="My Orders" domain="[('user_id','=',uid)]" name="my_sale_orders_filter"/> <filter string="My Orders" domain="[('user_id','=',uid)]" name="my_sale_orders_filter"/>
<separator/> <separator/>
<filter string="Quotations" name="draft" domain="[('state','in',('draft','sent'))]" help="Sales Order that haven't yet been confirmed"/> <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/> <separator/>
<filter string="To Invoice" domain="[('invoice_status','=','to invoice')]"/> <filter string="To Invoice" domain="[('invoice_status','=','to invoice')]"/>
<filter string="Upselling" domain="[('invoice_status','=','upselling')]"/> <filter string="Upselling" domain="[('invoice_status','=','upselling')]"/>
@@ -47,6 +47,7 @@
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="view_mode">form</field> <field name="view_mode">form</field>
<field name="target">new</field> <field name="target">new</field>
<field name="groups_id" eval="[(4,ref('base.group_sale_salesman'))]"/>
</record> </record>
<!-- TODO: check if we need this --> <!-- 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') sale_layout_cat_id = openerp.fields.Many2one('sale_layout.category', string='Section')
categ_sequence = openerp.fields.Integer(related='sale_layout_cat_id.sequence', categ_sequence = openerp.fields.Integer(related='sale_layout_cat_id.sequence',
string='Layout Sequence', store=True) string='Layout Sequence', store=True)
_defaults = {
'categ_sequence': 0
}
class SaleOrder(osv.Model): class SaleOrder(osv.Model):
_inherit = 'sale.order' _inherit = 'sale.order'
@@ -100,10 +96,6 @@ class SaleOrderLine(osv.Model):
# Store is intentionally set in order to keep the "historic" order. # 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' _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): 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') @api.onchange('product_uom_qty')
def _onchange_product_uom_qty(self): 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 = { warning_mess = {
'title': _('Ordered quantity decreased!'), 'title': _('Ordered quantity decreased!'),
'message' : _('You are decreasing the ordered quantity! Do not forget to manually update the delivery order if needed.'), '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 @api.multi
def _get_delivered_qty(self): 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() self.ensure_one()
super(SaleOrderLine, self)._get_delivered_qty() super(SaleOrderLine, self)._get_delivered_qty()
qty = 0.0 qty = 0.0
for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped): 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": 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) qty += self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom)
return qty 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."), 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"), '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_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), '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"> <group name="destination_grp" string="Destination" groups="stock.group_locations">
<field name="location_dest_id" /> <field name="location_dest_id" />
<field name="partner_id" context="{'contact_display':'partner'}" /> <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"/> <field name="move_dest_id" groups="base.group_no_one" readonly="1"/>
</group> </group>
<group name="quants_grp" string="Reserved Quants" colspan="2" groups="base.group_no_one" attrs="{'invisible': [('state', '=', 'done')]}"> <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) Model = request.session.model(model)
context = dict(request.context or {}, **params.get('context', {})) 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(): if not request.env[model]._is_an_ordinary_table():
fields = [field for field in fields if field['name'] != 'id'] fields = [field for field in fields if field['name'] != 'id']
@@ -992,8 +992,8 @@ var SelectCreateDialog = ViewDialog.extend({
this.searchview.destroy(); this.searchview.destroy();
} }
var $header = $('<div/>').addClass('o_modal_header').appendTo(this.$el); 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 $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 = new SearchView(this, this.dataset, false, search_defaults, {$buttons: $buttons});
this.searchview.on('search_data', self, function(domains, contexts, groupbys) { this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
if (self.initial_ids) { if (self.initial_ids) {
+1 -1
View File
@@ -1177,7 +1177,7 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{
ids = value; ids = value;
} }
new Model(column.relation) 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 // FIXME: nth horrible hack in this poor listview
record.set(column.id + '__display', record.set(column.id + '__display',
_(names).pluck(1).join(', ')); _(names).pluck(1).join(', '));
+1
View File
@@ -10,6 +10,7 @@
.o_modal_header { .o_modal_header {
.o-webclient-padding(@top: 10px, @bottom: 10px); .o-webclient-padding(@top: 10px, @bottom: 10px);
.clearfix();
.o_search_options { .o_search_options {
display: inline-block; display: inline-block;
+1 -1
View File
@@ -122,8 +122,8 @@ var FieldTextHtmlSimple = widget.extend({
if (this.options['style-inline']) { if (this.options['style-inline']) {
transcoder.class_to_style(this.$content); transcoder.class_to_style(this.$content);
transcoder.font_to_img(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 () { destroy_content: function () {
$(".oe-view-manager-content").off("scroll"); $(".oe-view-manager-content").off("scroll");
@@ -2320,6 +2320,12 @@ function summernote_paste (event) {
var html = clipboardData.getData("text/html"); var html = clipboardData.getData("text/html");
var $node = $('<div/>').html(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 remove undesirable tag
@@ -25,6 +25,7 @@
<div class="text-center o_web_settings_dashboard_apps"> <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> <i class="fa fa-cog fa-4x text-muted o_browse_apps" style="cursor: pointer;"></i>
<div class="o_web_settings_dashboard_header"> <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-if="installed_apps">
<t t-esc="installed_apps"></t> <t t-esc="installed_apps"></t>
<t t-if="installed_apps == 1">Installed App</t> <t t-if="installed_apps == 1">Installed App</t>
@@ -1032,7 +1032,7 @@ var ViewEditor = Widget.extend({
render_new_field :function( result ) { render_new_field :function( result ) {
var self = this; var self = this;
var action = { 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", res_model: "ir.model.fields",
views: [[false, 'form']], views: [[false, 'form']],
type: 'ir.actions.act_window', type: 'ir.actions.act_window',
+2 -2
View File
@@ -38,9 +38,9 @@ class Website(openerp.addons.web.controllers.main.Home):
else: else:
first_menu = main_menu.child_id and main_menu.child_id[0] first_menu = main_menu.child_id and main_menu.child_id[0]
if first_menu: 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) 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 request.registry['ir.http'].reroute(first_menu.url)
return self.page(page) return self.page(page)
+1 -1
View File
@@ -6,7 +6,7 @@
<template id="assets_common" inherit_id="web.assets_common" name="Website assets"> <template id="assets_common" inherit_id="web.assets_common" name="Website assets">
<xpath expr="." position="inside"> <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> </xpath>
</template> </template>
+14 -2
View File
@@ -1,12 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <openerp>
<data noupdate="1"> <data>
<record id="crm.model_crm_lead" model="ir.model"> <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_default_field_id" ref="crm.field_crm_lead_description" />
<field name="website_form_access">True</field> <field name="website_form_access">True</field>
<field name="website_form_label">Create a lead</field> <field name="website_form_label">Create a lead</field>
</record> </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"> <record id="action_open_website" model="ir.actions.act_url">
<field name="name">Website Contact Form</field> <field name="name">Website Contact Form</field>
<field name="target">self</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) @http.route('/website_form/<string:model_name>', type='http', auth="public", methods=['POST'], website=True)
def website_form(self, model_name, **kwargs): def website_form(self, model_name, **kwargs):
model_record = request.env['ir.model'].search([('model', '=', model_name), ('website_form_access', '=', True)]) model_record = request.env['ir.model'].search([('model', '=', model_name), ('website_form_access', '=', True)])
if not model_record: if not model_record:
return json.dumps(False) return json.dumps(False)
try: try:
data = self.extract_data(model_record, ** kwargs) data = self.extract_data(model_record, ** kwargs)
# If we encounter an issue while extracting data # If we encounter an issue while extracting data
except ValidationError, e: except ValidationError, e:
# I couldn't find a cleaner way to pass data to an exception # 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 'custom': '', # Custom fields values
} }
authorized_fields = model.sudo().get_authorized_fields(); authorized_fields = model.sudo()._get_form_writable_fields()
error_fields = [] error_fields = []
for field_name, field_value in kwargs.items(): for field_name, field_value in kwargs.items():
@@ -183,7 +181,7 @@ class WebsiteForm(http.Controller):
def insert_attachment(self, model, id_record, files): def insert_attachment(self, model, id_record, files):
orphan_attachment_ids = [] orphan_attachment_ids = []
record = model.env[model.model].browse(id_record) 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: for file in files:
custom_field = file.field_name not in authorized_fields custom_field = file.field_name not in authorized_fields
attachment_value = { attachment_value = {
+66 -6
View File
@@ -1,3 +1,5 @@
import itertools
from openerp import tools from openerp import tools
from openerp import models, fields, api 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_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'.") 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): def _get_form_writable_fields(self):
return [self.id] + [m.all_inherited_model_ids() for m in self.inherited_model_ids] """
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 @api.multi
def get_authorized_fields(self): def get_authorized_fields(self):
model = self.env[self.model] model = self.env[self.model]
fields_get = model.fields_get() 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(): for key, val in model._inherits.iteritems():
fields_get.pop(val,None) fields_get.pop(val,None)
@@ -49,4 +69,44 @@ class website_form_model_fields(models.Model):
_name = 'ir.model.fields' _name = 'ir.model.fields'
_inherit = '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): def post_save(self, forum, post, **kwargs):
if 'post_name' in kwargs and not kwargs.get('post_name').strip(): 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.')}) 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 = { vals = {
'tag_ids': post_tags, 'tag_ids': post_tags,
'name': kwargs.get('post_name'), 'name': kwargs.get('post_name'),
+12 -11
View File
@@ -4,10 +4,10 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 9.0\n" "Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-10-09 09:20+0000\n" "POT-Creation-Date: 2015-11-26 14:10+0000\n"
"PO-Revision-Date: 2015-10-09 09:20+0000\n" "PO-Revision-Date: 2015-11-26 14:10+0000\n"
"Last-Translator: <>\n" "Last-Translator: <>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -222,13 +222,19 @@ msgid "<i class=\"text-muted\">awarded users</i>"
msgstr "" msgstr ""
#. module: website_forum #. module: website_forum
#: code:addons/website_forum/models/forum.py:419 #: code:addons/website_forum/models/forum.py:483
#, python-format #, 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>" 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 "" msgstr ""
#. module: website_forum #. 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 #, 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>" 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 "" msgstr ""
@@ -1621,11 +1627,6 @@ msgstr ""
msgid "Last activity date" msgid "Last activity date"
msgstr "" 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 #. module: website_forum
#: model:ir.ui.view,arch_db:website_forum.header #: model:ir.ui.view,arch_db:website_forum.header
msgid "Last updated:" msgid "Last updated:"
@@ -2058,7 +2059,7 @@ msgstr ""
#. module: website_forum #. module: website_forum
#: model:ir.ui.view,arch_db:website_forum.header_footer_custom #: model:ir.ui.view,arch_db:website_forum.header_footer_custom
msgid "Q&amp;A" msgid "Q&amp;amp;A"
msgstr "" msgstr ""
#. module: website_forum #. module: website_forum
+1 -1
View File
@@ -25,7 +25,7 @@
<template id="header_footer_custom" inherit_id="website.footer_default" <template id="header_footer_custom" inherit_id="website.footer_default"
name="Footer Questions Link"> name="Footer Questions Link">
<xpath expr="//ul[@id='products']" position="inside"> <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> <li><a href="/forum">Forums</a></li>
</xpath> </xpath>
</template> </template>
@@ -16,10 +16,23 @@
<field name="action_id" ref="action_open_website"/> <field name="action_id" ref="action_open_website"/>
<field name="state">open</field> <field name="state">open</field>
</record> </record>
</data>
<data>
<record id="hr_recruitment.model_hr_applicant" model="ir.model"> <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_default_field_id" ref="hr_recruitment.field_hr_applicant_description" />
<field name="website_form_access">True</field> <field name="website_form_access">True</field>
<field name="website_form_label">Apply for a Job</field> <field name="website_form_label">Apply for a Job</field>
</record> </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> </data>
</openerp> </openerp>
@@ -1,15 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <openerp>
<data noupdate="1"> <data>
<record id="project_issue.model_project_issue" model="ir.model"> <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_default_field_id" ref="project_issue.field_project_issue_description" />
<field name="website_form_access">True</field> <field name="website_form_access">True</field>
<field name="website_form_label">Create an issue</field> <field name="website_form_label">Create an issue</field>
</record> </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> </data>
</openerp> </openerp>
@@ -68,9 +68,9 @@ options.registry.newsletter_popup = mass_mailing_common.extend({
}); });
web_editor.Class.include({ web_editor.Class.include({
edit: function () { start: function () {
this._super();
$('body').on('click','#edit_dialog',_.bind(this.edit_dialog, this.rte.editor)); $('body').on('click','#edit_dialog',_.bind(this.edit_dialog, this.rte.editor));
return this._super();
}, },
save : function() { save : function() {
var $target = $('#wrapwrap').find('#o_newsletter_popup'); var $target = $('#wrapwrap').find('#o_newsletter_popup');
@@ -53,7 +53,7 @@ if(!$('.o_website_quote').length) {
'submit #accept': 'submitForm', 'submit #accept': 'submitForm',
}, },
initSignature: function(ev){ 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'); this.empty_sign = this.$("#signature").jSignature("getData",'image');
}, },
clearSignature: function(ev){ clearSignature: function(ev){
@@ -146,9 +146,9 @@ $('.oe_website_sale').each(function () {
clickwatch(function(){ clickwatch(function(){
$dom_optional.each(function(){ $dom_optional.each(function(){
$(this).find('.js_quantity').text(value);
product_ids.push($(this).find('span[data-product-id]').data('product-id')); product_ids.push($(this).find('span[data-product-id]').data('product-id'));
}); });
if (isNaN(value)) value = 0;
$input.data('update_change', true); $input.data('update_change', true);
ajax.jsonRpc("/shop/cart/update_json", 'call', { ajax.jsonRpc("/shop/cart/update_json", 'call', {
@@ -31,4 +31,9 @@
</t> </t>
</xpath> </xpath>
</template> </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> </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 #. module: base
#: model:ir.module.module,shortdesc:base.module_payment_sips #: model:ir.module.module,shortdesc:base.module_payment_sips
msgid "Worldline SIPS Payment Acquiring for online payments" msgid "Worldline SIPS"
msgstr "" msgstr ""
#. module: base #. module: base
+5 -11
View File
@@ -93,7 +93,7 @@ class ir_model(osv.osv):
_defaults = { _defaults = {
'model': 'x_', '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): 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): def create(self, cr, user, vals, context=None):
if context is None: if context is None:
context = {} context = {}
if context and context.get('manual'):
vals['state']='manual'
res = super(ir_model,self).create(cr, user, vals, context) 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 # setup models; this automatically adds model in registry
self.pool.setup_models(cr, partial=(not self.pool.ready)) self.pool.setup_models(cr, partial=(not self.pool.ready))
# update database schema # update database schema
@@ -264,7 +262,7 @@ class ir_model_fields(osv.osv):
'selection': "", 'selection': "",
'domain': "[]", 'domain': "[]",
'name': 'x_', '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', 'on_delete': 'set null',
'field_description': '', 'field_description': '',
'selectable': 1, 'selectable': 1,
@@ -444,14 +442,12 @@ class ir_model_fields(osv.osv):
vals['model'] = model_data.model vals['model'] = model_data.model
if context is None: if context is None:
context = {} context = {}
if context and context.get('manual',False):
vals['state'] = 'manual'
if vals.get('ttype', False) == 'selection': if vals.get('ttype', False) == 'selection':
if not vals.get('selection',False): if not vals.get('selection',False):
raise UserError(_('For selection fields, the Selection Options must be given!')) raise UserError(_('For selection fields, the Selection Options must be given!'))
self._check_selection(cr, user, vals['selection'], context=context) self._check_selection(cr, user, vals['selection'], context=context)
res = super(ir_model_fields,self).create(cr, user, vals, 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_'): if not vals['name'].startswith('x_'):
raise UserError(_("Custom fields must have a name that starts with '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): def write(self, cr, user, ids, vals, context=None):
if context is None: if context is None:
context = {} 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 #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: 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!')) raise UserError(_('Can only rename one field at a time!'))
if vals['name'] in obj._fields: if vals['name'] in obj._fields:
raise UserError(_('Cannot rename field to %s, because that field already exists!') % vals['name']) 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!')) 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']: if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
raise ValueError('Invalid character in column name') raise ValueError('Invalid character in column name')
+3 -3
View File
@@ -41,7 +41,7 @@
</group> </group>
<notebook> <notebook>
<page string="Fields"> <page string="Fields">
<field context="{'manual':True}" name="field_id"> <field name="field_id">
<tree string="Fields Description"> <tree string="Fields Description">
<field name="name"/> <field name="name"/>
<field name="field_description"/> <field name="field_description"/>
@@ -203,7 +203,7 @@
<field name="name">Models</field> <field name="name">Models</field>
<field name="res_model">ir.model</field> <field name="res_model">ir.model</field>
<field name="view_type">form</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"/> <field name="view_id" ref="view_model_tree"/>
</record> </record>
<menuitem action="action_model_model" id="ir_model_model_menu" parent="next_id_9"/> <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="name">Fields</field>
<field name="res_model">ir.model.fields</field> <field name="res_model">ir.model.fields</field>
<field name="view_type">form</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"/> <field name="view_id" ref="view_model_fields_tree"/>
</record> </record>
<menuitem action="action_model_fields" id="ir_model_model_fields" parent="base.next_id_9"/> <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.type = ti.type
AND irt.module = ti.module AND irt.module = ti.module
AND irt.name = ti.name AND irt.name = ti.name
AND (ti.type IN ('field', 'help') OR irt.src = ti.src) AND (
AND ( ti.type NOT IN ('model', 'view') (ti.type = 'model' AND ti.res_id = irt.res_id AND irt.src = ti.src)
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 irt.src = ti.src)
OR (ti.type = 'view' AND (irt.res_id IS NULL OR ti.res_id = irt.res_id)) 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 # Step 2: update existing (matching) translations
@@ -536,41 +538,46 @@ class ir_translation(osv.osv):
def insert_missing(self, field, records): def insert_missing(self, field, records):
""" Insert missing translations for `field` on `records`. """ """ Insert missing translations for `field` on `records`. """
records = records.with_context(lang=None) records = records.with_context(lang=None)
external_ids = records.get_external_id() # if no xml_id, empty string
if callable(field.translate): if callable(field.translate):
# insert missing translations for each term in src # insert missing translations for each term in src
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value) 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 SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s, %(module)s
FROM res_lang l FROM res_lang l
WHERE l.active AND NOT EXISTS ( WHERE l.active AND NOT EXISTS (
SELECT 1 FROM ir_translation 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: for record in records:
module = external_ids[record.id].split('.')[0]
src = record[field.name] or None src = record[field.name] or None
for term in set(field.get_trans_terms(src)): for term in set(field.get_trans_terms(src)):
self._cr.execute(query, { self._cr.execute(query, {
'name': "%s,%s" % (field.model_name, field.name), 'name': "%s,%s" % (field.model_name, field.name),
'res_id': record.id, 'res_id': record.id,
'src': term, 'src': term,
'module': module
}) })
else: else:
# insert missing translations for src # insert missing translations for src
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value) 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 SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s, %(module)s
FROM res_lang l FROM res_lang l
WHERE l.active AND l.code != 'en_US' AND NOT EXISTS ( WHERE l.active AND l.code != 'en_US' AND NOT EXISTS (
SELECT 1 FROM ir_translation 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 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: for record in records:
module = external_ids[record.id].split('.')[0]
self._cr.execute(query, { self._cr.execute(query, {
'name': "%s,%s" % (field.model_name, field.name), 'name': "%s,%s" % (field.model_name, field.name),
'res_id': record.id, 'res_id': record.id,
'src': record[field.name] or None, 'src': record[field.name] or None,
'module': module
}) })
self.clear_caches() 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"), '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), 'company_id': fields.many2one('res.company', 'Company', select=1),
'color': fields.integer('Color Index'), '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'), 'contact_address': fields.function(_address_display, type='char', string='Complete Address'),
# technical field used for managing commercial fields # technical field used for managing commercial fields
+1 -1
View File
@@ -119,7 +119,7 @@ class TranslationToolsTestCase(unittest.TestCase):
terms = [] terms = []
source = """<div> source = """<div>
stuff before stuff before
<div translation="off">Do not translate this</div> <div t-translation="off">Do not translate this</div>
stuff after stuff after
</div>""" </div>"""
result = xml_translate(terms.append, source) 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') user = config['db_user'] or os.environ.get('PGUSER', 'default')
_logger.info('database: %s@%s:%s', user, host, port) _logger.info('database: %s@%s:%s', user, host, port)
def rm_pid_file(): def rm_pid_file(main_pid):
config = openerp.tools.config config = openerp.tools.config
if not openerp.evented and config['pidfile']: if config['pidfile'] and main_pid == os.getpid():
try: try:
os.unlink(config['pidfile']) os.unlink(config['pidfile'])
except OSError: except OSError:
@@ -76,10 +76,10 @@ def setup_pid_file():
""" """
config = openerp.tools.config config = openerp.tools.config
if not openerp.evented and config['pidfile']: if not openerp.evented and config['pidfile']:
pid = os.getpid()
with open(config['pidfile'], 'w') as fd: with open(config['pidfile'], 'w') as fd:
pidtext = "%d" % (os.getpid()) fd.write(str(pid))
fd.write(pidtext) atexit.register(rm_pid_file, pid)
atexit.register(rm_pid_file)
def export_translation(): def export_translation():
config = openerp.tools.config 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. # case, the request cursor is unusable. Rollback transaction to create a new one.
if self._cr: if self._cr:
self._cr.rollback() self._cr.rollback()
self.env.clear()
result = self.endpoint(*a, **kw) result = self.endpoint(*a, **kw)
if isinstance(result, Response) and result.is_qweb: if isinstance(result, Response) and result.is_qweb:
# Early rendering of lazy responses to benefit from @service_model.check protection # Early rendering of lazy responses to benefit from @service_model.check protection
+1 -1
View File
@@ -215,7 +215,7 @@ class XMLTranslator(object):
if ( if (
isinstance(node, SKIPPED_ELEMENT_TYPES) or isinstance(node, SKIPPED_ELEMENT_TYPES) or
node.tag in SKIPPED_ELEMENTS 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 node.tag == "attribute" and node.get("name") not in TRANSLATED_ATTRS
): ):
# do not translate the contents of the node # do not translate the contents of the node