[MERGE] forward port of branch 9.0 up to 00ee6e7b
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
reference_type: none
|
||||
payment_term_id: account.account_payment_term
|
||||
type: 'out_invoice'
|
||||
date_invoice: !eval time.strftime('%Y')+'-01-01'
|
||||
date_invoice: !eval time.strftime('%Y-%m')+'-01'
|
||||
invoice_line_ids:
|
||||
- name: 'Dell Inspiron Laptop without OS'
|
||||
product_id: product.product_product_49
|
||||
@@ -27,7 +27,7 @@
|
||||
partner_id: base.res_partner_2
|
||||
currency_id: base.EUR
|
||||
type: 'out_invoice'
|
||||
date_invoice: !eval time.strftime('%Y')+'-01-08'
|
||||
date_invoice: !eval time.strftime('%Y-%m')+'-08'
|
||||
invoice_line_ids:
|
||||
- name: '8-port Switch'
|
||||
product_id: product.product_product_51
|
||||
@@ -94,7 +94,7 @@
|
||||
reference_type: none
|
||||
payment_term_id: account.account_payment_term
|
||||
type: 'in_invoice'
|
||||
date_invoice: !eval time.strftime('%Y')+'-01-01'
|
||||
date_invoice: !eval time.strftime('%Y-%m')+'-01'
|
||||
invoice_line_ids:
|
||||
- name: 'Toner Cartridge'
|
||||
price_unit: 10.0
|
||||
@@ -112,7 +112,7 @@
|
||||
partner_id: base.res_partner_12
|
||||
currency_id: base.EUR
|
||||
type: 'in_invoice'
|
||||
date_invoice: !eval time.strftime('%Y')+'-01-01'
|
||||
date_invoice: !eval time.strftime('%Y-%m')+'-01'
|
||||
-
|
||||
!python {model: ir.model.data} : |
|
||||
line_vals = {
|
||||
@@ -133,7 +133,7 @@
|
||||
partner_id: base.res_partner_1
|
||||
currency_id: base.EUR
|
||||
type: 'in_invoice'
|
||||
date_invoice: !eval time.strftime('%Y')+'-01-15'
|
||||
date_invoice: !eval time.strftime('%Y-%m')+'-15'
|
||||
-
|
||||
!python {model: ir.model.data} : |
|
||||
line_vals = {
|
||||
|
||||
@@ -116,16 +116,21 @@ class AccountInvoice(models.Model):
|
||||
if len(lines) != 0:
|
||||
for line in lines:
|
||||
# get the outstanding residual value in invoice currency
|
||||
# get the outstanding residual value in its currency. We don't want to show it
|
||||
# in the invoice currency since the exchange rate between the invoice date and
|
||||
# the payment date might have changed.
|
||||
if line.currency_id:
|
||||
amount_to_show = line.currency_id.compute(abs(line.amount_residual_currency), self.currency_id)
|
||||
currency_id = line.currency_id
|
||||
amount_to_show = abs(line.amount_residual_currency)
|
||||
else:
|
||||
amount_to_show = line.company_id.currency_id.compute(abs(line.amount_residual), self.currency_id)
|
||||
currency_id = line.company_id.currency_id
|
||||
amount_to_show = abs(line.amount_residual)
|
||||
info['content'].append({
|
||||
'journal_name': line.ref or line.move_id.name,
|
||||
'amount': amount_to_show,
|
||||
'currency': self.currency_id.symbol,
|
||||
'currency': currency_id.symbol,
|
||||
'id': line.id,
|
||||
'position': self.currency_id.position,
|
||||
'position': currency_id.position,
|
||||
'digits': [69, self.currency_id.decimal_places],
|
||||
})
|
||||
info['title'] = type_payment
|
||||
@@ -148,7 +153,9 @@ class AccountInvoice(models.Model):
|
||||
elif self.type in ('in_invoice', 'out_refund'):
|
||||
amount = sum([p.amount for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids])
|
||||
amount_currency = sum([p.amount_currency for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids])
|
||||
# get the payment value in invoice currency
|
||||
# Get the payment value in its currency. We don't want to show it in the invoice
|
||||
# currency since the exchange rate between the invoice date and the payment date
|
||||
# might have changed.
|
||||
if payment.currency_id and amount_currency != 0:
|
||||
currency_id = payment.currency_id
|
||||
amount_to_show = -amount_currency
|
||||
|
||||
@@ -232,7 +232,7 @@ class ResPartner(models.Model):
|
||||
def _credit_debit_get(self):
|
||||
tables, where_clause, where_params = self.env['account.move.line']._query_get()
|
||||
where_params = [tuple(self.ids)] + where_params
|
||||
self._cr.execute("""SELECT l.partner_id, act.type, SUM(l.debit-l.credit)
|
||||
self._cr.execute("""SELECT l.partner_id, act.type, SUM(l.amount_residual)
|
||||
FROM account_move_line l
|
||||
LEFT JOIN account_account a ON (l.account_id=a.id)
|
||||
LEFT JOIN account_account_type act ON (a.user_type_id=act.id)
|
||||
|
||||
@@ -1874,6 +1874,7 @@ var bankStatementReconciliationLine = abstractReconciliationLine.extend({
|
||||
var propose_partial = self.getCreatedLines().length === 0
|
||||
&& balance_type === "greater"
|
||||
&& Math.abs(self.get("balance")) < Math.abs(last_line.debit - last_line.credit)
|
||||
&& self.get("balance") * (last_line.debit - last_line.credit) < 0
|
||||
&& ! last_line.partial_reconcile
|
||||
&& ! last_line.already_paid;
|
||||
last_line.propose_partial_reconcile = propose_partial;
|
||||
|
||||
@@ -131,3 +131,17 @@ class product_template(osv.Model):
|
||||
# Remove context so it is not going to filter on product_id with active_id of template
|
||||
result['context'] = "{}"
|
||||
return result
|
||||
|
||||
|
||||
class stock_move(osv.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
|
||||
# It will set the default analtyic account on the invoice line
|
||||
partner_id = self.pool['account.invoice'].browse(cr, uid, invoice_line_vals.get('invoice_id'), context=context).partner_id.id
|
||||
if 'account_analytic_id' not in invoice_line_vals or not invoice_line_vals.get('account_analytic_id'):
|
||||
rec = self.pool['account.analytic.default'].account_get(cr, uid, move.product_id.id, partner_id, uid, time.strftime('%Y-%m-%d'), company_id=move.company_id.id, context=context)
|
||||
if rec:
|
||||
invoice_line_vals.update({'account_analytic_id': rec.analytic_id.id})
|
||||
res = super(stock_move, self)._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context)
|
||||
return res
|
||||
|
||||
@@ -129,7 +129,7 @@ class AccountBankStatementImport(models.TransientModel):
|
||||
if currency_code:
|
||||
currency = self.env['res.currency'].search([('name', '=ilike', currency_code)], limit=1)
|
||||
if not currency:
|
||||
raise osv.except_osv(_("No currency found matching '%s'.") % currency_code)
|
||||
raise UserError(_("No currency found matching '%s'.") % currency_code)
|
||||
if currency == company_currency:
|
||||
currency = False
|
||||
|
||||
@@ -148,10 +148,10 @@ class AccountBankStatementImport(models.TransientModel):
|
||||
# If importing into an existing journal, its currency must be the same as the bank statement
|
||||
if journal:
|
||||
journal_currency = journal.currency_id
|
||||
if currency == None:
|
||||
if currency is None:
|
||||
currency = journal_currency
|
||||
if currency and currency != journal_currency:
|
||||
statement_cur_code = currency == False and company_currency.name or currency.name
|
||||
statement_cur_code = not currency and company_currency.name or currency.name
|
||||
journal_cur_code = not journal_currency and company_currency.name or journal_currency.name
|
||||
raise UserError(_('The currency of the bank statement (%s) is not the same as the currency of the journal (%s) !') % (statement_cur_code, journal_cur_code))
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
'version': '1.1',
|
||||
'website' : 'https://www.odoo.com/page/accounting',
|
||||
'category': 'Hidden/Dependency',
|
||||
'depends' : ['base', 'decimal_precision', 'mail'],
|
||||
'depends' : ['base', 'decimal_precision', 'mail', 'report'],
|
||||
'description': """
|
||||
Module for defining analytic accounting object.
|
||||
===============================================
|
||||
|
||||
@@ -265,8 +265,7 @@ class res_users(osv.Model):
|
||||
|
||||
res_partner.signup_prepare(cr, uid, partner_ids, signup_type="reset", expiration=expiration, context=context)
|
||||
|
||||
if not context:
|
||||
context = {}
|
||||
context = dict(context or {})
|
||||
|
||||
# send email to users with their signup url
|
||||
template = False
|
||||
@@ -283,6 +282,7 @@ class res_users(osv.Model):
|
||||
for user in self.browse(cr, uid, ids, context):
|
||||
if not user.email:
|
||||
raise UserError(_("Cannot send email: user %s has no email address.") % user.name)
|
||||
context['lang'] = user.lang
|
||||
self.pool.get('mail.template').send_mail(cr, uid, template.id, user.id, force_send=True, raise_exception=True, context=context)
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
|
||||
@@ -104,11 +104,11 @@ class base_action_rule(osv.osv):
|
||||
def onchange_kind(self, cr, uid, ids, kind, context=None):
|
||||
clear_fields = []
|
||||
if kind in ['on_create', 'on_create_or_write', 'on_unlink']:
|
||||
clear_fields = ['filter_pre_id', 'trg_date_id', 'trg_date_range', 'trg_date_range_type']
|
||||
clear_fields = ['filter_pre_id', 'filter_pre_domain', 'trg_date_id', 'trg_date_range', 'trg_date_range_type']
|
||||
elif kind in ['on_write', 'on_create_or_write']:
|
||||
clear_fields = ['trg_date_id', 'trg_date_range', 'trg_date_range_type']
|
||||
elif kind == 'on_time':
|
||||
clear_fields = ['filter_pre_id']
|
||||
clear_fields = ['filter_pre_id', 'filter_pre_domain']
|
||||
return {'value': dict.fromkeys(clear_fields, False)}
|
||||
|
||||
def onchange_filter_pre_id(self, cr, uid, ids, filter_pre_id, context=None):
|
||||
@@ -119,57 +119,88 @@ class base_action_rule(osv.osv):
|
||||
ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context)
|
||||
return {'value': {'filter_domain': ir_filter.domain}}
|
||||
|
||||
def _get_eval_context(self, cr, uid, context=None):
|
||||
@openerp.api.model
|
||||
def _get_actions(self, records, kinds):
|
||||
""" Return the actions of the given kinds for records' model. The
|
||||
returned actions' context contain an object to manage processing.
|
||||
"""
|
||||
if '__action_done' not in self._context:
|
||||
self = self.with_context(__action_done={})
|
||||
domain = [('model', '=', records._name), ('kind', 'in', kinds)]
|
||||
actions = self.with_context(active_test=True).search(domain)
|
||||
return actions.with_env(self.env)
|
||||
|
||||
@openerp.api.model
|
||||
def _get_eval_context(self):
|
||||
""" Prepare the context used when evaluating python code
|
||||
:returns: dict -- evaluation context given to (safe_)eval """
|
||||
return {
|
||||
'datetime': DT,
|
||||
'dateutil': dateutil,
|
||||
'time': time,
|
||||
'uid': uid,
|
||||
'user': self.pool['res.users'].browse(cr, uid, uid, context=context),
|
||||
'uid': self.env.uid,
|
||||
'user': self.env.user,
|
||||
}
|
||||
|
||||
def _filter(self, cr, uid, action, action_filter, record_ids, domain=False, context=None):
|
||||
""" Filter the list record_ids that satisfy the domain or the action filter. """
|
||||
if record_ids and (domain is not False or action_filter):
|
||||
eval_context = self._get_eval_context(cr, uid, context=context)
|
||||
if domain is not False:
|
||||
new_domain = [('id', 'in', record_ids)] + eval(domain, eval_context)
|
||||
ctx = context
|
||||
elif action_filter:
|
||||
assert action.model == action_filter.model_id, "Filter model different from action rule model"
|
||||
new_domain = [('id', 'in', record_ids)] + eval(action_filter.domain, eval_context)
|
||||
ctx = dict(context or {})
|
||||
ctx.update(eval(action_filter.context))
|
||||
record_ids = self.pool[action.model].search(cr, uid, new_domain, context=ctx)
|
||||
return record_ids
|
||||
@openerp.api.model
|
||||
def _filter_pre(self, records):
|
||||
""" Filter the records that satisfy the precondition of action ``self``. """
|
||||
if self.filter_pre_id and records:
|
||||
eval_context = self._get_eval_context()
|
||||
domain = [('id', 'in', records.ids)] + eval(self.filter_pre_id.domain, eval_context)
|
||||
ctx = eval(self.filter_pre_id.context)
|
||||
return records.with_context(**ctx).search(domain).with_env(records.env)
|
||||
elif self.filter_pre_domain and records:
|
||||
eval_context = self._get_eval_context()
|
||||
domain = [('id', 'in', records.ids)] + eval(self.filter_pre_domain, eval_context)
|
||||
return records.search(domain)
|
||||
else:
|
||||
return records
|
||||
|
||||
@openerp.api.model
|
||||
def _filter_post(self, records):
|
||||
""" Filter the records that satisfy the postcondition of action ``self``. """
|
||||
if self.filter_id and records:
|
||||
eval_context = self._get_eval_context()
|
||||
domain = [('id', 'in', records.ids)] + eval(self.filter_id.domain, eval_context)
|
||||
ctx = eval(self.filter_id.context)
|
||||
return records.with_context(**ctx).search(domain).with_env(records.env)
|
||||
elif self.filter_domain and records:
|
||||
eval_context = self._get_eval_context()
|
||||
domain = [('id', 'in', records.ids)] + eval(self.filter_domain, eval_context)
|
||||
return records.search(domain)
|
||||
else:
|
||||
return records
|
||||
|
||||
@openerp.api.multi
|
||||
def _process(self, records):
|
||||
""" Process action ``self`` on the ``records`` that have not been done yet. """
|
||||
# filter out the records on which self has already been done, then mark
|
||||
# remaining records as done (to avoid recursive processing)
|
||||
action_done = self._context['__action_done']
|
||||
records -= action_done.setdefault(self, records.browse())
|
||||
if not records:
|
||||
return
|
||||
action_done[self] |= records
|
||||
|
||||
def _process(self, cr, uid, action, record_ids, context=None):
|
||||
""" process the given action on the records """
|
||||
model = self.pool[action.model_id.model]
|
||||
# modify records
|
||||
values = {}
|
||||
if 'date_action_last' in model._fields:
|
||||
values['date_action_last'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
if action.act_user_id and 'user_id' in model._fields:
|
||||
values['user_id'] = action.act_user_id.id
|
||||
if 'date_action_last' in records._fields:
|
||||
values['date_action_last'] = openerp.fields.Datetime.now()
|
||||
if self.act_user_id and 'user_id' in records._fields:
|
||||
values['user_id'] = self.act_user_id.id
|
||||
if values:
|
||||
model.write(cr, uid, record_ids, values, context=context)
|
||||
records.write(values)
|
||||
|
||||
if action.act_followers and hasattr(model, 'message_subscribe'):
|
||||
follower_ids = map(int, action.act_followers)
|
||||
model.message_subscribe(cr, uid, record_ids, follower_ids, context=context)
|
||||
# subscribe followers
|
||||
if self.act_followers and hasattr(records, 'message_subscribe'):
|
||||
records.message_subscribe(self.act_followers.ids)
|
||||
|
||||
# execute server actions
|
||||
if action.server_action_ids:
|
||||
server_action_ids = map(int, action.server_action_ids)
|
||||
for record in model.browse(cr, uid, record_ids, context):
|
||||
action_server_obj = self.pool.get('ir.actions.server')
|
||||
ctx = dict(context, active_model=model._name, active_ids=[record.id], active_id=record.id)
|
||||
action_server_obj.run(cr, uid, server_action_ids, context=ctx)
|
||||
|
||||
return True
|
||||
if self.server_action_ids:
|
||||
for record in records:
|
||||
ctx = {'active_model': record._name, 'active_ids': record.ids, 'active_id': record.id}
|
||||
self.server_action_ids.with_context(**ctx).run()
|
||||
|
||||
def _register_hook(self, cr):
|
||||
""" Patch models that should trigger action rules based on creation,
|
||||
@@ -186,26 +217,19 @@ class base_action_rule(osv.osv):
|
||||
|
||||
def make_create():
|
||||
""" Instanciate a create method that processes action rules. """
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
# avoid loops or cascading actions
|
||||
if context and context.get('action'):
|
||||
return create.origin(self, cr, uid, vals, context=context)
|
||||
@openerp.api.model
|
||||
def create(self, vals):
|
||||
# retrieve the action rules to possibly execute
|
||||
actions = self.env['base.action.rule']._get_actions(self, ['on_create', 'on_create_or_write'])
|
||||
|
||||
# call original method with a modified context
|
||||
context = dict(context or {}, action=True)
|
||||
new_id = create.origin(self, cr, uid, vals, context=context)
|
||||
|
||||
# as it is a new record, we do not consider the actions that have a prefilter
|
||||
action_model = self.pool.get('base.action.rule')
|
||||
action_dom = [('model', '=', self._name),
|
||||
('kind', 'in', ['on_create', 'on_create_or_write'])]
|
||||
action_ids = action_model.search(cr, uid, action_dom, context=dict(context, active_test=True))
|
||||
# call original method
|
||||
record = create.origin(self.with_env(actions.env), vals)
|
||||
|
||||
# check postconditions, and execute actions on the records that satisfy them
|
||||
for action in action_model.browse(cr, uid, action_ids, context=context):
|
||||
if action_model._filter(cr, uid, action, action.filter_id, [new_id], domain=action.filter_domain, context=context):
|
||||
action_model._process(cr, uid, action, [new_id], context=context)
|
||||
return new_id
|
||||
for action in actions.with_context(old_values=None):
|
||||
action._process(action._filter_post(record))
|
||||
|
||||
return record.with_env(self.env)
|
||||
|
||||
return create
|
||||
|
||||
@@ -215,69 +239,45 @@ class base_action_rule(osv.osv):
|
||||
# Note: we patch method _write() instead of write() in order to
|
||||
# catch updates made by field recomputations.
|
||||
#
|
||||
def _write(self, cr, uid, ids, vals, context=None):
|
||||
# avoid loops or cascading actions
|
||||
if context and context.get('action'):
|
||||
return _write.origin(self, cr, uid, ids, vals, context=context)
|
||||
|
||||
# modify context
|
||||
context = dict(context or {}, action=True)
|
||||
ids = [ids] if isinstance(ids, (int, long, str)) else ids
|
||||
|
||||
@openerp.api.multi
|
||||
def _write(self, vals):
|
||||
# retrieve the action rules to possibly execute
|
||||
action_model = self.pool.get('base.action.rule')
|
||||
action_dom = [('model', '=', self._name),
|
||||
('kind', 'in', ['on_write', 'on_create_or_write'])]
|
||||
action_ids = action_model.search(cr, uid, action_dom, context=context)
|
||||
actions = action_model.browse(cr, uid, action_ids, context=context)
|
||||
actions = self.env['base.action.rule']._get_actions(self, ['on_write', 'on_create_or_write'])
|
||||
records = self.with_env(actions.env)
|
||||
|
||||
# check preconditions
|
||||
pre_ids = {}
|
||||
for action in actions:
|
||||
pre_ids[action] = action_model._filter(cr, uid, action, action.filter_pre_id, ids, domain=action.filter_pre_domain, context=context)
|
||||
# check preconditions on records
|
||||
pre = {action: action._filter_pre(records) for action in actions}
|
||||
|
||||
# read old values before the update
|
||||
old_values = {}
|
||||
for old_vals in self.read(cr, uid, ids, list(vals), context=context):
|
||||
old_values[old_vals.pop('id')] = old_vals
|
||||
old_values = {
|
||||
old_vals.pop('id'): old_vals
|
||||
for old_vals in records.read(list(vals))
|
||||
}
|
||||
|
||||
# call original method
|
||||
_write.origin(self, cr, uid, ids, vals, context=context)
|
||||
_write.origin(records, vals)
|
||||
|
||||
# check postconditions, and execute actions on the records that satisfy them
|
||||
for action in actions:
|
||||
post_ids = action_model._filter(cr, uid, action, action.filter_id, pre_ids[action], domain=action.filter_domain, context=context)
|
||||
if post_ids:
|
||||
action_model._process(cr, uid, action, post_ids, context=dict(context, old_values=old_values))
|
||||
for action in actions.with_context(old_values=old_values):
|
||||
action._process(action._filter_post(pre[action]))
|
||||
return True
|
||||
|
||||
return _write
|
||||
|
||||
def make_unlink():
|
||||
""" Instanciate an unlink method that processes action rules. """
|
||||
def unlink(self, cr, uid, ids, context=None, **kwargs):
|
||||
if context and context.get('action'):
|
||||
return unlink.origin(self, cr, uid, ids, context=context)
|
||||
|
||||
# modify context
|
||||
context = dict(context or {}, action=True)
|
||||
ids = [ids] if isinstance(ids, (int, long, str)) else ids
|
||||
|
||||
@openerp.api.multi
|
||||
def unlink(self, **kwargs):
|
||||
# retrieve the action rules to possibly execute
|
||||
action_model = self.pool.get('base.action.rule')
|
||||
action_dom = [('model', '=', self._name),
|
||||
('kind', '=', 'on_unlink')]
|
||||
action_ids = action_model.search(cr, uid, action_dom, context=context)
|
||||
actions = action_model.browse(cr, uid, action_ids, context=context)
|
||||
actions = self.env['base.action.rule']._get_actions(self, ['on_unlink'])
|
||||
records = self.with_env(actions.env)
|
||||
|
||||
# check conditions, and execute actions on the records that satisfy them
|
||||
for action in actions:
|
||||
pre_ids = action_model._filter(cr, uid, action, action.filter_id, ids, domain=action.filter_domain, context=context)
|
||||
if pre_ids:
|
||||
action_model._process(cr, uid, action, pre_ids, context=context)
|
||||
action._process(action._filter_post(pre[action]))
|
||||
|
||||
# call original method
|
||||
return unlink.origin(self, cr, uid, ids, context=context, **kwargs)
|
||||
return unlink.origin(self, **kwargs)
|
||||
|
||||
return unlink
|
||||
|
||||
|
||||
@@ -43,5 +43,12 @@
|
||||
<field name="act_user_id" ref="base.user_demo"/>
|
||||
<field name="filter_domain">[('customer', '=', True)]</field>
|
||||
</record>
|
||||
|
||||
<record id="test_rule_on_line" model="base.action.rule">
|
||||
<field name="name">Test rule on secondary record</field>
|
||||
<field name="kind">on_create</field>
|
||||
<field name="model_id" ref="base_action_rule.model_base_action_rule_line_test"/>
|
||||
<field name="act_user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_base_action_rule,base.action.rule,model_base_action_rule,,1,0,0,0
|
||||
access_base_action_rule_config,base.action.rule config,model_base_action_rule,base.group_system,1,1,1,1
|
||||
access_base_action_rule_lead_test,access_base_action_rule_lead_test,model_base_action_rule_lead_test,base.group_system,1,1,1,1
|
||||
access_base_action_rule_line_test,access_base_action_rule_line_test,model_base_action_rule_line_test,base.group_system,1,1,1,1
|
||||
|
||||
|
@@ -24,6 +24,7 @@ class lead_test(osv.Model):
|
||||
'active': fields.boolean('Active', required=False),
|
||||
'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null'),
|
||||
'date_action_last': fields.datetime('Last Action', readonly=1),
|
||||
'line_ids': fields.one2many('base.action.rule.line.test', 'lead_id'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
@@ -39,3 +40,12 @@ class lead_test(osv.Model):
|
||||
|
||||
def message_subscribe(self, cr, uid, ids, partner_ids=None, channel_ids=None, subtype_ids=None, force=True, context=None):
|
||||
pass
|
||||
|
||||
|
||||
class line_test(osv.Model):
|
||||
_name = "base.action.rule.line.test"
|
||||
_description = "Action Rule Line Test"
|
||||
|
||||
name = openerp.fields.Char()
|
||||
lead_id = openerp.fields.Many2one('base.action.rule.lead.test', ondelete='cascade')
|
||||
user_id = openerp.fields.Many2one('res.users')
|
||||
|
||||
@@ -77,7 +77,7 @@ class base_action_rule_test(common.TransactionCase):
|
||||
self.assertEqual(lead.state, 'done')
|
||||
self.assertEqual(lead.user_id, self.user_demo)
|
||||
|
||||
def test_04_recomputed_field(self):
|
||||
def test_10_recomputed_field(self):
|
||||
"""
|
||||
Check that a rule is executed whenever a field is recomputed after a
|
||||
change on another model.
|
||||
@@ -91,3 +91,20 @@ class base_action_rule_test(common.TransactionCase):
|
||||
partner.write({'customer': True})
|
||||
self.assertTrue(lead.customer)
|
||||
self.assertEqual(lead.user_id, self.user_demo)
|
||||
|
||||
def test_20_direct_line(self):
|
||||
"""
|
||||
Check that a rule is executed after creating a line record.
|
||||
"""
|
||||
line = self.env['base.action.rule.line.test'].create({'name': "Line"})
|
||||
self.assertEqual(line.user_id, self.user_demo)
|
||||
|
||||
def test_20_indirect_line(self):
|
||||
"""
|
||||
Check that creating a lead with a line executes rules on both records.
|
||||
"""
|
||||
lead = self.create_lead(line_ids=[(0, 0, {'name': "Line"})])
|
||||
self.assertEqual(lead.state, 'draft')
|
||||
self.assertEqual(lead.user_id, self.user_demo)
|
||||
self.assertEqual(len(lead.line_ids), 1)
|
||||
self.assertEqual(lead.line_ids.user_id, self.user_demo)
|
||||
|
||||
@@ -48,7 +48,13 @@ class meeting_invitation(http.Controller):
|
||||
if attendee:
|
||||
attendee_data['current_attendee'] = attendee[0]
|
||||
|
||||
values = dict(init="s.calendar.event('%s', '%s', '%s', '%s' , '%s');" % (db, action, id, 'form', json.dumps(attendee_data)))
|
||||
values = dict(
|
||||
init = """
|
||||
odoo.define('calendar.invitation_page', function (require) {
|
||||
require('base_calendar.base_calendar').showCalendarInvitation('%s', '%s', '%s', '%s', '%s');
|
||||
});
|
||||
""" % (db, action, id, 'form', json.dumps(attendee_data))
|
||||
)
|
||||
return request.render('web.webclient_bootstrap', values)
|
||||
|
||||
# Function used, in RPC to check every 5 minutes, if notification to do for an event or not
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 9.0\n"
|
||||
"Project-Id-Version: Odoo Server 9.0c\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-11-25 13:28+0000\n"
|
||||
"PO-Revision-Date: 2015-11-25 13:28+0000\n"
|
||||
"POT-Creation-Date: 2015-11-26 09:05+0000\n"
|
||||
"PO-Revision-Date: 2015-11-26 09:05+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -381,26 +381,26 @@ msgid "${object.event_id.name} - Reminder"
|
||||
msgstr ""
|
||||
|
||||
#. module: calendar
|
||||
#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form
|
||||
#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form_popup
|
||||
msgid "<span> hours</span>"
|
||||
msgstr ""
|
||||
|
||||
#. module: calendar
|
||||
#: code:addons/calendar/calendar.py:802
|
||||
#: code:addons/calendar/calendar.py:775
|
||||
#, python-format
|
||||
msgid "%s at %s To\n"
|
||||
" %s at %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#. module: calendar
|
||||
#: code:addons/calendar/calendar.py:800
|
||||
#: code:addons/calendar/calendar.py:773
|
||||
#, python-format
|
||||
msgid "%s at (%s To %s) (%s)"
|
||||
msgstr ""
|
||||
|
||||
#. module: calendar
|
||||
#: code:addons/calendar/calendar.py:1628
|
||||
#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form
|
||||
#: model:ir.ui.view,arch_db:calendar.view_calendar_event_form_popup
|
||||
msgid "<span> hours</span>"
|
||||
msgstr ""
|
||||
|
||||
#. module: calendar
|
||||
#: code:addons/calendar/calendar.py:1620
|
||||
#, python-format
|
||||
msgid "A email has been send to specify that the date has been changed !"
|
||||
msgstr ""
|
||||
@@ -526,13 +526,6 @@ msgstr ""
|
||||
msgid "Click to schedule a new meeting."
|
||||
msgstr ""
|
||||
|
||||
#. module: calendar
|
||||
#: view:calendar.alarm:calendar.view_calendar_alarm_tree
|
||||
#: model:ir.actions.act_window,name:calendar.action_calendar_alarm
|
||||
#: model:ir.ui.menu,name:calendar.menu_calendar_alarm
|
||||
msgid "Calendar Alarm"
|
||||
msgstr ""
|
||||
|
||||
#. module: calendar
|
||||
#: model:ir.model.fields,field_description:calendar.field_calendar_event_color_partner_id
|
||||
msgid "Color index of creator"
|
||||
|
||||
@@ -8,6 +8,7 @@ var Dialog = require('web.Dialog');
|
||||
var form_common = require('web.form_common');
|
||||
var Model = require('web.DataModel');
|
||||
var Notification = require('web.notification').Notification;
|
||||
var session = require('web.session');
|
||||
var WebClient = require('web.WebClient');
|
||||
var widgets = require('web_calendar.widgets');
|
||||
|
||||
@@ -252,6 +253,20 @@ var Many2ManyAttendee = FieldMany2ManyTags.extend({
|
||||
}
|
||||
});
|
||||
|
||||
function showCalendarInvitation(db, action, id, view, attendee_data) {
|
||||
session.session_bind(session.origin).then(function () {
|
||||
if (session.session_is_valid(db) && session.username !== "anonymous") {
|
||||
window.location.href = _.str.sprintf('/web?db=%s#id=%s&view_type=form&model=calendar.event', db, id);
|
||||
} else {
|
||||
$("body").prepend(QWeb.render('CalendarInvitation', {attendee_data: JSON.parse(attendee_data)}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
core.form_widget_registry.add('many2manyattendee', Many2ManyAttendee);
|
||||
|
||||
return {
|
||||
showCalendarInvitation: showCalendarInvitation,
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
&.needsAction {
|
||||
background-color: @odoo-brand-secondary;
|
||||
}
|
||||
|
||||
&.declined {
|
||||
background-color: @brand-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.o_calendar_all_responsibles {
|
||||
@@ -60,3 +64,40 @@
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.o_calendar_invitation_page {
|
||||
.o-flex(0, 0, auto);
|
||||
width: 50%;
|
||||
margin: 30px auto 0;
|
||||
.o-webclient-padding(@top: 10px, @bottom: 10px);
|
||||
background-color: @odoo-view-background-color;
|
||||
|
||||
.o_logo {
|
||||
width: 15%;
|
||||
}
|
||||
.o_event_title {
|
||||
margin-left: 20%;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.o_event_table {
|
||||
clear: both;
|
||||
margin: 15px 0 0;
|
||||
|
||||
th {
|
||||
padding-right: 15px;
|
||||
}
|
||||
ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_accepted {
|
||||
color: @brand-success;
|
||||
}
|
||||
.o_declined {
|
||||
color: @brand-danger;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,41 @@
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<div t-name="CalendarInvitation" class="o_calendar_invitation_page">
|
||||
<div class="pull-right">
|
||||
<t t-esc="attendee_data['current_attendee'].cn"/> (<t t-esc="attendee_data['current_attendee'].email"/>)
|
||||
<div t-if="attendee_data['current_attendee'].state !== 'needsAction'" t-attf-class="o_#{attendee_data['current_attendee'].state}">
|
||||
<t t-if="attendee_data['current_attendee'].state === 'accepted'">Yes I'm going.</t>
|
||||
<t t-if="attendee_data['current_attendee'].state === 'declined'">No I'm not going.</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-left o_logo">
|
||||
<img class="img img-responsive" src='/web/binary/company_logo'/>
|
||||
</div>
|
||||
<div class="o_event_title">
|
||||
<h2>Calendar Invitation</h2>
|
||||
<h3><t t-esc="attendee_data['meeting'].event"/></h3>
|
||||
</div>
|
||||
<table class="o_event_table table table-striped">
|
||||
<tr>
|
||||
<th>When</th>
|
||||
<td><t t-esc="attendee_data['meeting'].when"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Where</th>
|
||||
<td><t t-esc="attendee_data['meeting'].where or '-'"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Who</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li t-foreach="attendee_data['attendee']" t-as="attendee" t-attf-class="o_#{attendee.status}">
|
||||
<t t-esc="attendee.name"/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -290,7 +290,15 @@ class crm_lead(format_address, osv.osv):
|
||||
def on_change_user(self, cr, uid, ids, user_id, context=None):
|
||||
""" When changing the user, also set a team_id or restrict team id
|
||||
to the ones user_id is member of. """
|
||||
if user_id and context.get('team_id'):
|
||||
team = self.pool['crm.team'].browse(cr, uid, context['team_id'], context=context)
|
||||
if user_id in team.member_ids.ids:
|
||||
return {}
|
||||
team_id = self.pool['crm.team']._get_default_team_id(cr, uid, context=context, user_id=user_id)
|
||||
if user_id and not team_id and self.pool['res.users'].has_group(cr, uid, 'base.group_multi_salesteams'):
|
||||
team_ids = self.pool.get('crm.team').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context)
|
||||
if team_ids:
|
||||
team_id = team_ids[0]
|
||||
return {'value': {'team_id': team_id}}
|
||||
|
||||
def stage_find(self, cr, uid, cases, team_id, domain=None, order='sequence', context=None):
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="user_id" on_change="on_change_user(user_id, context)"
|
||||
context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_sale_salesman_all_leads'] }" widget="selection"/>
|
||||
context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_sale_salesman_all_leads'], 'team_id': team_id}"/>
|
||||
<field name="team_id" widget="selection" domain="[('use_leads','=',True)]"/>
|
||||
<field name="type" invisible="1"/>
|
||||
</group>
|
||||
@@ -496,7 +496,7 @@
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<field name="user_id" on_change="on_change_user(user_id, context)" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_sale_salesman_all_leads']}" widget="selection"/>
|
||||
<field name="user_id" on_change="on_change_user(user_id, context)" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_sale_salesman_all_leads'], 'team_id': team_id}"/>
|
||||
<field name="team_id" widget="selection"/>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
import hr
|
||||
|
||||
import res_users
|
||||
import res_partner
|
||||
|
||||
@@ -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
|
||||
@@ -208,47 +208,54 @@ class HrExpense(models.Model):
|
||||
if any(not expense.journal_id for expense in self):
|
||||
raise UserError(_("Expenses must have an expense journal specified to generate accounting entries."))
|
||||
|
||||
#create the move that will contain the accounting entries
|
||||
move = self.env['account.move'].create({
|
||||
'journal_id': self.journal_id.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
})
|
||||
|
||||
journal_dict = {}
|
||||
for expense in self:
|
||||
company_currency = expense.company_id.currency_id
|
||||
diff_currency_p = expense.currency_id != company_currency
|
||||
#one account.move.line per expense (+taxes..)
|
||||
move_lines = expense._move_line_get()
|
||||
if expense.journal_id not in journal_dict:
|
||||
journal_dict[expense.journal_id] = []
|
||||
journal_dict[expense.journal_id].append(expense)
|
||||
|
||||
#create one more move line, a counterline for the total on payable account
|
||||
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines)
|
||||
if expense.payment_mode == 'company_account':
|
||||
if not expense.bank_journal_id.default_credit_account_id:
|
||||
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name))
|
||||
emp_account = expense.bank_journal_id.default_credit_account_id.id
|
||||
else:
|
||||
if not expense.employee_id.address_home_id:
|
||||
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
|
||||
emp_account = expense.employee_id.address_home_id.property_account_payable_id.id
|
||||
for journal, expense_list in journal_dict.items():
|
||||
#create the move that will contain the accounting entries
|
||||
move = self.env['account.move'].create({
|
||||
'journal_id': journal.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
})
|
||||
for expense in expense_list:
|
||||
company_currency = expense.company_id.currency_id
|
||||
diff_currency_p = expense.currency_id != company_currency
|
||||
#one account.move.line per expense (+taxes..)
|
||||
move_lines = expense._move_line_get()
|
||||
|
||||
move_lines.append({
|
||||
'type': 'dest',
|
||||
'name': '/',
|
||||
'price': total,
|
||||
'account_id': emp_account,
|
||||
'date_maturity': expense.date,
|
||||
'amount_currency': diff_currency_p and total_currency or False,
|
||||
'currency_id': diff_currency_p and expense.currency_id.id or False,
|
||||
'ref': expense.employee_id.address_home_id.ref or False
|
||||
})
|
||||
#create one more move line, a counterline for the total on payable account
|
||||
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines)
|
||||
if expense.payment_mode == 'company_account':
|
||||
if not expense.bank_journal_id.default_credit_account_id:
|
||||
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.bank_journal_id.name))
|
||||
emp_account = expense.bank_journal_id.default_credit_account_id.id
|
||||
else:
|
||||
if not expense.employee_id.address_home_id:
|
||||
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
|
||||
emp_account = expense.employee_id.address_home_id.property_account_payable_id.id
|
||||
|
||||
#convert eml into an osv-valid format
|
||||
lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines)
|
||||
move.write({'line_ids': lines})
|
||||
expense.write({'account_move_id': move.id, 'state': 'post'})
|
||||
if expense.payment_mode == 'company_account':
|
||||
expense.paid_expenses()
|
||||
return move.post()
|
||||
move_lines.append({
|
||||
'type': 'dest',
|
||||
'name': '/',
|
||||
'price': total,
|
||||
'account_id': emp_account,
|
||||
'date_maturity': expense.date,
|
||||
'amount_currency': diff_currency_p and total_currency or False,
|
||||
'currency_id': diff_currency_p and expense.currency_id.id or False,
|
||||
'ref': expense.employee_id.address_home_id.ref or False
|
||||
})
|
||||
|
||||
#convert eml into an osv-valid format
|
||||
lines = map(lambda x:(0, 0, expense._prepare_move_line(x)), move_lines)
|
||||
move.write({'line_ids': lines})
|
||||
expense.write({'account_move_id': move.id, 'state': 'post'})
|
||||
if expense.payment_mode == 'company_account':
|
||||
expense.paid_expenses()
|
||||
move.post()
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def _move_line_get(self):
|
||||
|
||||
@@ -325,7 +325,7 @@
|
||||
</record>
|
||||
|
||||
<record id="hr_expense_entry_action_server" model="ir.actions.server">
|
||||
<field name="name">Generate Single Accounting Entry</field>
|
||||
<field name="name">Generate Accounting Entries</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="model_id" ref="model_hr_expense"/>
|
||||
<field name="state">code</field>
|
||||
|
||||
@@ -128,15 +128,28 @@ var LivechatButton = Widget.extend({
|
||||
},
|
||||
|
||||
add_message: function (data) {
|
||||
this.messages.push({
|
||||
var msg = {
|
||||
id: data.id,
|
||||
attachment_ids: data.attachment_ids,
|
||||
author_id: data.author_id,
|
||||
body: data.body,
|
||||
date: data.date,
|
||||
date: moment(time.str_to_datetime(data.date)),
|
||||
is_needaction: false,
|
||||
is_note: data.is_note,
|
||||
});
|
||||
};
|
||||
|
||||
// Compute displayed author name or email
|
||||
msg.displayed_author = msg.author_id && msg.author_id[1] ||
|
||||
this.options.default_username;
|
||||
|
||||
// Compute the avatar_url
|
||||
if (msg.author_id && msg.author_id[0]) {
|
||||
msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
|
||||
} else {
|
||||
msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg";
|
||||
}
|
||||
|
||||
this.messages.push(msg);
|
||||
},
|
||||
|
||||
render_messages: function () {
|
||||
|
||||
@@ -20,12 +20,12 @@ msgstr ""
|
||||
#. module: l10n_be
|
||||
#: model:account.tax.template,name:l10n_be.attn_VAT-IN-V83-00
|
||||
msgid "0% Biens d'investissement"
|
||||
msgstr ""
|
||||
msgstr "0% Investeringsgoederen"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.tax.template,name:l10n_be.attn_VAT-IN-V82-00-G
|
||||
msgid "0% Biens divers"
|
||||
msgstr ""
|
||||
msgstr "0% Diverse goederen"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.tax.template,name:l10n_be.attn_VAT-OUT-00-CC
|
||||
@@ -189,7 +189,7 @@ msgstr "Vooruitbetalingen"
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a360
|
||||
msgid "Acomptes versés sur achats pour stocks"
|
||||
msgstr ""
|
||||
msgstr "Vooruitbetalingen op aankopen voor aandelen"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a4550
|
||||
@@ -239,7 +239,7 @@ msgstr "Advertenties, publiciteit, propaganda en documentatie"
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a3100
|
||||
msgid "Approvisionements et fournitures"
|
||||
msgstr ""
|
||||
msgstr "Materialen en benodigdheden"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a4160
|
||||
@@ -280,7 +280,7 @@ msgstr "Overige leningen"
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a4125
|
||||
msgid "Autres impôts belges"
|
||||
msgstr ""
|
||||
msgstr "Overige Belgische belastingen"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a49102
|
||||
@@ -301,7 +301,7 @@ msgstr "Voorschotten en leningen aan het personeel"
|
||||
#. module: l10n_be
|
||||
#: model:account.chart.template,name:l10n_be.l10nbe_chart_template
|
||||
msgid "Belgian PCMN"
|
||||
msgstr ""
|
||||
msgstr "Belgische PCMN"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a140
|
||||
@@ -331,7 +331,7 @@ msgstr "Vrachtwagens"
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a1000
|
||||
msgid "Capital"
|
||||
msgstr ""
|
||||
msgstr "Kapitaal"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a418
|
||||
@@ -341,7 +341,7 @@ msgstr "Borgtochten betaald in contanten"
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a6600
|
||||
msgid "Charges exceptionelles"
|
||||
msgstr ""
|
||||
msgstr "Uitzonderlijke kosten"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a6101
|
||||
@@ -371,7 +371,7 @@ msgstr "Klanten"
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a370
|
||||
msgid "Commandes en cours d'exécution"
|
||||
msgstr ""
|
||||
msgstr "Lopende opdrachten"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a49101
|
||||
@@ -448,7 +448,7 @@ msgstr "Leasingschulden van roerende goederen"
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a17300
|
||||
msgid "Dettes en compte \\ Banque A"
|
||||
msgstr ""
|
||||
msgstr "Schulden op rekeningen \\Bank A"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a4597
|
||||
@@ -636,7 +636,7 @@ msgstr "Diverse huren"
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a3400
|
||||
msgid "Marchandises"
|
||||
msgstr ""
|
||||
msgstr "Goederen"
|
||||
|
||||
#. module: l10n_be
|
||||
#: model:account.account.template,name:l10n_be.a300
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
<record id="open_l10n_eu_service" model="ir.actions.todo">
|
||||
<field name="action_id" ref="action_eu_service"/>
|
||||
<field name="type">automatic</field>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
|
||||
<record id="view_account_config_settings_inherit" model="ir.ui.view">
|
||||
|
||||
@@ -44,6 +44,7 @@ class MailController(http.Controller):
|
||||
result.append({
|
||||
'id': follower.id,
|
||||
'name': follower.partner_id.name or follower.channel_id.name,
|
||||
'email': follower.partner_id.email if follower.partner_id else None,
|
||||
'res_model': 'res.partner' if follower.partner_id else 'mail.channel',
|
||||
'res_id': follower.partner_id.id or follower.channel_id.id,
|
||||
'is_editable': is_editable,
|
||||
@@ -241,6 +242,7 @@ class MailController(http.Controller):
|
||||
values = {
|
||||
'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(),
|
||||
'chatter_needaction_auto': request.env.user.chatter_needaction_auto,
|
||||
'channel_slots': request.env['mail.channel'].channel_fetch_slot()
|
||||
'channel_slots': request.env['mail.channel'].channel_fetch_slot(),
|
||||
'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(),
|
||||
}
|
||||
return values
|
||||
|
||||
@@ -166,7 +166,6 @@ class Channel(models.Model):
|
||||
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
|
||||
return result
|
||||
|
||||
|
||||
@api.multi
|
||||
def message_get_email_values(self, notif_mail=None):
|
||||
self.ensure_one()
|
||||
@@ -423,6 +422,10 @@ class Channel(models.Model):
|
||||
for channel in self:
|
||||
partners_to_add = partners - channel.channel_partner_ids
|
||||
channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
|
||||
for partner in partners_to_add:
|
||||
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
|
||||
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id)
|
||||
|
||||
# broadcast the channel header to the added partner
|
||||
self._broadcast(partner_ids)
|
||||
|
||||
@@ -525,7 +528,7 @@ class Channel(models.Model):
|
||||
'email_send': False,
|
||||
'channel_partner_ids': [(4, self.env.user.partner_id.id)]
|
||||
})
|
||||
channel_info = new_channel.channel_info()[0]
|
||||
channel_info = new_channel.channel_info('creation')[0]
|
||||
notification = _('<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (new_channel.id, new_channel.name,)
|
||||
new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
|
||||
@@ -538,10 +541,21 @@ class Channel(models.Model):
|
||||
user isn't registered to. """
|
||||
domain = expression.AND([
|
||||
[('name', 'ilike', search)],
|
||||
[('channel_type', '!=', 'chat')],
|
||||
[('channel_type', '=', 'channel')],
|
||||
expression.OR([
|
||||
[('public', '!=', 'private')],
|
||||
[('channel_partner_ids', 'in', [self.env.user.partner_id.id])]
|
||||
])
|
||||
])
|
||||
return self.search_read(domain, ['id', 'name', 'public'], limit=limit)
|
||||
|
||||
@api.model
|
||||
def channel_fetch_listeners(self, uuid):
|
||||
""" Return the id, name and email of partners listening to the given channel """
|
||||
self._cr.execute("""
|
||||
SELECT P.id, P.name, P.email
|
||||
FROM mail_channel_partner CP
|
||||
INNER JOIN res_partner P ON CP.partner_id = P.id
|
||||
INNER JOIN mail_channel C ON CP.channel_id = C.id
|
||||
WHERE C.uuid = %s""", (uuid,))
|
||||
return self._cr.dictfetchall()
|
||||
|
||||
@@ -212,49 +212,27 @@ class Partner(models.Model):
|
||||
return 0
|
||||
|
||||
@api.model
|
||||
def get_mention_suggestions(self, search, channel, exclude=None, limit=8):
|
||||
def get_static_mention_suggestions(self):
|
||||
""" To be overwritten to return the id, name and email of partners used as static mention
|
||||
suggestions loaded once at webclient initialization and stored client side. """
|
||||
return []
|
||||
|
||||
@api.model
|
||||
def get_mention_suggestions(self, search, limit=8):
|
||||
""" Return 'limit'-first partners' id, name and email such that the name or email matches a
|
||||
'search' string. Prioritize partners registered to channel 'channel[channel_id]' if
|
||||
given, or partners that are followers of a document identified by 'channel[res_model]'
|
||||
and 'channel[res_id]' otherwise, then users, and finally extend the research to all
|
||||
partners. Exclude partners whose id is in 'exclude'. """
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
members = []
|
||||
users = []
|
||||
partners = []
|
||||
search_dom = expression.AND([
|
||||
expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]]),
|
||||
[('id', 'not in', exclude)]
|
||||
])
|
||||
'search' string. Prioritize users, and then extend the research to all partners. """
|
||||
search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]])
|
||||
fields = ['id', 'name', 'email']
|
||||
|
||||
def search_partners(domain, fields, limit, exclude):
|
||||
partners = self.search_read(domain, fields, limit=limit)
|
||||
limit -= len(partners)
|
||||
exclude += [partner['id'] for partner in partners]
|
||||
return partners, limit, exclude
|
||||
# Search users
|
||||
domain = expression.AND([[('user_ids.id', '!=', False)], search_dom])
|
||||
users = self.search_read(domain, fields, limit=limit)
|
||||
|
||||
# Search users registered to the channel
|
||||
if 'channel_id' in channel:
|
||||
domain = expression.AND([[('channel_ids', 'in', [channel['channel_id']])], search_dom])
|
||||
members, limit, exclude = search_partners(domain, fields, limit, exclude)
|
||||
else:
|
||||
domain = expression.AND([
|
||||
[('res_model', '=', channel['res_model'])],
|
||||
[('res_id', '=', channel['res_id'])]
|
||||
])
|
||||
followers = self.env['mail.followers'].search(domain)
|
||||
domain = expression.AND([[('id', 'in', followers.mapped('partner_id').ids)], search_dom])
|
||||
members, limit, exclude = search_partners(domain, fields, limit, exclude)
|
||||
# Search partners if less than 'limit' users found
|
||||
partners = []
|
||||
if len(users) < limit:
|
||||
partners = self.search_read(search_dom, fields, limit=limit)
|
||||
# Remove duplicates
|
||||
partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])]
|
||||
|
||||
if limit > 0:
|
||||
# Search users
|
||||
domain = expression.AND([[('user_ids.id', '!=', False)], search_dom])
|
||||
users, limit, exclude = search_partners(domain, fields, limit, exclude)
|
||||
|
||||
if limit > 0:
|
||||
# Search partners
|
||||
partners = self.search_read(search_dom, fields, limit=limit)
|
||||
|
||||
return [members, users, partners]
|
||||
return [users, partners]
|
||||
|
||||
@@ -6,6 +6,7 @@ var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var Model = require('web.Model');
|
||||
var session = require('web.session');
|
||||
var time = require('web.time');
|
||||
var web_client = require('web.web_client');
|
||||
|
||||
var _t = core._t;
|
||||
@@ -23,6 +24,7 @@ var channel_defs = {};
|
||||
var emojis = [];
|
||||
var emoji_substitutions = {};
|
||||
var needaction_counter = 0;
|
||||
var mention_partner_suggestions = [];
|
||||
var discuss_ids = {};
|
||||
|
||||
// Message and channel manipulation helpers
|
||||
@@ -39,6 +41,13 @@ function add_message (data, options) {
|
||||
messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
|
||||
_.each(msg.channel_ids, function (channel_id) {
|
||||
var channel = chat_manager.get_channel(channel_id);
|
||||
|
||||
if (channel) {
|
||||
add_to_cache(msg, []);
|
||||
if (options.domain && options.domain !== []) {
|
||||
add_to_cache(msg, options.domain);
|
||||
}
|
||||
}
|
||||
if (channel && channel.hidden) {
|
||||
channel.hidden = false;
|
||||
chat_manager.bus.trigger('new_channel', channel);
|
||||
@@ -61,6 +70,8 @@ function add_message (data, options) {
|
||||
if (!options.silent) {
|
||||
chat_manager.bus.trigger('new_message', msg);
|
||||
}
|
||||
} else if (options.domain && options.domain !== []) {
|
||||
add_to_cache(msg, options.domain);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
@@ -71,7 +82,7 @@ function make_message (data) {
|
||||
author_id: data.author_id,
|
||||
body_short: data.body_short || "",
|
||||
body: data.body || "",
|
||||
date: data.date,
|
||||
date: moment(time.str_to_datetime(data.date)),
|
||||
message_type: data.message_type,
|
||||
subtype_description: data.subtype_description,
|
||||
is_note: data.is_note,
|
||||
@@ -118,13 +129,38 @@ function make_message (data) {
|
||||
if (_.contains(data.starred_partner_ids, session.partner_id)) {
|
||||
msg.is_starred = true;
|
||||
}
|
||||
var real_channels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred');
|
||||
var origin = real_channels.length ? real_channels[0] : undefined;
|
||||
var channel = origin && chat_manager.get_channel(origin);
|
||||
if (channel) {
|
||||
msg.origin_id = origin;
|
||||
msg.origin_name = channel.name;
|
||||
if (msg.model === 'mail.channel') {
|
||||
var real_channels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred');
|
||||
var origin = real_channels.length === 1 ? real_channels[0] : undefined;
|
||||
var channel = origin && chat_manager.get_channel(origin);
|
||||
if (channel) {
|
||||
msg.origin_id = origin;
|
||||
msg.origin_name = channel.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute displayed author name or email
|
||||
if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) {
|
||||
msg.mailto = msg.email_from;
|
||||
} else {
|
||||
msg.displayed_author = msg.author_id && msg.author_id[1] ||
|
||||
msg.email_from || _t('Anonymous');
|
||||
}
|
||||
|
||||
// Compute the avatar_url
|
||||
if (msg.author_id && msg.author_id[0]) {
|
||||
msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
|
||||
} else if (msg.message_type === 'email') {
|
||||
msg.avatar_src = "/mail/static/src/img/email_icon.png";
|
||||
} else {
|
||||
msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg";
|
||||
}
|
||||
|
||||
// Compute url of attachments
|
||||
_.each(msg.attachment_ids, function(a) {
|
||||
a.url = '/web/content/' + a.id + '?download=true';
|
||||
});
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -207,7 +243,7 @@ function make_channel (data, options) {
|
||||
cache: {'[]': {
|
||||
all_history_loaded: false,
|
||||
loaded: false,
|
||||
message_ids: [],
|
||||
messages: [],
|
||||
}},
|
||||
};
|
||||
if (channel.type === "channel" && data.public !== "private") {
|
||||
@@ -229,17 +265,39 @@ function get_channel_cache (channel, domain) {
|
||||
channel.cache[stringified_domain] = {
|
||||
all_history_loaded: false,
|
||||
loaded: false,
|
||||
message_ids: [],
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
return channel.cache[stringified_domain];
|
||||
}
|
||||
|
||||
function invalidate_caches(channel_ids) {
|
||||
_.each(channel_ids, function (channel_id) {
|
||||
var channel = chat_manager.get_channel(channel_id);
|
||||
if (channel) {
|
||||
channel.cache = { '[]': channel.cache['[]']};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function add_to_cache(message, domain) {
|
||||
_.each(message.channel_ids, function (channel_id) {
|
||||
var channel = chat_manager.get_channel(channel_id);
|
||||
if (channel) {
|
||||
var channel_cache = get_channel_cache(channel, domain);
|
||||
var index = _.sortedIndex(channel_cache.messages, message, 'id');
|
||||
if (channel_cache.messages[index] !== message) {
|
||||
channel_cache.messages.splice(index, 0, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function remove_message_from_channel (channel_id, message) {
|
||||
message.channel_ids = _.without(message.channel_ids, channel_id);
|
||||
var channel = _.findWhere(channels, { id: channel_id });
|
||||
_.each(channel.cache, function (cache) {
|
||||
cache.message_ids = _.without(cache.message_ids, message.id);
|
||||
cache.messages = _.without(cache.messages, message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -256,28 +314,21 @@ function fetch_from_channel (channel, options) {
|
||||
domain = new data.CompoundDomain(domain, options.domain || []);
|
||||
}
|
||||
if (options.load_more) {
|
||||
var min_message_id = _.chain(messages)
|
||||
.filter(function (msg) { return _.contains(cache.message_ids, msg.id); })
|
||||
.pluck("id")
|
||||
.min()
|
||||
.value();
|
||||
|
||||
var min_message_id = cache.messages[0].id;
|
||||
domain = new data.CompoundDomain([['id', '<', min_message_id]], domain);
|
||||
}
|
||||
|
||||
return MessageModel.call('message_fetch', [domain], {limit: LIMIT}).then(function (msgs) {
|
||||
cache.message_ids = _.uniq(cache.message_ids.concat(_.pluck(msgs, 'id')));
|
||||
if (!cache.all_history_loaded) {
|
||||
cache.all_history_loaded = msgs.length < LIMIT;
|
||||
}
|
||||
cache.loaded = true;
|
||||
|
||||
_.each(msgs, function (msg) {
|
||||
add_message(msg, {channel_id: channel.id, silent: true});
|
||||
});
|
||||
return _.filter(messages, function (m) {
|
||||
return _.contains(cache.message_ids, m.id);
|
||||
add_message(msg, {channel_id: channel.id, silent: true, domain: options.domain});
|
||||
});
|
||||
var channel_cache = get_channel_cache(channel, options.domain || []);
|
||||
return channel_cache.messages;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -335,6 +386,7 @@ function on_notification (notification) {
|
||||
|
||||
function on_needaction_notification (message) {
|
||||
message = add_message(message, { channel_id: 'channel_inbox', show_notification: true} );
|
||||
invalidate_caches(message.channel_ids);
|
||||
needaction_counter++;
|
||||
_.each(message.channel_ids, function (channel_id) {
|
||||
var channel = chat_manager.get_channel(channel_id);
|
||||
@@ -360,6 +412,7 @@ function on_channel_notification (message) {
|
||||
}
|
||||
});
|
||||
add_message(message, { show_notification: true });
|
||||
invalidate_caches(message.channel_ids);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -382,6 +435,7 @@ function on_toggle_star_notification (data) {
|
||||
_.each(data.message_ids, function (msg_id) {
|
||||
var message = _.findWhere(messages, { id: msg_id });
|
||||
if (message) {
|
||||
invalidate_caches(message.channel_ids);
|
||||
message.is_starred = data.starred;
|
||||
if (!message.is_starred) {
|
||||
remove_message_from_channel("channel_starred", message);
|
||||
@@ -395,6 +449,7 @@ function on_mark_as_read_notification (data) {
|
||||
_.each(data.message_ids, function (msg_id) {
|
||||
var message = _.findWhere(messages, { id: msg_id });
|
||||
if (message) {
|
||||
invalidate_caches(message.channel_ids);
|
||||
remove_message_from_channel("channel_inbox", message);
|
||||
chat_manager.bus.trigger('update_message', message);
|
||||
}
|
||||
@@ -419,6 +474,7 @@ function on_mark_as_unread_notification (data) {
|
||||
_.each(data.message_ids, function (message_id) {
|
||||
var message = _.findWhere(messages, { id: message_id });
|
||||
if (message) {
|
||||
invalidate_caches(message.channel_ids);
|
||||
add_channel_to_message(message, 'channel_inbox');
|
||||
}
|
||||
});
|
||||
@@ -433,10 +489,10 @@ function on_mark_as_unread_notification (data) {
|
||||
}
|
||||
|
||||
function on_chat_session_notification (chat_session) {
|
||||
if ((chat_session.channel_type === "channel") && (chat_session.public === "private") && (chat_session.state === "open")) {
|
||||
if ((chat_session.channel_type === "channel") && (chat_session.state === "open")) {
|
||||
add_channel(chat_session, {autoswitch: false});
|
||||
if (!chat_session.is_minimized) {
|
||||
web_client.do_notify(_t("Private Channel"), _t("You have been invited to: ") + chat_session.name);
|
||||
if (!chat_session.is_minimized && chat_session.info !== 'creation') {
|
||||
web_client.do_notify(_t("Invitation"), _t("You have been invited to: ") + chat_session.name);
|
||||
}
|
||||
}
|
||||
// partner specific change (open a detached window for example)
|
||||
@@ -460,9 +516,7 @@ var chat_manager = {
|
||||
var channel = this.get_channel(options.channel_id);
|
||||
var channel_cache = get_channel_cache(channel, options.domain);
|
||||
if (channel_cache.loaded) {
|
||||
return $.when(_.filter(messages, function (message) {
|
||||
return _.contains(message.channel_ids, options.channel_id);
|
||||
}));
|
||||
return $.when(channel_cache.messages);
|
||||
} else {
|
||||
return fetch_from_channel(channel, {domain: options.domain});
|
||||
}
|
||||
@@ -531,6 +585,27 @@ var chat_manager = {
|
||||
return get_channel_cache(channel, domain).all_history_loaded;
|
||||
},
|
||||
|
||||
get_mention_partner_suggestions: function (channel) {
|
||||
if (!channel) {
|
||||
return mention_partner_suggestions;
|
||||
}
|
||||
if (!channel.members_deferred) {
|
||||
channel.members_deferred = ChannelModel
|
||||
.call("channel_fetch_listeners", [channel.uuid])
|
||||
.then(function (members) {
|
||||
var suggestions = [];
|
||||
_.each(mention_partner_suggestions, function (partners) {
|
||||
suggestions.push(_.filter(partners, function (partner) {
|
||||
return !_.findWhere(members, { id: partner.id });
|
||||
}));
|
||||
});
|
||||
|
||||
return [members].concat(suggestions);
|
||||
});
|
||||
}
|
||||
return channel.members_deferred;
|
||||
},
|
||||
|
||||
get_emojis: function() {
|
||||
return emojis;
|
||||
},
|
||||
@@ -618,6 +693,7 @@ function init () {
|
||||
_.each(channels, add_channel);
|
||||
});
|
||||
needaction_counter = result.needaction_inbox_counter;
|
||||
mention_partner_suggestions = result.mention_partner_suggestions;
|
||||
});
|
||||
|
||||
var load_emojis = session.rpc("/mail/chat_init").then(function (result) {
|
||||
|
||||
@@ -37,6 +37,7 @@ var Followers = form_common.AbstractField.extend({
|
||||
|
||||
this.value = [];
|
||||
this.followers = [];
|
||||
this.followers_fetched = $.Deferred();
|
||||
this.data_subtype = {};
|
||||
|
||||
this.view_is_editable = this.__parentedParent.is_action_enabled('edit');
|
||||
@@ -188,9 +189,14 @@ var Followers = form_common.AbstractField.extend({
|
||||
},
|
||||
|
||||
fetch_followers: function (value_) {
|
||||
var self = this;
|
||||
this.value = value_ || [];
|
||||
return ajax.jsonRpc('/mail/read_followers', 'call', {'follower_ids': this.value})
|
||||
.then(this.proxy('display_followers'), this.proxy('fetch_generic'))
|
||||
.then(function (followers) {
|
||||
self.trigger('followers_update', followers);
|
||||
self.display_followers(followers);
|
||||
self.fetch_generic();
|
||||
})
|
||||
.then(this.proxy('display_buttons'))
|
||||
.then(this.proxy('fetch_subtypes'));
|
||||
},
|
||||
@@ -706,6 +712,7 @@ var Chatter = form_common.AbstractField.extend({
|
||||
if (this.followers) {
|
||||
this.$('.o_chatter_topbar').append(this.followers.$el);
|
||||
this.followers.on('redirect', this, this.on_redirect);
|
||||
this.followers.on('followers_update', this, this.on_followers_update);
|
||||
}
|
||||
|
||||
this.thread = new ChatThread(this, {
|
||||
@@ -798,6 +805,30 @@ var Chatter = form_common.AbstractField.extend({
|
||||
});
|
||||
},
|
||||
|
||||
on_followers_update: function (followers) {
|
||||
this.mention_suggestions = [];
|
||||
var self = this;
|
||||
var prefetched_partners = chat_manager.get_mention_partner_suggestions();
|
||||
var follower_suggestions = [];
|
||||
_.each(followers, function (follower) {
|
||||
if (follower.res_model === 'res.partner') {
|
||||
follower_suggestions.push({
|
||||
id: follower.res_id,
|
||||
name: follower.name,
|
||||
email: follower.email,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (follower_suggestions.length) {
|
||||
this.mention_suggestions.push(follower_suggestions);
|
||||
}
|
||||
_.each(prefetched_partners, function (partners) {
|
||||
self.mention_suggestions.push(_.filter(partners, function (partner) {
|
||||
return !_.findWhere(follower_suggestions, { id: partner.id });
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
load_more_messages: function () {
|
||||
this.fetch_and_render_thread(this.msg_ids, {force_fetch: true});
|
||||
},
|
||||
@@ -854,9 +885,9 @@ var Chatter = form_common.AbstractField.extend({
|
||||
internal_subtypes: this.options.internal_subtypes,
|
||||
is_log: options && options.is_log,
|
||||
record_name: this.record_name,
|
||||
get_channel_info: function () {
|
||||
return { res_id: self.res_id, res_model: self.model };
|
||||
},
|
||||
});
|
||||
this.composer.on('input_focused', this, function () {
|
||||
this.composer.mention_set_prefetched_partners(this.mention_suggestions || []);
|
||||
});
|
||||
this.composer.insertBefore(this.$('.o_mail_thread')).then(function () {
|
||||
// destroy existing composer
|
||||
|
||||
@@ -160,11 +160,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
this.searchview = new SearchView(this, dataset, view_id, {}, options);
|
||||
this.searchview.on('search_data', this, this.on_search);
|
||||
|
||||
this.composer = new ChatComposer(this, {
|
||||
get_channel_info: function () {
|
||||
return { channel_id: self.channel.id };
|
||||
},
|
||||
});
|
||||
this.composer = new ChatComposer(this);
|
||||
this.thread = new ChatThread(this, {
|
||||
display_help: true
|
||||
});
|
||||
@@ -200,6 +196,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
chat_manager.toggle_star_status(message_id);
|
||||
});
|
||||
this.composer.on('post_message', this, this.on_post_message);
|
||||
this.composer.on('input_focused', this, this.on_composer_input_focused);
|
||||
|
||||
var def1 = this.thread.prependTo(this.$('.o_mail_chat_content'));
|
||||
var def2 = this.composer.appendTo(this.$('.o_mail_chat_content'));
|
||||
@@ -425,6 +422,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
display_needactions: this.channel.display_needactions,
|
||||
messages_separator_position: this.messages_separator_position,
|
||||
squash_close_messages: this.channel.type !== 'static',
|
||||
display_empty_channel: !messages.length && !this.domain.length,
|
||||
display_no_match: !messages.length && this.domain.length,
|
||||
display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox",
|
||||
};
|
||||
},
|
||||
|
||||
@@ -581,6 +581,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
this.set_channel(channel);
|
||||
}
|
||||
},
|
||||
on_composer_input_focused: function () {
|
||||
var suggestions = chat_manager.get_mention_partner_suggestions(this.channel);
|
||||
this.composer.mention_set_prefetched_partners(suggestions);
|
||||
},
|
||||
|
||||
on_click_button_invite: function () {
|
||||
var title = _.str.sprintf(_t('Invite people to %s'), this.channel.name);
|
||||
|
||||
@@ -12,6 +12,19 @@ var Widget = require('web.Widget');
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var accented_letters_mapping = {
|
||||
'a': '[àáâãäå]',
|
||||
'ae': 'æ',
|
||||
'c': 'ç',
|
||||
'e': '[èéêë]',
|
||||
'i': '[ìíîï]',
|
||||
'n': 'ñ',
|
||||
'o': '[òóôõö]',
|
||||
'oe': 'œ',
|
||||
'u': '[ùúûűü]',
|
||||
'y': '[ýÿ]',
|
||||
};
|
||||
|
||||
// The MentionManager allows the Composer to register listeners. For each
|
||||
// listener, it detects if the user is currently typing a mention (starting by a
|
||||
// given delimiter). If so, if fetches mention suggestions and renders them. On
|
||||
@@ -63,16 +76,14 @@ var MentionManager = Widget.extend({
|
||||
};
|
||||
|
||||
// add the selected suggestion to the list
|
||||
var selection = this.active_listener.selection;
|
||||
if (selection.length && this.active_listener.sort_selection) {
|
||||
if (this.active_listener.selection.length) {
|
||||
// get mention matches (ordered by index in the text)
|
||||
var matches = this._get_match(text_input, this.active_listener);
|
||||
var index = get_mention_index(matches, this._get_selection_positions().start);
|
||||
selection.splice(index, 0, selected_suggestion);
|
||||
this.active_listener.selection.splice(index, 0, selected_suggestion);
|
||||
} else {
|
||||
selection = selection.concat([selected_suggestion]);
|
||||
this.active_listener.selection.push(selected_suggestion);
|
||||
}
|
||||
this._update_listener_selection(this.active_listener, selection);
|
||||
|
||||
// update input text, and reset dropdown
|
||||
var cursor_position = this._get_selection_positions().start;
|
||||
@@ -90,10 +101,7 @@ var MentionManager = Widget.extend({
|
||||
* @param {char} [delimiter] the mention delimiter
|
||||
* @param {function} [fetch_callback] the callback to fetch mention suggestions
|
||||
* @param {string} [model] the model used for redirection
|
||||
* @param {boolean} [notify_selection_changed] true to trigger an event when the selection changes
|
||||
* @param {string} [redirect_classname] the classname of the <a> wrapping the mention
|
||||
* @param {boolean} [sort_selection] true if the selection must be sorted
|
||||
* @param {string} [suggestions_template] the QWeb template to render suggestions
|
||||
*/
|
||||
register: function (listener) {
|
||||
this.listeners.push(_.extend(listener, {
|
||||
@@ -137,7 +145,6 @@ var MentionManager = Widget.extend({
|
||||
*/
|
||||
detect_delimiter: function () {
|
||||
var self = this;
|
||||
var options = this.options;
|
||||
var text_val = this.composer.$input.val();
|
||||
var cursor_position = this._get_selection_positions().start;
|
||||
var left_string = text_val.substring(0, cursor_position);
|
||||
@@ -146,7 +153,7 @@ var MentionManager = Widget.extend({
|
||||
var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))";
|
||||
var regex_start = new RegExp(pattern, "g");
|
||||
search_str = search_str.replace(/^\s\s*|^[\n\r]/g, '');
|
||||
if (regex_start.test(search_str) && search_str.length > options.min_length) {
|
||||
if (regex_start.test(search_str) && search_str.length > self.options.min_length) {
|
||||
search_str = search_str.replace(pattern, '');
|
||||
return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false;
|
||||
}
|
||||
@@ -164,19 +171,19 @@ var MentionManager = Widget.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (this.active_listener) {
|
||||
// start a timeout to fetch data with the current 'mention word'. The timer avoid to start
|
||||
// an RPC for each pushed key when the user is still typing.
|
||||
// The 'typing_speed' option should approach the time for a human to type a letter.
|
||||
clearTimeout(this.mention_fetch_timer);
|
||||
this.mention_fetch_timer = setTimeout(function () {
|
||||
$.when(listener.fetch_callback(self.mention_word, self.active_listener)).then(function (suggestions) {
|
||||
// start a timeout to fetch data with the current 'mention word'. The timer avoid to start
|
||||
// an RPC for each pushed key when the user is still typing.
|
||||
// The 'typing_speed' option should approach the time for a human to type a letter.
|
||||
clearTimeout(this.mention_fetch_timer);
|
||||
this.mention_fetch_timer = setTimeout(function () {
|
||||
if (self.active_listener) {
|
||||
$.when(listener.fetch_callback(self.mention_word)).then(function (suggestions) {
|
||||
self.set('mention_suggestions', suggestions);
|
||||
});
|
||||
}, this.options.typing_speed);
|
||||
} else {
|
||||
this.set('mention_suggestions', []); // close the dropdown
|
||||
}
|
||||
} else {
|
||||
self.set('mention_suggestions', []); // close the dropdown
|
||||
}
|
||||
}, this.options.typing_speed);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -201,7 +208,7 @@ var MentionManager = Widget.extend({
|
||||
}
|
||||
}
|
||||
if (to_remove.length) {
|
||||
self._update_listener_selection(listener, _.difference(mention_selection, to_remove));
|
||||
listener.selection = _.difference(mention_selection, to_remove);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -235,9 +242,8 @@ var MentionManager = Widget.extend({
|
||||
},
|
||||
|
||||
reset_selections: function () {
|
||||
var self = this;
|
||||
_.each(this.listeners, function (listener) {
|
||||
self._update_listener_selection(listener, []);
|
||||
listener.selection = [];
|
||||
});
|
||||
},
|
||||
|
||||
@@ -269,7 +275,7 @@ var MentionManager = Widget.extend({
|
||||
},
|
||||
_render_suggestions: function () {
|
||||
if (_.flatten(this.get('mention_suggestions')).length) {
|
||||
this.$el.html(QWeb.render(this.active_listener.suggestions_template, {
|
||||
this.$el.html(QWeb.render('mail.ChatComposer.MentionSuggestions', {
|
||||
suggestions: this.get('mention_suggestions'),
|
||||
}));
|
||||
this.$el
|
||||
@@ -282,12 +288,6 @@ var MentionManager = Widget.extend({
|
||||
this.open = false;
|
||||
}
|
||||
},
|
||||
_update_listener_selection: function (listener, selection) {
|
||||
listener.selection = selection;
|
||||
if (listener.notify_selection_changed) {
|
||||
this.trigger('selection_changed', listener);
|
||||
}
|
||||
},
|
||||
|
||||
// Cursor position and selection utils
|
||||
_get_selection_positions: function () {
|
||||
@@ -327,10 +327,8 @@ var Composer = Widget.extend({
|
||||
input_max_height: 150,
|
||||
input_min_height: 28,
|
||||
mention_fetch_limit: 8,
|
||||
get_channel_info: function () {},
|
||||
});
|
||||
this.context = this.options.context;
|
||||
this.get_channel_info = this.options.get_channel_info;
|
||||
|
||||
// Attachments
|
||||
this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
|
||||
@@ -343,19 +341,14 @@ var Composer = Widget.extend({
|
||||
delimiter: '@',
|
||||
fetch_callback: this.mention_fetch_partners.bind(this),
|
||||
model: 'res.partner',
|
||||
notify_selection_changed: true,
|
||||
redirect_classname: 'o_mail_redirect',
|
||||
sort_selection: true,
|
||||
suggestions_template: 'mail.ChatComposer.MentionPartnerSuggestions',
|
||||
});
|
||||
this.mention_manager.register({
|
||||
delimiter: '#',
|
||||
fetch_callback: this.mention_fetch_channels.bind(this),
|
||||
model: 'mail.channel',
|
||||
redirect_classname: 'o_channel_redirect',
|
||||
suggestions_template: 'mail.ChatComposer.MentionChannelSuggestions'
|
||||
});
|
||||
this.mention_manager.on('selection_changed', this, this.mention_render_selection);
|
||||
this.PartnerModel = new Model('res.partner');
|
||||
this.ChannelModel = new Model('mail.channel');
|
||||
},
|
||||
@@ -365,8 +358,10 @@ var Composer = Widget.extend({
|
||||
|
||||
this.$attachment_button = this.$(".o_composer_button_add_attachment");
|
||||
this.$attachments_list = this.$('.o_composer_attachments_list');
|
||||
this.$mention_partner_tags = this.$('.o_composer_mentioned_partners');
|
||||
this.$input = this.$('.o_composer_input');
|
||||
this.$input.focus(function () {
|
||||
self.trigger('input_focused');
|
||||
});
|
||||
this.resize_input();
|
||||
|
||||
// Attachments
|
||||
@@ -403,7 +398,7 @@ var Composer = Widget.extend({
|
||||
return $.when({
|
||||
content: this.mention_manager.generate_links(value),
|
||||
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
|
||||
partner_ids: _.pluck(this.mention_manager.get_listener_selection('@'), 'id'),
|
||||
partner_ids: _.uniq(_.pluck(this.mention_manager.get_listener_selection('@'), 'id')),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -607,19 +602,38 @@ var Composer = Widget.extend({
|
||||
});
|
||||
});
|
||||
},
|
||||
mention_fetch_partners: function (search, listener) {
|
||||
var kwargs = {
|
||||
channel: this.get_channel_info(),
|
||||
exclude: listener ? _.pluck(listener.selection, 'id') : [],
|
||||
limit: this.options.mention_fetch_limit,
|
||||
search: search,
|
||||
};
|
||||
return this.PartnerModel.call('get_mention_suggestions', kwargs);
|
||||
mention_fetch_partners: function (search) {
|
||||
var self = this;
|
||||
return $.when(this.mention_prefetched_partners).then(function (prefetched_partners) {
|
||||
// filter prefetched partners with the given search string
|
||||
var suggestions = [];
|
||||
var limit = self.options.mention_fetch_limit;
|
||||
var search_regexp = new RegExp(self.unaccent(search), 'i');
|
||||
_.each(prefetched_partners, function (partners) {
|
||||
if (limit > 0) {
|
||||
var filtered_partners = _.filter(partners, function (partner) {
|
||||
return partner.email && partner.email.search(search_regexp) !== -1 ||
|
||||
partner.name && self.unaccent(partner.name).search(search_regexp) !== -1;
|
||||
});
|
||||
if (filtered_partners.length) {
|
||||
suggestions.push(filtered_partners.slice(0, limit));
|
||||
limit -= filtered_partners.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!suggestions.length) {
|
||||
// no result found among prefetched partners, fetch other suggestions
|
||||
var kwargs = {
|
||||
limit: limit,
|
||||
search: search,
|
||||
};
|
||||
suggestions = self.PartnerModel.call('get_mention_suggestions', kwargs);
|
||||
}
|
||||
return suggestions;
|
||||
});
|
||||
},
|
||||
mention_render_selection: function (listener) {
|
||||
this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', {
|
||||
partners: listener.selection,
|
||||
}));
|
||||
mention_set_prefetched_partners: function (prefetched_partners) {
|
||||
this.mention_prefetched_partners = prefetched_partners;
|
||||
},
|
||||
|
||||
// Others
|
||||
@@ -629,6 +643,12 @@ var Composer = Widget.extend({
|
||||
focus: function () {
|
||||
this.$input.focus();
|
||||
},
|
||||
unaccent: function (str) {
|
||||
_.each(accented_letters_mapping, function (value, key) {
|
||||
str = str.replace(new RegExp(value, 'g'), key);
|
||||
});
|
||||
return str;
|
||||
},
|
||||
});
|
||||
|
||||
return Composer;
|
||||
|
||||
@@ -2,7 +2,6 @@ odoo.define('mail.ChatThread', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var time = require('web.time');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
@@ -49,7 +48,6 @@ var Thread = Widget.extend({
|
||||
display_order: ORDER.ASC,
|
||||
display_needactions: true,
|
||||
display_stars: true,
|
||||
default_username: _t('Anonymous'),
|
||||
display_document_link: true,
|
||||
display_avatar: true,
|
||||
squash_close_messages: true,
|
||||
@@ -67,7 +65,7 @@ var Thread = Widget.extend({
|
||||
// one are both comments wrote by the same author at the same minute
|
||||
var prev_msg;
|
||||
_.each(msgs, function (msg) {
|
||||
if (!prev_msg || (Math.abs(moment(msg.date).diff(prev_msg.date)) > 60000) ||
|
||||
if (!prev_msg || (Math.abs(msg.date.diff(prev_msg.date)) > 60000) ||
|
||||
prev_msg.message_type !== 'comment' || msg.message_type !== 'comment' ||
|
||||
(prev_msg.author_id[0] !== msg.author_id[0])) {
|
||||
msg.display_author = true;
|
||||
@@ -79,7 +77,7 @@ var Thread = Widget.extend({
|
||||
|
||||
this.$el.html(QWeb.render('mail.ChatThread', {
|
||||
messages: msgs,
|
||||
options: _.extend({}, this.options, options),
|
||||
options: options,
|
||||
ORDER: ORDER,
|
||||
}));
|
||||
},
|
||||
@@ -113,31 +111,19 @@ var Thread = Widget.extend({
|
||||
var msg = _.extend({}, message);
|
||||
|
||||
// Set the date in the browser timezone
|
||||
msg.date = moment(time.str_to_datetime(msg.date)).format('YYYY-MM-DD HH:mm:ss');
|
||||
var date = msg.date.format('YYYY-MM-DD');
|
||||
|
||||
// Compute displayed author name or email
|
||||
if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) {
|
||||
msg.mailto = msg.email_from;
|
||||
if (date === moment().format('YYYY-MM-DD')) {
|
||||
msg.day = _t("Today");
|
||||
msg.hour = msg.date.fromNow();
|
||||
} else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) {
|
||||
msg.day = _t("Yesterday");
|
||||
msg.hour = msg.date.format('hh:mm');
|
||||
} else {
|
||||
msg.displayed_author = msg.author_id && msg.author_id[1] ||
|
||||
msg.email_from ||
|
||||
this.options.default_username;
|
||||
msg.day = msg.date.format('LL');
|
||||
msg.hour = msg.date.format('hh:mm');
|
||||
}
|
||||
|
||||
// Compute the avatar_url
|
||||
if (msg.author_id && msg.author_id[0]) {
|
||||
msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
|
||||
} else if (msg.message_type === 'email') {
|
||||
msg.avatar_src = "/mail/static/src/img/email_icon.png";
|
||||
} else {
|
||||
msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg";
|
||||
}
|
||||
|
||||
// Compute url of attachments
|
||||
_.each(msg.attachment_ids, function(a) {
|
||||
a.url = '/web/content/' + a.id + '?download=true';
|
||||
});
|
||||
|
||||
return msg;
|
||||
},
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
.o_mail_chat {
|
||||
.o_mail_chat_sidebar {
|
||||
background-color: darken(@gray, 8%);
|
||||
color: lighten(@gray, 50%);
|
||||
color: lighten(@gray, 60%);
|
||||
opacity: 0.9;
|
||||
|
||||
.o_mail_chat_channel_item {
|
||||
@@ -214,16 +214,6 @@
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.o_composer_mentioned_partners {
|
||||
color: white;
|
||||
|
||||
> .badge {
|
||||
color: white;
|
||||
border: 1px solid white;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.o_composer_mentioned_partners, .o_composer_attachments_list {
|
||||
margin-top: 5px;
|
||||
.o_composer_attachments_list {
|
||||
margin-bottom: 5px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
-->
|
||||
<t t-name="mail.chatter.ChatComposer" t-extend="mail.ChatComposer">
|
||||
<!-- Insert information before the composer -->
|
||||
<t t-jquery=".o_composer_mentioned_partners" t-operation="before">
|
||||
<t t-jquery=".o_composer_attachments_list" t-operation="before">
|
||||
<!-- Internal note composer -->
|
||||
<t t-if="widget.options.is_log">
|
||||
<span class="o_chatter_composer_info">
|
||||
@@ -53,7 +53,7 @@
|
||||
</t>
|
||||
|
||||
<!-- Add a button to open the full composer dialog -->
|
||||
<t t-jquery=".o_composer_buttons" t-operation="append">
|
||||
<t t-jquery=".o_composer_button_add_attachment" t-operation="after">
|
||||
<button class="btn btn-sm btn-icon fa fa-pencil-square-o o_composer_button_full_composer" type="button"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mail.ChatComposer" class="o_chat_composer">
|
||||
<div class="o_composer_mentioned_partners"/>
|
||||
<div class="o_composer_attachments_list"/>
|
||||
<div class="o_composer">
|
||||
<textarea class="o_composer_input" placeholder="Write something"/>
|
||||
<textarea class="o_composer_input" placeholder="Write something..."/>
|
||||
<div class="btn-group o_composer_buttons">
|
||||
<button class="btn btn-sm btn-primary o_composer_button_send" type="button">Send</button>
|
||||
<button class="btn btn-sm btn-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/>
|
||||
<button class="btn btn-sm btn-icon fa fa-paperclip o_composer_button_add_attachment" type="button"/>
|
||||
<button class="btn btn-sm btn-primary o_composer_button_send" type="button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,16 +32,16 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatComposer.MentionPartnerSuggestions">
|
||||
<t t-name="mail.ChatComposer.MentionSuggestions">
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
||||
<t t-set="show_divider" t-value="false"/>
|
||||
<t t-foreach="suggestions" t-as="group">
|
||||
<t t-foreach="group" t-as="partner">
|
||||
<li t-if="show_divider && (partner_index === 0)" class="divider"/>
|
||||
<t t-foreach="group" t-as="suggestion">
|
||||
<li t-if="show_divider && (suggestion_index === 0)" class="divider"/>
|
||||
<t t-set="show_divider" t-value="true"/>
|
||||
<li class="o_mention_proposition" t-att-data-id="partner.id">
|
||||
<li class="o_mention_proposition" t-att-data-id="suggestion.id">
|
||||
<a href="#">
|
||||
<t t-esc="partner.name"/> <t t-if="partner.email">(<t t-esc="partner.email"/>)</t>
|
||||
<t t-esc="suggestion.name"/> <t t-if="suggestion.email">(<t t-esc="suggestion.email"/>)</t>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
@@ -50,26 +49,4 @@
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatComposer.MentionChannelSuggestions">
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
||||
<t t-set="show_divider" t-value="false"/>
|
||||
<t t-foreach="suggestions" t-as="group">
|
||||
<t t-foreach="group" t-as="channel">
|
||||
<li t-if="show_divider && (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>
|
||||
|
||||
@@ -9,9 +9,13 @@
|
||||
<t t-if="options.display_load_more && options.display_order == ORDER.DESC"
|
||||
t-call="mail.ChatThread.LoadMore"/>
|
||||
</t>
|
||||
<t t-if="!messages.length && options.display_help">
|
||||
<t t-if="options.display_empty_channel">
|
||||
<t t-call="mail.EmptyChannel" />
|
||||
</t>
|
||||
<t t-if="options.display_no_match">
|
||||
<div class="o_thread_title">No matches found</div>
|
||||
<div>No message matches your search. Try to change your search filters.</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- When a channel is empty-->
|
||||
@@ -30,11 +34,11 @@
|
||||
<t t-if="options.messages_separator_position == 'top'" t-call="mail.MessagesSeparator"/>
|
||||
<t t-set="current_day" t-value="0"/>
|
||||
<t t-foreach="messages" t-as="message">
|
||||
<div t-if="current_day != message.date.split(' ')[0]" class="o_thread_date_separator">
|
||||
<div t-if="current_day != message.day" class="o_thread_date_separator">
|
||||
<span class="o_thread_date">
|
||||
<t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LL')"/>
|
||||
<t t-esc="message.day"/>
|
||||
</span>
|
||||
<t t-set="current_day" t-value="message.date.split(' ')[0]"/>
|
||||
<t t-set="current_day" t-value="message.day"/>
|
||||
</div>
|
||||
|
||||
<t t-call="mail.ChatThread.Message"/>
|
||||
@@ -56,8 +60,8 @@
|
||||
t-att-src="message.avatar_src"
|
||||
class="o_thread_message_avatar"/>
|
||||
</t>
|
||||
<span t-if="!message.display_author" t-att-title="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LLL')" class="o_thread_message_side_date">
|
||||
<t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm')"/>
|
||||
<span t-if="!message.display_author" t-att-title="message.date" class="o_thread_message_side_date">
|
||||
<t t-esc="message.date.format('hh:mm')"/>
|
||||
</span>
|
||||
<i t-if="!message.display_author && options.display_stars && message.message_type != 'notification'"
|
||||
t-att-class="'fa o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')"
|
||||
@@ -83,8 +87,8 @@
|
||||
<t t-esc="message.displayed_author"/>
|
||||
</strong>
|
||||
|
||||
<small t-att-title="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LLL')">
|
||||
- <t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm A')"/>
|
||||
<small t-att-title="message.date">
|
||||
- <t t-esc="message.hour"/>
|
||||
</small>
|
||||
<t t-if="message.model && (message.model != 'mail.channel') && options.display_document_link">
|
||||
on <a class="o_mail_redirect" href="#" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id"><t t-esc="message.record_name"/></a>
|
||||
@@ -107,7 +111,7 @@
|
||||
</t>
|
||||
<t t-call="mail.ChatThread.MessageTracking"/>
|
||||
</t>
|
||||
<p t-if="message.subject" class="o_mail_subject">Subject: <t t-esc="message.subject"/></p>
|
||||
<p t-if="options.display_subject && message.subject && message.message_type != 'notification'" class="o_mail_subject">Subject: <t t-esc="message.subject"/></p>
|
||||
<t t-if="!(message.tracking_value_ids && message.tracking_value_ids.length > 0)">
|
||||
<t t-if="message.body_short">
|
||||
<div class="o_mail_body_short">
|
||||
@@ -115,7 +119,6 @@
|
||||
</div>
|
||||
<div class="o_mail_body_long">
|
||||
<t t-raw="message.body"/>
|
||||
<span class="oe_mail_expand"><a href="#">... read less</a></span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="! message.body_short">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#
|
||||
##############################################################################
|
||||
{
|
||||
'name': 'Worldline SIPS Payment Acquiring for online payments',
|
||||
'name': 'Worldline SIPS',
|
||||
'version': '1.0',
|
||||
'author': 'Eezee-It',
|
||||
'category': 'Hidden',
|
||||
|
||||
@@ -6,6 +6,7 @@ var PosDB = require('point_of_sale.DB');
|
||||
var devices = require('point_of_sale.devices');
|
||||
var core = require('web.core');
|
||||
var Model = require('web.DataModel');
|
||||
var formats = require('web.formats');
|
||||
var session = require('web.session');
|
||||
var time = require('web.time');
|
||||
var utils = require('web.utils');
|
||||
@@ -1070,8 +1071,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
}
|
||||
this.product = options.product;
|
||||
this.price = options.product.price;
|
||||
this.quantity = 1;
|
||||
this.quantityStr = '1';
|
||||
this.set_quantity(1);
|
||||
this.discount = 0;
|
||||
this.discountStr = '0';
|
||||
this.type = 'unit';
|
||||
@@ -1134,7 +1134,8 @@ exports.Orderline = Backbone.Model.extend({
|
||||
if(unit){
|
||||
if (unit.rounding) {
|
||||
this.quantity = round_pr(quant, unit.rounding);
|
||||
this.quantityStr = this.quantity.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10)));
|
||||
var decimals = Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10));
|
||||
this.quantityStr = formats.format_value(round_di(this.quantity, decimals), { type: 'float', digits: [69, decimals]});
|
||||
} else {
|
||||
this.quantity = round_pr(quant, 1);
|
||||
this.quantityStr = this.quantity.toFixed(0);
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
<field name="groups_id" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<header position="replace">
|
||||
<field name="state" invisible="1"/>
|
||||
</header>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="procurement_ids"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="product_id"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_delivered_updateable"]' position="replace"/>
|
||||
|
||||
@@ -324,6 +324,18 @@ class product_pricelist_item(osv.osv):
|
||||
class product_pricelist_item_new(models.Model):
|
||||
_inherit = "product.pricelist.item"
|
||||
|
||||
_applied_on_field_map = {
|
||||
'0_product_variant': 'product_id',
|
||||
'1_product': 'product_tmpl_id',
|
||||
'2_product_category': 'categ_id',
|
||||
}
|
||||
|
||||
_compute_price_field_map = {
|
||||
'fixed': ['fixed_price'],
|
||||
'percentage': ['percent_price'],
|
||||
'formula': ['price_discount', 'price_surcharge', 'price_round', 'price_min_margin', 'price_max_margin'],
|
||||
}
|
||||
|
||||
@api.one
|
||||
@api.depends('categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \
|
||||
'pricelist_id', 'percent_price', 'price_discount', 'price_surcharge')
|
||||
@@ -347,3 +359,16 @@ class product_pricelist_item_new(models.Model):
|
||||
#functional fields used for usability purposes
|
||||
name = Fields.Char(compute='_get_pricelist_item_name_price', string='Name', multi='item_name_price', help="Explicit rule name for this pricelist line.")
|
||||
price = Fields.Char(compute='_get_pricelist_item_name_price', string='Price', multi='item_name_price', help="Explicit rule name for this pricelist line.")
|
||||
|
||||
@api.onchange('applied_on')
|
||||
def _onchange_applied_on(self):
|
||||
for applied_on, field in self._applied_on_field_map.iteritems():
|
||||
if self.applied_on != applied_on:
|
||||
setattr(self, field, False)
|
||||
|
||||
@api.onchange('compute_price')
|
||||
def _onchange_compute_price(self):
|
||||
for compute_price, field in self._compute_price_field_map.iteritems():
|
||||
if self.compute_price != compute_price:
|
||||
for f in field:
|
||||
setattr(self, f, 0.0)
|
||||
|
||||
@@ -894,7 +894,12 @@ class product_product(osv.osv):
|
||||
def _get_image_variant(self, cr, uid, ids, name, args, context=None):
|
||||
result = dict.fromkeys(ids, False)
|
||||
for obj in self.browse(cr, uid, ids, context=context):
|
||||
result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
|
||||
if context.get('bin_size'):
|
||||
result[obj.id] = obj.image_variant
|
||||
else:
|
||||
result[obj.id] = tools.image_get_resized_images(obj.image_variant, return_big=True, avoid_resize_medium=True)[name]
|
||||
if not result[obj.id]:
|
||||
result[obj.id] = getattr(obj.product_tmpl_id, name)
|
||||
return result
|
||||
|
||||
def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
|
||||
|
||||
@@ -388,7 +388,7 @@ class task(osv.osv):
|
||||
project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
|
||||
if project and project.partner_id:
|
||||
return {'value': {'partner_id': project.partner_id.id}}
|
||||
return {'value': {'partner_id': False}}
|
||||
return {}
|
||||
|
||||
def onchange_user_id(self, cr, uid, ids, user_id, context=None):
|
||||
vals = {}
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
<field name="project_id" ref="project.project_project_data"/>
|
||||
<field name="name">You can set a deadline on a task</field>
|
||||
<field name="stage_id" ref="project_stage_data_1"/>
|
||||
<field name="date_deadline" eval="'2015-%s-01' %(datetime.now().month+1)"/>
|
||||
<field name="date_deadline" eval="datetime.now()+timedelta(days=30)"/>
|
||||
</record>
|
||||
|
||||
<record id="project_task_data_14" model="project.task">
|
||||
|
||||
@@ -149,6 +149,8 @@ class PurchaseOrder(models.Model):
|
||||
|
||||
picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\
|
||||
help="This will determine picking type of incoming shipment")
|
||||
default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\
|
||||
help="Technical field used to display the Drop Ship Address")
|
||||
group_id = fields.Many2one('procurement.group', string="Procurement Group")
|
||||
|
||||
@api.model
|
||||
@@ -207,6 +209,11 @@ class PurchaseOrder(models.Model):
|
||||
self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id
|
||||
return {}
|
||||
|
||||
@api.onchange('picking_type_id')
|
||||
def _onchange_picking_type_id(self):
|
||||
if self.picking_type_id.default_location_dest_id.usage != 'customer':
|
||||
self.dest_address_id = False
|
||||
|
||||
@api.multi
|
||||
def action_rfq_send(self):
|
||||
'''
|
||||
@@ -853,6 +860,7 @@ class ProcurementOrder(models.Model):
|
||||
'price_unit': price_unit,
|
||||
'procurement_ids': [(4, procurement.id)]
|
||||
})
|
||||
break
|
||||
if not po_line:
|
||||
vals = procurement._prepare_purchase_order_line(po, supplier)
|
||||
self.env['purchase.order.line'].create(vals)
|
||||
|
||||
@@ -251,6 +251,8 @@
|
||||
<group>
|
||||
<field name="date_planned"/>
|
||||
<field name="picking_type_id" domain="[('code','=','incoming')]" options="{'no_create': True}" context="{'special_shortened_wh_name': True}" groups="stock.group_locations"/>
|
||||
<field name="dest_address_id" groups="stock.group_locations" attrs="{'invisible': [('default_location_dest_id_usage', '!=', 'customer')], 'required': [('default_location_dest_id_usage', '=', 'customer')]}"/>
|
||||
<field name="default_location_dest_id_usage" invisible="1"/>
|
||||
<field name="incoterm_id"/>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
<label for="id" string="Customer portal"/>
|
||||
<div>
|
||||
<div>
|
||||
<field name="module_website_portal" class="oe_inline" widget="upgrade_boolean"/>
|
||||
<field name="module_website_portal" class="oe_inline" />
|
||||
<label for="module_website_portal"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="module_website_sale_digital" class="oe_inline" widget="upgrade_boolean"/>
|
||||
<field name="module_website_sale_digital" class="oe_inline" />
|
||||
<label for="module_website_sale_digital"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-1
@@ -485,7 +485,9 @@ class SaleOrderLine(models.Model):
|
||||
def _get_invoice_qty(self):
|
||||
"""
|
||||
Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. Note
|
||||
that this is the case only if the refund is generated from the SO.
|
||||
that this is the case only if the refund is generated from the SO and that is intentional: if
|
||||
a refund made would automatically decrease the invoiced quantity, then there is a risk of reinvoicing
|
||||
it automatically, which may not be wanted at all. That's why the refund has to be created from the SO
|
||||
"""
|
||||
for line in self:
|
||||
qty_invoiced = 0.0
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
<field name="partner_id" domain="[('customer','=',True)]" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/>
|
||||
<field name="partner_invoice_id" groups="sale.group_delivery_invoice_address" context="{'default_type':'invoice'}"/>
|
||||
<field name="partner_shipping_id" groups="sale.group_delivery_invoice_address" context="{'default_type':'delivery'}"/>
|
||||
<field name="project_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" groups="sale.group_analytic_accounting"/>
|
||||
<field name="project_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" groups="analytic.group_analytic_accounting"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_order" invisible="1"/>
|
||||
@@ -361,7 +361,7 @@
|
||||
<filter string="My Orders" domain="[('user_id','=',uid)]" name="my_sale_orders_filter"/>
|
||||
<separator/>
|
||||
<filter string="Quotations" name="draft" domain="[('state','in',('draft','sent'))]" help="Sales Order that haven't yet been confirmed"/>
|
||||
<filter string="Sales" name="sales" domain="[('state','in',('progress','Done'))]"/>
|
||||
<filter string="Sales" name="sales" domain="[('state','in',('sale','done'))]"/>
|
||||
<separator/>
|
||||
<filter string="To Invoice" domain="[('invoice_status','=','to invoice')]"/>
|
||||
<filter string="Upselling" domain="[('invoice_status','=','upselling')]"/>
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="groups_id" eval="[(4,ref('base.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- TODO: check if we need this -->
|
||||
|
||||
@@ -67,10 +67,6 @@ class AccountInvoiceLine(osv.Model):
|
||||
sale_layout_cat_id = openerp.fields.Many2one('sale_layout.category', string='Section')
|
||||
categ_sequence = openerp.fields.Integer(related='sale_layout_cat_id.sequence',
|
||||
string='Layout Sequence', store=True)
|
||||
_defaults = {
|
||||
'categ_sequence': 0
|
||||
}
|
||||
|
||||
|
||||
class SaleOrder(osv.Model):
|
||||
_inherit = 'sale.order'
|
||||
@@ -100,10 +96,6 @@ class SaleOrderLine(osv.Model):
|
||||
# Store is intentionally set in order to keep the "historic" order.
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'categ_sequence': 0
|
||||
}
|
||||
|
||||
_order = 'order_id, categ_sequence, sale_layout_cat_id, sequence, id'
|
||||
|
||||
def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
|
||||
|
||||
@@ -134,7 +134,7 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
@api.onchange('product_uom_qty')
|
||||
def _onchange_product_uom_qty(self):
|
||||
if self.state == 'sale' and self.product_id.type != 'service' and self.product_uom_qty < self._origin.product_uom_qty:
|
||||
if self.state == 'sale' and self.product_id.type in ['product', 'consu'] and self.product_uom_qty < self._origin.product_uom_qty:
|
||||
warning_mess = {
|
||||
'title': _('Ordered quantity decreased!'),
|
||||
'message' : _('You are decreasing the ordered quantity! Do not forget to manually update the delivery order if needed.'),
|
||||
@@ -159,10 +159,14 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _get_delivered_qty(self):
|
||||
"""Computes the delivered quantity on sale order lines, based on done stock moves related to its procurements
|
||||
"""
|
||||
self.ensure_one()
|
||||
super(SaleOrderLine, self)._get_delivered_qty()
|
||||
qty = 0.0
|
||||
for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped):
|
||||
#Note that we don't decrease quantity for customer returns on purpose: these are exeptions that must be treated manually. Indeed,
|
||||
#modifying automatically the delivered quantity may trigger an automatic reinvoicing (refund) of the SO, which is definitively not wanted
|
||||
if move.location_dest_id.usage == "customer":
|
||||
qty += self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom)
|
||||
return qty
|
||||
|
||||
@@ -1920,7 +1920,7 @@ class stock_move(osv.osv):
|
||||
auto_join=True, help="Location where the system will stock the finished products."),
|
||||
|
||||
'partner_id': fields.many2one('res.partner', 'Destination Address ', states={'done': [('readonly', True)]}, help="Optional address where goods are to be delivered, specifically used for allotment"),
|
||||
|
||||
'picking_partner_id': fields.related('picking_id', 'partner_id', type='many2one', relation='res.partner', string='Transfer Destination Address'),
|
||||
|
||||
'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True, copy=False),
|
||||
'move_orig_ids': fields.one2many('stock.move', 'move_dest_id', 'Original Move', help="Optional: previous stock move when chaining them", select=True),
|
||||
|
||||
@@ -1338,6 +1338,7 @@
|
||||
<group name="destination_grp" string="Destination" groups="stock.group_locations">
|
||||
<field name="location_dest_id" />
|
||||
<field name="partner_id" context="{'contact_display':'partner'}" />
|
||||
<field name="picking_partner_id" context="{'contact_display':'partner'}"/>
|
||||
<field name="move_dest_id" groups="base.group_no_one" readonly="1"/>
|
||||
</group>
|
||||
<group name="quants_grp" string="Reserved Quants" colspan="2" groups="base.group_no_one" attrs="{'invisible': [('state', '=', 'done')]}">
|
||||
|
||||
@@ -1411,7 +1411,7 @@ class ExportFormat(object):
|
||||
|
||||
Model = request.session.model(model)
|
||||
context = dict(request.context or {}, **params.get('context', {}))
|
||||
ids = ids or Model.search(domain, 0, False, False, context)
|
||||
ids = ids or Model.search(domain, 0, False, False, context=context)
|
||||
|
||||
if not request.env[model]._is_an_ordinary_table():
|
||||
fields = [field for field in fields if field['name'] != 'id']
|
||||
|
||||
@@ -992,8 +992,8 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
this.searchview.destroy();
|
||||
}
|
||||
var $header = $('<div/>').addClass('o_modal_header').appendTo(this.$el);
|
||||
var $buttons = $('<div/>').addClass('o_search_options').appendTo($header);
|
||||
var $pager = $('<div/>').addClass('o_pager').appendTo($header);
|
||||
var $buttons = $('<div/>').addClass('o_search_options').appendTo($header);
|
||||
this.searchview = new SearchView(this, this.dataset, false, search_defaults, {$buttons: $buttons});
|
||||
this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
|
||||
if (self.initial_ids) {
|
||||
|
||||
@@ -1177,7 +1177,7 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{
|
||||
ids = value;
|
||||
}
|
||||
new Model(column.relation)
|
||||
.call('name_get', [ids, this.dataset.context]).done(function (names) {
|
||||
.call('name_get', [ids, this.dataset.get_context()]).done(function (names) {
|
||||
// FIXME: nth horrible hack in this poor listview
|
||||
record.set(column.id + '__display',
|
||||
_(names).pluck(1).join(', '));
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
.o_modal_header {
|
||||
.o-webclient-padding(@top: 10px, @bottom: 10px);
|
||||
.clearfix();
|
||||
|
||||
.o_search_options {
|
||||
display: inline-block;
|
||||
|
||||
@@ -122,8 +122,8 @@ var FieldTextHtmlSimple = widget.extend({
|
||||
if (this.options['style-inline']) {
|
||||
transcoder.class_to_style(this.$content);
|
||||
transcoder.font_to_img(this.$content);
|
||||
this.internal_set_value(this.$content.html());
|
||||
}
|
||||
this.internal_set_value(this.$content.html());
|
||||
},
|
||||
destroy_content: function () {
|
||||
$(".oe-view-manager-content").off("scroll");
|
||||
|
||||
@@ -2320,6 +2320,12 @@ function summernote_paste (event) {
|
||||
|
||||
var html = clipboardData.getData("text/html");
|
||||
var $node = $('<div/>').html(html);
|
||||
// if copying source did not provide html, default to plain text
|
||||
if(!html) {
|
||||
$node.text(clipboardData.getData("text/plain")).html(function(_, html){
|
||||
return html.replace(/\r?\n/g,'<br>');
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
remove undesirable tag
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="text-center o_web_settings_dashboard_apps">
|
||||
<i class="fa fa-cog fa-4x text-muted o_browse_apps" style="cursor: pointer;"></i>
|
||||
<div class="o_web_settings_dashboard_header">
|
||||
<t t-set="installed_apps" t-value="widget.data.installed_apps"/>
|
||||
<t t-if="installed_apps">
|
||||
<t t-esc="installed_apps"></t>
|
||||
<t t-if="installed_apps == 1">Installed App</t>
|
||||
|
||||
@@ -1032,7 +1032,7 @@ var ViewEditor = Widget.extend({
|
||||
render_new_field :function( result ) {
|
||||
var self = this;
|
||||
var action = {
|
||||
context: {'default_model_id': result.id, 'manual': true, 'module' : result.model},
|
||||
context: {'default_model_id': result.id, 'module' : result.model},
|
||||
res_model: "ir.model.fields",
|
||||
views: [[false, 'form']],
|
||||
type: 'ir.actions.act_window',
|
||||
|
||||
@@ -38,9 +38,9 @@ class Website(openerp.addons.web.controllers.main.Home):
|
||||
else:
|
||||
first_menu = main_menu.child_id and main_menu.child_id[0]
|
||||
if first_menu:
|
||||
if not (first_menu.url.startswith(('/page/', '/?', '/#')) or (first_menu.url=='/')):
|
||||
if first_menu.url and (not (first_menu.url.startswith(('/page/', '/?', '/#')) or (first_menu.url == '/'))):
|
||||
return request.redirect(first_menu.url)
|
||||
if first_menu.url.startswith('/page/'):
|
||||
if first_menu.url and first_menu.url.startswith('/page/'):
|
||||
return request.registry['ir.http'].reroute(first_menu.url)
|
||||
return self.page(page)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<template id="assets_common" inherit_id="web.assets_common" name="Website assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script groups="base.group_website_designer" type="text/javascript" src="/website/static/src/js/website.tour.rte.js"></script>
|
||||
<script type="text/javascript" src="/website/static/src/js/website.tour.rte.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<data>
|
||||
<record id="crm.model_crm_lead" model="ir.model">
|
||||
<field name="website_form_default_field_id" ref="crm.field_crm_lead_description" />
|
||||
<field name="website_form_access">True</field>
|
||||
<field name="website_form_label">Create a lead</field>
|
||||
</record>
|
||||
|
||||
<function model="ir.model.fields" name="formbuilder_whitelist">
|
||||
<value>crm.lead</value>
|
||||
<value eval="[
|
||||
'contact_name',
|
||||
'description',
|
||||
'email_from',
|
||||
'name',
|
||||
'partner_name',
|
||||
'phone',
|
||||
]"/>
|
||||
</function>
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="action_open_website" model="ir.actions.act_url">
|
||||
<field name="name">Website Contact Form</field>
|
||||
<field name="target">self</field>
|
||||
|
||||
@@ -15,13 +15,11 @@ class WebsiteForm(http.Controller):
|
||||
@http.route('/website_form/<string:model_name>', type='http', auth="public", methods=['POST'], website=True)
|
||||
def website_form(self, model_name, **kwargs):
|
||||
model_record = request.env['ir.model'].search([('model', '=', model_name), ('website_form_access', '=', True)])
|
||||
|
||||
if not model_record:
|
||||
return json.dumps(False)
|
||||
|
||||
try:
|
||||
data = self.extract_data(model_record, ** kwargs)
|
||||
|
||||
# If we encounter an issue while extracting data
|
||||
except ValidationError, e:
|
||||
# I couldn't find a cleaner way to pass data to an exception
|
||||
@@ -96,7 +94,7 @@ class WebsiteForm(http.Controller):
|
||||
'custom': '', # Custom fields values
|
||||
}
|
||||
|
||||
authorized_fields = model.sudo().get_authorized_fields();
|
||||
authorized_fields = model.sudo()._get_form_writable_fields()
|
||||
error_fields = []
|
||||
|
||||
for field_name, field_value in kwargs.items():
|
||||
@@ -183,7 +181,7 @@ class WebsiteForm(http.Controller):
|
||||
def insert_attachment(self, model, id_record, files):
|
||||
orphan_attachment_ids = []
|
||||
record = model.env[model.model].browse(id_record)
|
||||
authorized_fields = model.sudo().get_authorized_fields()
|
||||
authorized_fields = model.sudo()._get_form_writable_fields()
|
||||
for file in files:
|
||||
custom_field = file.field_name not in authorized_fields
|
||||
attachment_value = {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import itertools
|
||||
|
||||
from openerp import tools
|
||||
from openerp import models, fields, api
|
||||
|
||||
@@ -16,18 +18,36 @@ class website_form_model(models.Model):
|
||||
website_form_default_field_id = fields.Many2one('ir.model.fields', 'Field for custom form data', domain="[('model', '=', model), ('ttype', '=', 'text')]", help="Specify the field which will contain meta and custom form fields datas.")
|
||||
website_form_label = fields.Char("Label for form action", help="Form action label. Ex: crm.lead could be 'Send an e-mail' and project.issue could be 'Create an Issue'.")
|
||||
|
||||
def _all_inherited_model_ids(self):
|
||||
return list(itertools.chain(
|
||||
[self.id],
|
||||
*(m._all_inherited_model_ids() for m in self.inherited_model_ids)
|
||||
))
|
||||
|
||||
def all_inherited_model_ids(self):
|
||||
return [self.id] + [m.all_inherited_model_ids() for m in self.inherited_model_ids]
|
||||
def _get_form_writable_fields(self):
|
||||
"""
|
||||
Restriction of "authorized fields" (fields which can be used in the
|
||||
form builders) to fields which have actually been opted into form
|
||||
builders and are writable. By default no field is writable by the
|
||||
form builder.
|
||||
"""
|
||||
excluded = {
|
||||
field.name
|
||||
for field in self.env['ir.model.fields'].sudo().search([
|
||||
('model_id', 'in', self._all_inherited_model_ids()),
|
||||
('website_form_blacklisted', '=', True)
|
||||
])
|
||||
}
|
||||
return {
|
||||
k: v for k, v in self.get_authorized_fields().iteritems()
|
||||
if k not in excluded
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def get_authorized_fields(self):
|
||||
model = self.env[self.model]
|
||||
fields_get = model.fields_get()
|
||||
|
||||
for elem in self.env['ir.model.fields'].search([('model_id', 'in', self.all_inherited_model_ids())]):
|
||||
if elem.website_form_blacklisted:
|
||||
fields_get.pop(elem.name, None)
|
||||
for key, val in model._inherits.iteritems():
|
||||
fields_get.pop(val,None)
|
||||
|
||||
@@ -49,4 +69,44 @@ class website_form_model_fields(models.Model):
|
||||
_name = 'ir.model.fields'
|
||||
_inherit = 'ir.model.fields'
|
||||
|
||||
website_form_blacklisted = fields.Boolean('Blacklisted in web forms', help='Blacklist this field for web forms')
|
||||
def init(self, cr):
|
||||
# set all existing unset website_form_blacklisted fields to ``true``
|
||||
# (so that we can use it as a whitelist rather than a blacklist)
|
||||
cr.execute('UPDATE ir_model_fields'
|
||||
' SET website_form_blacklisted=true'
|
||||
' WHERE website_form_blacklisted IS NULL')
|
||||
# add an SQL-level default value on website_form_blacklisted to that
|
||||
# pure-SQL ir.model.field creations (e.g. in _field_create) generate
|
||||
# the right default value for a whitelist (aka fields should be
|
||||
# blacklisted by default)
|
||||
cr.execute('ALTER TABLE ir_model_fields '
|
||||
' ALTER COLUMN website_form_blacklisted SET DEFAULT true')
|
||||
|
||||
@api.model
|
||||
def formbuilder_whitelist(self, model, fields):
|
||||
"""
|
||||
:param str model: name of the model on which to whitelist fields
|
||||
:param list(str) fields: list of fields to whitelist on the model
|
||||
:return: nothing of import
|
||||
"""
|
||||
# postgres does *not* like ``in [EMPTY TUPLE]`` queries
|
||||
if not fields: return False
|
||||
|
||||
# only allow users who can change the website structure
|
||||
if not self.env['res.users'].has_group('base.group_website_designer'):
|
||||
return False
|
||||
|
||||
# the ORM only allows writing on custom fields and will trigger a
|
||||
# registry reload once that's happened. We want to be able to
|
||||
# whitelist non-custom fields and the registry reload absolutely
|
||||
# isn't desirable, so go with a method and raw SQL
|
||||
self.env.cr.execute(
|
||||
"UPDATE ir_model_fields"
|
||||
" SET website_form_blacklisted=false"
|
||||
" WHERE model=%s AND name in %s", (model, tuple(fields)))
|
||||
return True
|
||||
|
||||
website_form_blacklisted = fields.Boolean(
|
||||
'Blacklisted in web forms', default=True, select=True, # required=True,
|
||||
help='Blacklist this field for web forms'
|
||||
)
|
||||
|
||||
@@ -384,7 +384,7 @@ class WebsiteForum(http.Controller):
|
||||
def post_save(self, forum, post, **kwargs):
|
||||
if 'post_name' in kwargs and not kwargs.get('post_name').strip():
|
||||
return request.website.render('website.http_error', {'status_code': _('Bad Request'), 'status_message': _('Title should not be empty.')})
|
||||
post_tags = forum._tag_to_write_vals(kwargs.get('post_tag', ''))
|
||||
post_tags = forum._tag_to_write_vals(kwargs.get('post_tags', ''))
|
||||
vals = {
|
||||
'tag_ids': post_tags,
|
||||
'name': kwargs.get('post_name'),
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 9.0\n"
|
||||
"Project-Id-Version: Odoo Server 9.0c\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-10-09 09:20+0000\n"
|
||||
"PO-Revision-Date: 2015-10-09 09:20+0000\n"
|
||||
"POT-Creation-Date: 2015-11-26 14:10+0000\n"
|
||||
"PO-Revision-Date: 2015-11-26 14:10+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -222,13 +222,19 @@ msgid "<i class=\"text-muted\">awarded users</i>"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: code:addons/website_forum/models/forum.py:419
|
||||
#: code:addons/website_forum/models/forum.py:483
|
||||
#, python-format
|
||||
msgid "<p>A new answer for <i>%s</i> has been posted. <a href=\"%s/forum/%s/question/%s\">Click here to access the post.</a></p>"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: code:addons/website_forum/models/forum.py:425
|
||||
#: code:addons/website_forum/models/forum.py:499
|
||||
#, python-format
|
||||
msgid "<p>A new question <i>%s</i> has been asked on %s and require your validation. <a href=\"%s/forum/%s/question/%s\">Click here to access the question.</a></p>"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: code:addons/website_forum/models/forum.py:489
|
||||
#, python-format
|
||||
msgid "<p>A new question <i>%s</i> has been asked on %s. <a href=\"%s/forum/%s/question/%s\">Click here to access the question.</a></p>"
|
||||
msgstr ""
|
||||
@@ -1621,11 +1627,6 @@ msgstr ""
|
||||
msgid "Last activity date"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.model.fields,field_description:website_forum.field_res_users_calendar_last_notif_ack
|
||||
msgid "Last notification marked as read from base Calendar"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.header
|
||||
msgid "Last updated:"
|
||||
@@ -2058,7 +2059,7 @@ msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.header_footer_custom
|
||||
msgid "Q&A"
|
||||
msgid "Q&amp;A"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<template id="header_footer_custom" inherit_id="website.footer_default"
|
||||
name="Footer Questions Link">
|
||||
<xpath expr="//ul[@id='products']" position="inside">
|
||||
<li><a t-attf-href="/forum/%(website_forum.forum_help)d">Q&A</a></li>
|
||||
<li><a t-attf-href="/forum/%(website_forum.forum_help)d">Q&amp;A</a></li>
|
||||
<li><a href="/forum">Forums</a></li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -16,10 +16,23 @@
|
||||
<field name="action_id" ref="action_open_website"/>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
</data>
|
||||
<data>
|
||||
<record id="hr_recruitment.model_hr_applicant" model="ir.model">
|
||||
<field name="website_form_default_field_id" ref="hr_recruitment.field_hr_applicant_description" />
|
||||
<field name="website_form_access">True</field>
|
||||
<field name="website_form_label">Apply for a Job</field>
|
||||
</record>
|
||||
<function model="ir.model.fields" name="formbuilder_whitelist">
|
||||
<value>hr.applicant</value>
|
||||
<value eval="[
|
||||
'description',
|
||||
'email_from',
|
||||
'partner_name',
|
||||
'partner_phone',
|
||||
'job_id',
|
||||
'department_id',
|
||||
]"/>
|
||||
</function>
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<data>
|
||||
<record id="project_issue.model_project_issue" model="ir.model">
|
||||
<field name="website_form_default_field_id" ref="project_issue.field_project_issue_description" />
|
||||
<field name="website_form_access">True</field>
|
||||
<field name="website_form_label">Create an issue</field>
|
||||
</record>
|
||||
<!-- Template to blacklist field
|
||||
<record id="project_issue.field_project_issue_{field_name}" model="ir.model.fields">
|
||||
<field name="website_form_blacklisted">True</field>
|
||||
</record>
|
||||
-->
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -68,9 +68,9 @@ options.registry.newsletter_popup = mass_mailing_common.extend({
|
||||
});
|
||||
|
||||
web_editor.Class.include({
|
||||
edit: function () {
|
||||
this._super();
|
||||
start: function () {
|
||||
$('body').on('click','#edit_dialog',_.bind(this.edit_dialog, this.rte.editor));
|
||||
return this._super();
|
||||
},
|
||||
save : function() {
|
||||
var $target = $('#wrapwrap').find('#o_newsletter_popup');
|
||||
|
||||
@@ -53,7 +53,7 @@ if(!$('.o_website_quote').length) {
|
||||
'submit #accept': 'submitForm',
|
||||
},
|
||||
initSignature: function(ev){
|
||||
this.$("#signature").empty().jSignature({'decor-color' : '#D1D0CE'});
|
||||
this.$("#signature").empty().jSignature({'decor-color' : '#D1D0CE', 'color': '#000', 'background-color': '#fff'});
|
||||
this.empty_sign = this.$("#signature").jSignature("getData",'image');
|
||||
},
|
||||
clearSignature: function(ev){
|
||||
|
||||
@@ -146,9 +146,9 @@ $('.oe_website_sale').each(function () {
|
||||
clickwatch(function(){
|
||||
|
||||
$dom_optional.each(function(){
|
||||
$(this).find('.js_quantity').text(value);
|
||||
product_ids.push($(this).find('span[data-product-id]').data('product-id'));
|
||||
});
|
||||
if (isNaN(value)) value = 0;
|
||||
$input.data('update_change', true);
|
||||
|
||||
ajax.jsonRpc("/shop/cart/update_json", 'call', {
|
||||
|
||||
@@ -31,4 +31,9 @@
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="report_picking_portal" inherit_id="stock.report_picking" groups="base.group_portal">
|
||||
<xpath expr="//tr[@t-foreach='o.move_lines']" position="attributes">
|
||||
<attribute name="t-foreach">o.move_lines.sudo()</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -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
|
||||
|
||||
@@ -16749,7 +16749,7 @@ msgstr ""
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_payment_sips
|
||||
msgid "Worldline SIPS Payment Acquiring for online payments"
|
||||
msgid "Worldline SIPS"
|
||||
msgstr ""
|
||||
|
||||
#. module: base
|
||||
|
||||
@@ -93,7 +93,7 @@ class ir_model(osv.osv):
|
||||
|
||||
_defaults = {
|
||||
'model': 'x_',
|
||||
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
|
||||
'state': 'manual',
|
||||
}
|
||||
|
||||
def _check_model_name(self, cr, uid, ids, context=None):
|
||||
@@ -177,10 +177,8 @@ class ir_model(osv.osv):
|
||||
def create(self, cr, user, vals, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if context and context.get('manual'):
|
||||
vals['state']='manual'
|
||||
res = super(ir_model,self).create(cr, user, vals, context)
|
||||
if vals.get('state','base')=='manual':
|
||||
if vals.get('state','manual')=='manual':
|
||||
# setup models; this automatically adds model in registry
|
||||
self.pool.setup_models(cr, partial=(not self.pool.ready))
|
||||
# update database schema
|
||||
@@ -264,7 +262,7 @@ class ir_model_fields(osv.osv):
|
||||
'selection': "",
|
||||
'domain': "[]",
|
||||
'name': 'x_',
|
||||
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
|
||||
'state': 'manual',
|
||||
'on_delete': 'set null',
|
||||
'field_description': '',
|
||||
'selectable': 1,
|
||||
@@ -444,14 +442,12 @@ class ir_model_fields(osv.osv):
|
||||
vals['model'] = model_data.model
|
||||
if context is None:
|
||||
context = {}
|
||||
if context and context.get('manual',False):
|
||||
vals['state'] = 'manual'
|
||||
if vals.get('ttype', False) == 'selection':
|
||||
if not vals.get('selection',False):
|
||||
raise UserError(_('For selection fields, the Selection Options must be given!'))
|
||||
self._check_selection(cr, user, vals['selection'], context=context)
|
||||
res = super(ir_model_fields,self).create(cr, user, vals, context)
|
||||
if vals.get('state','base') == 'manual':
|
||||
if vals.get('state','manual') == 'manual':
|
||||
if not vals['name'].startswith('x_'):
|
||||
raise UserError(_("Custom fields must have a name that starts with 'x_' !"))
|
||||
|
||||
@@ -479,8 +475,6 @@ class ir_model_fields(osv.osv):
|
||||
def write(self, cr, user, ids, vals, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if context and context.get('manual',False):
|
||||
vals['state'] = 'manual'
|
||||
|
||||
#For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
|
||||
if 'serialization_field_id' in vals or 'name' in vals:
|
||||
@@ -523,7 +517,7 @@ class ir_model_fields(osv.osv):
|
||||
raise UserError(_('Can only rename one field at a time!'))
|
||||
if vals['name'] in obj._fields:
|
||||
raise UserError(_('Cannot rename field to %s, because that field already exists!') % vals['name'])
|
||||
if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'):
|
||||
if vals.get('state', 'manual') == 'manual' and not vals['name'].startswith('x_'):
|
||||
raise UserError(_('New field name must still start with x_ , because it is a custom field!'))
|
||||
if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
|
||||
raise ValueError('Invalid character in column name')
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Fields">
|
||||
<field context="{'manual':True}" name="field_id">
|
||||
<field name="field_id">
|
||||
<tree string="Fields Description">
|
||||
<field name="name"/>
|
||||
<field name="field_description"/>
|
||||
@@ -203,7 +203,7 @@
|
||||
<field name="name">Models</field>
|
||||
<field name="res_model">ir.model</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="context">{'manual':True}</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="view_id" ref="view_model_tree"/>
|
||||
</record>
|
||||
<menuitem action="action_model_model" id="ir_model_model_menu" parent="next_id_9"/>
|
||||
@@ -351,7 +351,7 @@
|
||||
<field name="name">Fields</field>
|
||||
<field name="res_model">ir.model.fields</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="context">{'manual':True}</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="view_id" ref="view_model_fields_tree"/>
|
||||
</record>
|
||||
<menuitem action="action_model_fields" id="ir_model_model_fields" parent="base.next_id_9"/>
|
||||
|
||||
@@ -130,11 +130,13 @@ class ir_translation_import_cursor(object):
|
||||
AND irt.type = ti.type
|
||||
AND irt.module = ti.module
|
||||
AND irt.name = ti.name
|
||||
AND (ti.type IN ('field', 'help') OR irt.src = ti.src)
|
||||
AND ( ti.type NOT IN ('model', 'view')
|
||||
OR (ti.type = 'model' AND ti.res_id = irt.res_id)
|
||||
OR (ti.type = 'view' AND (irt.res_id IS NULL OR ti.res_id = irt.res_id))
|
||||
)
|
||||
AND (
|
||||
(ti.type = 'model' AND ti.res_id = irt.res_id AND irt.src = ti.src)
|
||||
OR (ti.type = 'view' AND (irt.res_id IS NULL OR ti.res_id = irt.res_id) AND irt.src = ti.src)
|
||||
OR (ti.type = 'field')
|
||||
OR (ti.type = 'help')
|
||||
OR (ti.type NOT IN ('model', 'view', 'field', 'help') AND irt.src = ti.src)
|
||||
)
|
||||
"""
|
||||
|
||||
# Step 2: update existing (matching) translations
|
||||
@@ -536,41 +538,46 @@ class ir_translation(osv.osv):
|
||||
def insert_missing(self, field, records):
|
||||
""" Insert missing translations for `field` on `records`. """
|
||||
records = records.with_context(lang=None)
|
||||
external_ids = records.get_external_id() # if no xml_id, empty string
|
||||
if callable(field.translate):
|
||||
# insert missing translations for each term in src
|
||||
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value)
|
||||
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s
|
||||
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value, module)
|
||||
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s, %(module)s
|
||||
FROM res_lang l
|
||||
WHERE l.active AND NOT EXISTS (
|
||||
SELECT 1 FROM ir_translation
|
||||
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND src=%(src)s
|
||||
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND src=%(src)s AND module=%(module)s
|
||||
);
|
||||
"""
|
||||
for record in records:
|
||||
module = external_ids[record.id].split('.')[0]
|
||||
src = record[field.name] or None
|
||||
for term in set(field.get_trans_terms(src)):
|
||||
self._cr.execute(query, {
|
||||
'name': "%s,%s" % (field.model_name, field.name),
|
||||
'res_id': record.id,
|
||||
'src': term,
|
||||
'module': module
|
||||
})
|
||||
else:
|
||||
# insert missing translations for src
|
||||
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value)
|
||||
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s
|
||||
query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value, module)
|
||||
SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s, %(module)s
|
||||
FROM res_lang l
|
||||
WHERE l.active AND l.code != 'en_US' AND NOT EXISTS (
|
||||
SELECT 1 FROM ir_translation
|
||||
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s
|
||||
WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND module=%(module)s
|
||||
);
|
||||
UPDATE ir_translation SET src=%(src)s
|
||||
WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s;
|
||||
WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s AND module=%(module)s;
|
||||
"""
|
||||
for record in records:
|
||||
module = external_ids[record.id].split('.')[0]
|
||||
self._cr.execute(query, {
|
||||
'name': "%s,%s" % (field.model_name, field.name),
|
||||
'res_id': record.id,
|
||||
'src': record[field.name] or None,
|
||||
'module': module
|
||||
})
|
||||
self.clear_caches()
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ class res_partner(osv.Model, format_address):
|
||||
'use_parent_address': fields.boolean('Use Company Address', help="Select this if you want to set company's address information for this contact"),
|
||||
'company_id': fields.many2one('res.company', 'Company', select=1),
|
||||
'color': fields.integer('Color Index'),
|
||||
'user_ids': fields.one2many('res.users', 'partner_id', 'Users'),
|
||||
'user_ids': fields.one2many('res.users', 'partner_id', 'Users', auto_join=True),
|
||||
'contact_address': fields.function(_address_display, type='char', string='Complete Address'),
|
||||
|
||||
# technical field used for managing commercial fields
|
||||
|
||||
@@ -119,7 +119,7 @@ class TranslationToolsTestCase(unittest.TestCase):
|
||||
terms = []
|
||||
source = """<div>
|
||||
stuff before
|
||||
<div translation="off">Do not translate this</div>
|
||||
<div t-translation="off">Do not translate this</div>
|
||||
stuff after
|
||||
</div>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
|
||||
@@ -61,9 +61,9 @@ def report_configuration():
|
||||
user = config['db_user'] or os.environ.get('PGUSER', 'default')
|
||||
_logger.info('database: %s@%s:%s', user, host, port)
|
||||
|
||||
def rm_pid_file():
|
||||
def rm_pid_file(main_pid):
|
||||
config = openerp.tools.config
|
||||
if not openerp.evented and config['pidfile']:
|
||||
if config['pidfile'] and main_pid == os.getpid():
|
||||
try:
|
||||
os.unlink(config['pidfile'])
|
||||
except OSError:
|
||||
@@ -76,10 +76,10 @@ def setup_pid_file():
|
||||
"""
|
||||
config = openerp.tools.config
|
||||
if not openerp.evented and config['pidfile']:
|
||||
pid = os.getpid()
|
||||
with open(config['pidfile'], 'w') as fd:
|
||||
pidtext = "%d" % (os.getpid())
|
||||
fd.write(pidtext)
|
||||
atexit.register(rm_pid_file)
|
||||
fd.write(str(pid))
|
||||
atexit.register(rm_pid_file, pid)
|
||||
|
||||
def export_translation():
|
||||
config = openerp.tools.config
|
||||
|
||||
@@ -305,6 +305,7 @@ class WebRequest(object):
|
||||
# case, the request cursor is unusable. Rollback transaction to create a new one.
|
||||
if self._cr:
|
||||
self._cr.rollback()
|
||||
self.env.clear()
|
||||
result = self.endpoint(*a, **kw)
|
||||
if isinstance(result, Response) and result.is_qweb:
|
||||
# Early rendering of lazy responses to benefit from @service_model.check protection
|
||||
|
||||
@@ -215,7 +215,7 @@ class XMLTranslator(object):
|
||||
if (
|
||||
isinstance(node, SKIPPED_ELEMENT_TYPES) or
|
||||
node.tag in SKIPPED_ELEMENTS or
|
||||
node.get("translation", "").strip() == "off" or
|
||||
node.get("t-translation", "").strip() == "off" or
|
||||
node.tag == "attribute" and node.get("name") not in TRANSLATED_ATTRS
|
||||
):
|
||||
# do not translate the contents of the node
|
||||
|
||||
Reference in New Issue
Block a user