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