[MERGE] forward port of branch 9.0 up to 14799a9
This commit is contained in:
@@ -137,28 +137,29 @@ class AccountInvoice(models.Model):
|
||||
if self.payment_move_line_ids:
|
||||
info = {'title': _('Less Payment'), 'outstanding': False, 'content': []}
|
||||
for payment in self.payment_move_line_ids:
|
||||
#we don't take into account the movement created due to a change difference
|
||||
if payment.currency_id and payment.move_id.rate_diff_partial_rec_id:
|
||||
continue
|
||||
if self.type in ('out_invoice', 'in_refund'):
|
||||
amount = sum([p.amount for p in payment.matched_debit_ids if p.debit_move_id in self.move_id.line_ids])
|
||||
amount_currency = sum([p.amount_currency for p in payment.matched_debit_ids if p.debit_move_id in self.move_id.line_ids])
|
||||
elif self.type in ('in_invoice', 'out_refund'):
|
||||
amount = sum([p.amount for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids])
|
||||
amount_currency = sum([p.amount_currency for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids])
|
||||
#we don't take into account the movement created due to a change difference
|
||||
if payment.currency_id and payment.move_id.rate_diff_partial_rec_id:
|
||||
continue
|
||||
# get the payment value in invoice currency
|
||||
if payment.currency_id and amount_currency != 0:
|
||||
currency_id = payment.currency_id
|
||||
amount_to_show = -amount_currency
|
||||
else:
|
||||
self.with_context(date=payment.date)
|
||||
amount_to_show = payment.company_id.currency_id.compute(-amount, self.currency_id)
|
||||
currency_id = payment.company_id.currency_id
|
||||
amount_to_show = -amount
|
||||
info['content'].append({
|
||||
'name': payment.name,
|
||||
'journal_name': payment.journal_id.name,
|
||||
'amount': amount_to_show,
|
||||
'currency': self.currency_id.symbol,
|
||||
'digits': [69, self.currency_id.decimal_places],
|
||||
'position': self.currency_id.position,
|
||||
'currency': currency_id.symbol,
|
||||
'digits': [69, currency_id.decimal_places],
|
||||
'position': currency_id.position,
|
||||
'date': payment.date,
|
||||
'payment_id': payment.id,
|
||||
'move_id': payment.move_id.id,
|
||||
|
||||
@@ -228,18 +228,6 @@ class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
_description = 'Partner'
|
||||
|
||||
@api.multi
|
||||
def onchange_state(self, state_id):
|
||||
res = super(ResPartner, self).onchange_state(state_id=state_id)
|
||||
res['value']['property_account_position_id'] = self.env['account.fiscal.position']._get_fpos_by_region(
|
||||
country_id=self.env.context.get('country_id'), state_id=state_id, zipcode=self.env.context.get('zip'))
|
||||
return res
|
||||
|
||||
@api.onchange('country_id', 'zip')
|
||||
def _onchange_country_id(self):
|
||||
self.property_account_position_id = self.env['account.fiscal.position']._get_fpos_by_region(
|
||||
country_id=self.country_id.id, state_id=self.state_id.id, zipcode=self.zip)
|
||||
|
||||
@api.multi
|
||||
def _credit_debit_get(self):
|
||||
tables, where_clause, where_params = self.env['account.move.line']._query_get()
|
||||
|
||||
@@ -13,6 +13,7 @@ access_account_tax,account.tax,model_account_tax,account.group_account_invoice,1
|
||||
access_account_account_template,account.account.template,model_account_account_template,account.group_account_manager,1,1,1,1
|
||||
access_account_chart_template,account.chart.template,model_account_chart_template,account.group_account_manager,1,1,1,1
|
||||
access_account_tax_template,account.tax.template,model_account_tax_template,account.group_account_manager,1,1,1,1
|
||||
access_account_bank_statement_group_invoice,account.bank.statement.group.invoice,model_account_bank_statement,account.group_account_invoice,1,0,0,0
|
||||
access_account_bank_statement,account.bank.statement,model_account_bank_statement,account.group_account_user,1,1,1,1
|
||||
access_account_bank_statement_line,account.bank.statement.line,model_account_bank_statement_line,account.group_account_user,1,1,1,1
|
||||
access_account_analytic_line_manager,account.analytic.line manager,model_account_analytic_line,account.group_account_manager,1,0,0,0
|
||||
@@ -65,6 +66,7 @@ access_account_financial_report_invoice,account.financial.report invoice,model_a
|
||||
access_account_account_tax,account.account.tag,model_account_account_tag,account.group_account_user,1,0,0,0
|
||||
access_account_account_tax_user,account.account.tag,model_account_account_tag,account.group_account_invoice,1,0,0,0
|
||||
access_account_operation_template,account.operation.template,model_account_operation_template,account.group_account_user,1,1,1,1
|
||||
access_account_partial_reconcile_group_invoice,account.partial.reconcile.group.invoice,model_account_partial_reconcile,account.group_account_invoice,1,1,1,0
|
||||
access_account_partial_reconcile,account.partial.reconcile,model_account_partial_reconcile,account.group_account_user,1,1,1,1
|
||||
access_account_payment_method,account.payment.method,model_account_payment_method,account.group_account_invoice,1,1,1,1
|
||||
access_account_payment,account.payment,model_account_payment,account.group_account_invoice,1,1,1,1
|
||||
|
||||
|
@@ -10,7 +10,7 @@ It must be used as a dependency for modules that provide country-specific check
|
||||
The check settings are located in the accounting journals configuration page.
|
||||
""",
|
||||
'website': 'https://www.odoo.com/page/accounting',
|
||||
'depends' : ['account_accountant'],
|
||||
'depends' : ['account'],
|
||||
'data': [
|
||||
'data/check_printing.xml',
|
||||
'views/account_journal_dashboard_view.xml',
|
||||
|
||||
@@ -96,7 +96,9 @@ class AuthSignupHome(openerp.addons.web.controllers.main.Home):
|
||||
values = dict((key, qcontext.get(key)) for key in ('login', 'name', 'password'))
|
||||
assert any([k for k in values.values()]), "The form was not properly filled in."
|
||||
assert values.get('password') == qcontext.get('confirm_password'), "Passwords do not match; please retype them."
|
||||
values['lang'] = request.lang
|
||||
supported_langs = [lang['code'] for lang in request.registry['res.lang'].search_read(request.cr, openerp.SUPERUSER_ID, [], ['code'])]
|
||||
if request.lang in supported_langs:
|
||||
values['lang'] = request.lang
|
||||
self._signup_with_values(qcontext.get('token'), values)
|
||||
request.cr.commit()
|
||||
|
||||
|
||||
@@ -63,7 +63,10 @@ class res_partner(osv.Model):
|
||||
continue # no signup token, no user, thus no signup url!
|
||||
|
||||
fragment = dict()
|
||||
if action:
|
||||
base = '/web#'
|
||||
if action == '/mail/view':
|
||||
base = '/mail/view?'
|
||||
elif action:
|
||||
fragment['action'] = action
|
||||
if view_type:
|
||||
fragment['view_type'] = view_type
|
||||
@@ -72,10 +75,10 @@ class res_partner(osv.Model):
|
||||
if model:
|
||||
fragment['model'] = model
|
||||
if res_id:
|
||||
fragment['id'] = res_id
|
||||
fragment['res_id'] = res_id
|
||||
|
||||
if fragment:
|
||||
query['redirect'] = '/web#' + werkzeug.url_encode(fragment)
|
||||
query['redirect'] = base + werkzeug.url_encode(fragment)
|
||||
|
||||
res[partner.id] = urljoin(base_url, "/web/%s?%s" % (route, werkzeug.url_encode(query)))
|
||||
|
||||
@@ -148,7 +151,7 @@ class res_partner(osv.Model):
|
||||
if partner.user_ids:
|
||||
res['login'] = partner.user_ids[0].login
|
||||
else:
|
||||
res['email'] = partner.email or ''
|
||||
res['email'] = res['login'] = partner.email or ''
|
||||
return res
|
||||
|
||||
class res_users(osv.Model):
|
||||
|
||||
@@ -11,7 +11,7 @@ This module installs the base for IBAN (International Bank Account Number) bank
|
||||
The ability to extract the correctly represented local accounts from IBAN accounts
|
||||
with a single statement.
|
||||
""",
|
||||
'depends': ['account_accountant'],
|
||||
'depends': ['account'],
|
||||
'demo': ['demo/iban_demo.xml'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
|
||||
@@ -1011,8 +1011,10 @@ class calendar_event(osv.Model):
|
||||
return value
|
||||
start = datetime.strptime(start, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
value['stop_date'] = (start + timedelta(hours=duration)).strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
value['stop'] = (start + timedelta(hours=duration)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
value['stop_datetime'] = (start + timedelta(hours=duration)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
value['start_date'] = start.strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
value['start'] = start.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
return {'value': value}
|
||||
|
||||
def onchange_dates(self, cr, uid, ids, fromtype, start=False, end=False, checkallday=False, allday=False, context=None):
|
||||
@@ -1038,16 +1040,6 @@ class calendar_event(osv.Model):
|
||||
value['stop_datetime'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
value['stop'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
else:
|
||||
if fromtype == 'start' and start:
|
||||
start = datetime.strptime(start, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
value['start_date'] = datetime.strftime(start, DEFAULT_SERVER_DATE_FORMAT)
|
||||
value['start'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
if fromtype == 'stop' and end:
|
||||
end = datetime.strptime(end, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
value['stop_date'] = datetime.strftime(end, DEFAULT_SERVER_DATE_FORMAT)
|
||||
value['stop'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
return {'value': value}
|
||||
|
||||
def new_invitation_token(self, cr, uid, record, partner_id):
|
||||
|
||||
@@ -528,7 +528,7 @@
|
||||
|
||||
<group>
|
||||
<label for="contact_name"/>
|
||||
<div>
|
||||
<div class="o_row">
|
||||
<field name="contact_name"/>
|
||||
<field name="title" placeholder="Title" domain="[]" options='{"no_open": True}'/>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +76,8 @@ class StockPicking(models.Model):
|
||||
if self.carrier_id and self.carrier_id.delivery_type not in ['fixed', 'base_on_rule'] and self.carrier_id.shipping_enabled:
|
||||
self.send_to_shipper()
|
||||
self._add_delivery_cost_to_so()
|
||||
elif self.carrier_id and self.carrier_id.delivery_type in ['fixed', 'base_on_rule'] and self.carrier_id.shipping_enabled:
|
||||
self.carrier_price = sum(self.sale_id.order_line.filtered(lambda r: r.is_delivery).mapped('price_unit'))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
<!-- Discussion groups, done in 2 steps to remove creator from followers -->
|
||||
<record model="mail.channel" id="channel_1">
|
||||
<field name="name">Best Sales Practices</field>
|
||||
<field name="name">sales</field>
|
||||
<field name="description">Discussion about best sales practices and deals.</field>
|
||||
</record>
|
||||
<record model="mail.channel" id="channel_2">
|
||||
<field name="name">Board meetings</field>
|
||||
<field name="name">board-meetings</field>
|
||||
<field name="description">Board meetings, budgets, strategic plans</field>
|
||||
</record>
|
||||
<record model="mail.channel" id="channel_3">
|
||||
<field name="name">R&D</field>
|
||||
<field name="name">rd</field>
|
||||
<field name="description">Research and development discussion group</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import uuid
|
||||
from openerp import _, api, fields, models, modules, tools
|
||||
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from openerp.exceptions import UserError
|
||||
from openerp.osv import expression
|
||||
|
||||
from openerp.addons.bus.models.bus_presence import AWAY_TIMER
|
||||
|
||||
@@ -141,14 +142,17 @@ class Channel(models.Model):
|
||||
|
||||
@api.multi
|
||||
def action_follow(self):
|
||||
return self.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.env.user.partner_id.id})]})
|
||||
self.ensure_one()
|
||||
channel_partner = self.mapped('channel_last_seen_partner_ids').filtered(lambda cp: cp.partner_id == self.env.user.partner_id)
|
||||
if not channel_partner:
|
||||
return self.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.env.user.partner_id.id})]})
|
||||
|
||||
@api.multi
|
||||
def action_unfollow(self):
|
||||
result = self.write({'channel_partner_ids': [(3, self.env.user.partner_id.id)]})
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), self.channel_info()[0])
|
||||
notification = _('<div class="o_mail_notification">left <a href="#" data-channel-id="%s">#%s</a></div>') % (self.uuid, self.name,)
|
||||
self.message_post(body=notification, message_type="comment", subtype="mail.mt_comment")
|
||||
notification = _('<div class="o_mail_notification">left <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")
|
||||
return result
|
||||
|
||||
|
||||
@@ -485,7 +489,7 @@ class Channel(models.Model):
|
||||
@api.multi
|
||||
def channel_join_and_get_info(self):
|
||||
self.ensure_one()
|
||||
notification = _('<div class="o_mail_notification">joined <a href="#" data-channel-id="%s">#%s</a></div>') % (self.uuid, self.name,)
|
||||
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")
|
||||
self.action_follow()
|
||||
|
||||
@@ -509,7 +513,22 @@ class Channel(models.Model):
|
||||
'channel_partner_ids': [(4, self.env.user.partner_id.id)]
|
||||
})
|
||||
channel_info = new_channel.channel_info()[0]
|
||||
notification = _('<div class="o_mail_notification">created <a href="#" data-channel-id="%s">#%s</a></div>') % (new_channel.uuid, 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")
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
|
||||
return channel_info
|
||||
|
||||
@api.model
|
||||
def get_mention_suggestions(self, search, limit=8):
|
||||
""" Return 'limit'-first channels' id, name and public fields such that the name matches a
|
||||
'search' string. Exclude channels of type chat (DM), and private channels the current
|
||||
user isn't registered to. """
|
||||
domain = expression.AND([
|
||||
[('name', 'ilike', search)],
|
||||
[('channel_type', '!=', 'chat')],
|
||||
expression.OR([
|
||||
[('public', '!=', 'private')],
|
||||
[('channel_partner_ids', 'in', [self.env.user.partner_id.id])]
|
||||
])
|
||||
])
|
||||
return self.search_read(domain, ['id', 'name', 'public'], limit=limit)
|
||||
|
||||
@@ -85,7 +85,7 @@ class Message(models.Model):
|
||||
# recipients
|
||||
partner_ids = fields.Many2many('res.partner', string='Recipients')
|
||||
needaction_partner_ids = fields.Many2many(
|
||||
'res.partner', 'mail_message_res_partner_needaction_rel', string='Need Action')
|
||||
'res.partner', 'mail_message_res_partner_needaction_rel', string='Partners with Need Action')
|
||||
needaction = fields.Boolean(
|
||||
'Need Action', compute='_get_needaction', search='_search_needaction',
|
||||
help='Need Action')
|
||||
@@ -155,6 +155,19 @@ class Message(models.Model):
|
||||
self = self.sudo()
|
||||
return self.write({'needaction_partner_ids': [(4, pid) for pid in partner_ids]})
|
||||
|
||||
@api.multi
|
||||
def mark_all_as_read(self):
|
||||
user_id = self.env.user.partner_id.id
|
||||
|
||||
# possibly horribly inefficient method:
|
||||
# it does one db request for the search, and one for each message in
|
||||
# the result set to remove the current user from the relation.
|
||||
# unread_messages = self.search([('needaction_partner_ids', 'in', user_id)])
|
||||
# unread_messages.write({'needaction_partner_ids': [(3, user_id)]})
|
||||
|
||||
# a much faster way to do this is in pure sql:
|
||||
self.env.cr.execute("DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s", [(user_id,)])
|
||||
|
||||
@api.multi
|
||||
def set_message_done(self, partner_ids=None):
|
||||
if not partner_ids:
|
||||
|
||||
@@ -9,7 +9,7 @@ var session = require('web.session');
|
||||
var web_client = require('web.web_client');
|
||||
|
||||
var _t = core._t;
|
||||
var LIMIT = 20;
|
||||
var LIMIT = 100;
|
||||
var preview_msg_max_size = 50;
|
||||
|
||||
var MessageModel = new Model('mail.message', session.context);
|
||||
@@ -22,6 +22,7 @@ var channels = [];
|
||||
var emojis = [];
|
||||
var emoji_substitutions = {};
|
||||
var needaction_counter = 0;
|
||||
var discuss_ids = {};
|
||||
|
||||
// Message and channel manipulation helpers
|
||||
//----------------------------------------------------------------------------------
|
||||
@@ -34,7 +35,7 @@ function add_message (data, options) {
|
||||
if (!msg) {
|
||||
msg = make_message(data);
|
||||
// Keep the array ordered by date when inserting the new message
|
||||
messages.splice(_.sortedIndex(messages, msg, 'date'), 0, msg);
|
||||
messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
|
||||
if (options.channel_id) {
|
||||
var channel = _.findWhere(channels, {id: options.channel_id});
|
||||
if (channel.hidden) {
|
||||
@@ -356,6 +357,17 @@ var chat_manager = {
|
||||
});
|
||||
},
|
||||
|
||||
mark_all_as_read: function () {
|
||||
MessageModel.call('mark_all_as_read', [[]], {}).then(function (ids) {
|
||||
_.each(messages, function (msg) {
|
||||
remove_message_from_channel("channel_inbox", msg);
|
||||
chat_manager.bus.trigger('update_message', msg);
|
||||
});
|
||||
needaction_counter = 0;
|
||||
chat_manager.bus.trigger('update_needaction', needaction_counter);
|
||||
});
|
||||
},
|
||||
|
||||
get_channels: function () {
|
||||
return _.clone(channels);
|
||||
},
|
||||
@@ -376,6 +388,10 @@ var chat_manager = {
|
||||
return needaction_counter;
|
||||
},
|
||||
|
||||
get_discuss_ids: function () {
|
||||
return discuss_ids;
|
||||
},
|
||||
|
||||
detach_channel: function (channel) {
|
||||
return ChannelModel.call("channel_minimize", [channel.uuid, true]);
|
||||
},
|
||||
@@ -454,6 +470,10 @@ function init () {
|
||||
});
|
||||
});
|
||||
|
||||
var ir_model = new Model("ir.model.data");
|
||||
var load_menu_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]);
|
||||
var load_action_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]);
|
||||
|
||||
bus.on('notification', null, function (notification) {
|
||||
var model = notification[0][1];
|
||||
if (model === 'ir.needaction') {
|
||||
@@ -498,7 +518,11 @@ function init () {
|
||||
}
|
||||
});
|
||||
|
||||
return $.when(load_channels, load_emojis).then(function () {
|
||||
return $.when(load_menu_id, load_action_id, load_channels, load_emojis).then(function (menu_id, action_id) {
|
||||
discuss_ids = {
|
||||
menu_id: menu_id,
|
||||
action_id: action_id,
|
||||
};
|
||||
bus.start_polling();
|
||||
return chat_manager;
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ var Followers = form_common.AbstractField.extend({
|
||||
// event: click on 'edit_subtype(pencil)' button to edit subscription
|
||||
this.$el.on('click', '.o_edit_subtype', self.on_edit_subtype);
|
||||
this.$el.on('click', '.o_remove_follower', self.on_remove_follower);
|
||||
this.$el.on('click', 'a[data-partner]', self.on_follower_clicked);
|
||||
this.$el.on('click', '.o_mail_redirect', self.on_click_redirect);
|
||||
},
|
||||
|
||||
on_edit_subtype: function (event) {
|
||||
@@ -167,24 +167,11 @@ var Followers = form_common.AbstractField.extend({
|
||||
}
|
||||
},
|
||||
|
||||
on_follower_clicked: function (event) {
|
||||
on_click_redirect: function (event) {
|
||||
event.preventDefault();
|
||||
var partner_id = $(event.target).data('partner');
|
||||
var state = {
|
||||
'model': 'res.partner',
|
||||
'id': partner_id,
|
||||
'title': this.record_name
|
||||
};
|
||||
web_client.action_manager.do_push_state(state);
|
||||
var action = {
|
||||
type:'ir.actions.act_window',
|
||||
view_type: 'form',
|
||||
view_mode: 'form',
|
||||
res_model: 'res.partner',
|
||||
views: [[false, 'form']],
|
||||
res_id: partner_id,
|
||||
};
|
||||
this.do_action(action);
|
||||
var res_id = $(event.target).data('oe-id');
|
||||
var res_model = $(event.target).data('oe-model');
|
||||
this.trigger('redirect', res_model, res_id);
|
||||
},
|
||||
|
||||
read_value: function () {
|
||||
@@ -697,7 +684,6 @@ var Chatter = form_common.AbstractField.extend({
|
||||
template: 'mail.Chatter',
|
||||
|
||||
events: {
|
||||
"click .o_mail_redirect": "on_click_redirect",
|
||||
"click .o_chatter_button_new_message": "on_open_composer_new_message",
|
||||
"click .o_chatter_button_log_note": "on_open_composer_log_note",
|
||||
},
|
||||
@@ -717,6 +703,7 @@ var Chatter = form_common.AbstractField.extend({
|
||||
this.followers = this.field_manager.fields.message_follower_ids;
|
||||
if (this.followers) {
|
||||
this.$('.o_chatter_topbar').append(this.followers.$el);
|
||||
this.followers.on('redirect', this, this.on_redirect);
|
||||
}
|
||||
|
||||
this.thread = new ChatThread(this, {
|
||||
@@ -728,6 +715,8 @@ var Chatter = form_common.AbstractField.extend({
|
||||
this.thread.on('toggle_star_status', this, function (message_id) {
|
||||
chat_manager.toggle_star_status(message_id);
|
||||
});
|
||||
this.thread.on('redirect', this, this.on_redirect);
|
||||
this.thread.on('redirect_to_channel', this, this.on_channel_redirect);
|
||||
|
||||
var def1 = this._super.apply(this, arguments);
|
||||
var def2 = this.thread.appendTo(this.$el);
|
||||
@@ -778,15 +767,31 @@ var Chatter = form_common.AbstractField.extend({
|
||||
}
|
||||
},
|
||||
|
||||
on_click_redirect: function (event) {
|
||||
on_channel_redirect: function (channel_id) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
var def;
|
||||
var channel = chat_manager.get_channel(channel_id);
|
||||
// If not registered to 'channel' yet, do it
|
||||
if (channel.id !== channel_id) {
|
||||
def = chat_manager.join_channel(channel_id);
|
||||
}
|
||||
$.when(def).then(function () {
|
||||
// Execute Discuss client action with 'channel' as default channel
|
||||
var discuss_ids = chat_manager.get_discuss_ids();
|
||||
self.do_action(discuss_ids.action_id, {active_id: channel_id});
|
||||
});
|
||||
},
|
||||
|
||||
on_redirect: function (res_model, res_id) {
|
||||
event.preventDefault();
|
||||
this.do_action({
|
||||
type:'ir.actions.act_window',
|
||||
view_type: 'form',
|
||||
view_mode: 'form',
|
||||
res_model: $(event.target).data('oe-model'),
|
||||
res_model: res_model,
|
||||
views: [[false, 'form']],
|
||||
res_id: $(event.target).data('oe-id'),
|
||||
res_id: res_id,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ var Model = require('web.Model');
|
||||
var pyeval = require('web.pyeval');
|
||||
var SearchView = require('web.SearchView');
|
||||
var session = require('web.session');
|
||||
var Sidebar = require('web.Sidebar');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
@@ -119,11 +118,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
|
||||
},
|
||||
|
||||
init: function(parent, action) {
|
||||
init: function(parent, action, options) {
|
||||
this._super.apply(this, arguments);
|
||||
this.action_manager = parent;
|
||||
this.domain = [];
|
||||
this.action = action;
|
||||
this.options = options || {};
|
||||
this.channels_scrolltop = {};
|
||||
},
|
||||
|
||||
@@ -138,24 +138,13 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
};
|
||||
var dataset = new data.DataSetSearch(this, 'mail.message');
|
||||
var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
|
||||
var default_channel_id = this.action.context.active_id ||
|
||||
var default_channel_id = this.options.active_id ||
|
||||
this.action.context.active_id ||
|
||||
this.action.params.default_active_id ||
|
||||
'channel_inbox';
|
||||
this.searchview = new SearchView(this, dataset, view_id, {}, options);
|
||||
this.searchview.on('search_data', this, this.on_search);
|
||||
|
||||
this.sidebar = new Sidebar(this, {
|
||||
sections: [
|
||||
{name: 'action', label: _t('Action')},
|
||||
],
|
||||
items: {
|
||||
action: [
|
||||
{label: _t('Unsubscribe'), classname: 'o_mail_chat_button_unsubscribe', callback: this.on_click_button_unsubscribe},
|
||||
{label: _t('Settings'), classname: 'o_mail_chat_button_settings', callback: this.on_click_button_settings},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
this.composer = new ChatComposer(this, {
|
||||
get_channel_info: function () {
|
||||
return { channel_id: self.channel.id };
|
||||
@@ -169,13 +158,22 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
this.$buttons.find('button').css({display:"inline-block"});
|
||||
this.$buttons.on('click', '.o_mail_chat_button_invite', this.on_click_button_invite);
|
||||
this.$buttons.on('click', '.o_mail_chat_button_detach', this.on_click_button_detach);
|
||||
this.$buttons.on('click', '.o_mail_chat_button_unsubscribe', this.on_click_button_unsubscribe);
|
||||
this.$buttons.on('click', '.o_mail_chat_button_settings', this.on_click_button_settings);
|
||||
this.$buttons.on('click', '.o_mail_toggle_channels', function () {
|
||||
self.$('.o_mail_chat_sidebar').slideToggle(200);
|
||||
});
|
||||
this.$buttons.on('click', '.o_mail_chat_button_mark_read', chat_manager.mark_all_as_read);
|
||||
|
||||
|
||||
this.thread.on('redirect', this, this.on_redirect);
|
||||
this.thread.on('redirect_to_channel', this, function (channel_id) {
|
||||
this.set_channel(chat_manager.get_channel(channel_id));
|
||||
var channel = chat_manager.get_channel(channel_id);
|
||||
if (channel.id === channel_id) {
|
||||
this.set_channel(channel);
|
||||
} else {
|
||||
chat_manager.join_channel(channel_id);
|
||||
}
|
||||
});
|
||||
this.thread.on('load_more_messages', this, this.load_more_messages);
|
||||
this.thread.on('mark_as_read', this, function (message_id) {
|
||||
@@ -189,11 +187,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
var def1 = this.thread.prependTo(this.$('.o_mail_chat_content'));
|
||||
var def2 = this.composer.appendTo(this.$('.o_mail_chat_content'));
|
||||
var def3 = this.searchview.appendTo($("<div>"));
|
||||
var def4 = this.sidebar.appendTo($("<div>"));
|
||||
|
||||
this.render_sidebar();
|
||||
|
||||
return $.when(def1, def2, def3, def4)
|
||||
return $.when(def1, def2, def3)
|
||||
.then(this.set_channel.bind(this, chat_manager.get_channel(default_channel_id)))
|
||||
.then(function () {
|
||||
chat_manager.bus.on('new_message', self, self.on_new_message);
|
||||
@@ -219,7 +216,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
self.last_search_val = _.escape(request.term);
|
||||
self.do_search_channel(self.last_search_val).done(function(result){
|
||||
result.push({
|
||||
'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"'+self.last_search_val+'"</em>'),
|
||||
'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"#'+self.last_search_val+'"</em>'),
|
||||
'value': '_create',
|
||||
});
|
||||
response(result);
|
||||
@@ -301,15 +298,24 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
}
|
||||
var new_channel_scrolltop = this.channels_scrolltop[channel.id];
|
||||
this.channel = channel;
|
||||
|
||||
// Update control panel
|
||||
this.set("title", channel.name);
|
||||
// Hide 'detach' button in static channels
|
||||
this.$buttons
|
||||
.find('.o_mail_chat_button_detach')
|
||||
.toggle(channel.type !== "static");
|
||||
// Hide 'invite', 'unsubscribe' and 'settings' buttons in static channels and DM
|
||||
this.$buttons
|
||||
.find('.o_mail_chat_button_invite')
|
||||
.find('.o_mail_chat_button_invite, .o_mail_chat_button_unsubscribe, .o_mail_chat_button_settings')
|
||||
.toggle(channel.type !== "dm" && channel.type !== 'static');
|
||||
|
||||
this.$buttons
|
||||
.find('.o_mail_chat_button_mark_read')
|
||||
.toggle(channel.id === "channel_inbox");
|
||||
|
||||
this.update_cp();
|
||||
|
||||
this.action.context.active_id = channel.id;
|
||||
this.action.context.active_ids = [channel.id];
|
||||
|
||||
@@ -320,7 +326,6 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
.addClass('o_active');
|
||||
|
||||
this.$('.o_chat_composer').toggle(channel.type !== 'static');
|
||||
this.sidebar.$el.toggle(channel.type !== 'static');
|
||||
|
||||
return this.fetch_and_render_thread().then(function () {
|
||||
self.thread.scroll_to({offset: new_channel_scrolltop});
|
||||
@@ -388,7 +393,6 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
$buttons: this.$buttons,
|
||||
$searchview: this.searchview.$el,
|
||||
$searchview_buttons: this.searchview.$buttons.contents(),
|
||||
$sidebar: this.sidebar.$el,
|
||||
},
|
||||
searchview: this.searchview,
|
||||
});
|
||||
@@ -482,7 +486,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
var current_channel_id = this.channel.id;
|
||||
if ((current_channel_id === "channel_starred" && !message.is_starred) ||
|
||||
(current_channel_id === "channel_inbox" && !message.is_needaction)) {
|
||||
chat_manager.fetch(this.channel, this.domain).then(function (messages) {
|
||||
chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (messages) {
|
||||
var options = self.get_thread_rendering_options();
|
||||
self.thread.remove_message_and_render(message.id, messages, options);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,301 @@ var Widget = require('web.Widget');
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
// The MentionManager allows the Composer to register listeners. For each
|
||||
// listener, it detects if the user is currently typing a mention (starting by a
|
||||
// given delimiter). If so, if fetches mention suggestions and renders them. On
|
||||
// suggestion clicked, it updates the selection for the corresponding listener.
|
||||
var MentionManager = Widget.extend({
|
||||
className: 'dropup o_composer_mention_dropdown',
|
||||
|
||||
events: {
|
||||
"mouseover .o_mention_proposition": "on_hover_mention_proposition",
|
||||
"click .o_mention_proposition": "on_click_mention_item",
|
||||
},
|
||||
|
||||
init: function (parent, options) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.composer = parent;
|
||||
this.options = _.extend({}, options, {
|
||||
min_length: 0,
|
||||
typing_speed: 200,
|
||||
});
|
||||
|
||||
this.open = false;
|
||||
this.listeners = [];
|
||||
this.set('mention_suggestions', []);
|
||||
this.on('change:mention_suggestions', this, this._render_suggestions);
|
||||
},
|
||||
|
||||
// Events
|
||||
on_hover_mention_proposition: function (event) {
|
||||
var $elem = $(event.currentTarget);
|
||||
this.$('.o_mention_proposition').removeClass('active');
|
||||
$elem.addClass('active');
|
||||
},
|
||||
on_click_mention_item: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
var text_input = this.composer.$input.val();
|
||||
var id = $(event.currentTarget).data('id');
|
||||
var selected_suggestion = _.find(_.flatten(this.get('mention_suggestions')), function (s) {
|
||||
return s.id === id;
|
||||
});
|
||||
var get_mention_index = function (matches, cursor_position) {
|
||||
for (var i=0; i<matches.length; i++) {
|
||||
if (cursor_position <= matches[i].index) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
};
|
||||
|
||||
// add the selected suggestion to the list
|
||||
var selection = this.active_listener.selection;
|
||||
if (selection.length && this.active_listener.sort_selection) {
|
||||
// get mention matches (ordered by index in the text)
|
||||
var matches = this._get_match(text_input, this.active_listener);
|
||||
var index = get_mention_index(matches, this._get_selection_positions().start);
|
||||
selection.splice(index, 0, selected_suggestion);
|
||||
} else {
|
||||
selection = selection.concat([selected_suggestion]);
|
||||
}
|
||||
this._update_listener_selection(this.active_listener, selection);
|
||||
|
||||
// update input text, and reset dropdown
|
||||
var cursor_position = this._get_selection_positions().start;
|
||||
var text_left = text_input.substring(0, cursor_position-(this.mention_word.length+1));
|
||||
var text_right = text_input.substring(cursor_position, text_input.length);
|
||||
var text_input_new = text_left + this.active_listener.delimiter + selected_suggestion.name + ' ' + text_right;
|
||||
this.composer.$input.val(text_input_new);
|
||||
this._set_cursor_position(text_left.length+selected_suggestion.name.length+2);
|
||||
this.set('mention_suggestions', []);
|
||||
},
|
||||
|
||||
// Public API
|
||||
/**
|
||||
* Registers a new listener, described by an object containing the following keys
|
||||
* @param {char} [delimiter] the mention delimiter
|
||||
* @param {function} [fetch_callback] the callback to fetch mention suggestions
|
||||
* @param {string} [model] the model used for redirection
|
||||
* @param {boolean} [notify_selection_changed] true to trigger an event when the selection changes
|
||||
* @param {string} [redirect_classname] the classname of the <a> wrapping the mention
|
||||
* @param {boolean} [sort_selection] true if the selection must be sorted
|
||||
* @param {string} [suggestions_template] the QWeb template to render suggestions
|
||||
*/
|
||||
register: function (listener) {
|
||||
this.listeners.push(_.extend(listener, {
|
||||
selection: [],
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the mention suggestions dropdown is open, false otherwise
|
||||
*/
|
||||
is_open: function () {
|
||||
return this.open;
|
||||
},
|
||||
|
||||
get_listener_selection: function (delimiter) {
|
||||
var listener = _.findWhere(this.listeners, {delimiter: delimiter});
|
||||
return listener ? listener.selection : [];
|
||||
},
|
||||
|
||||
proposition_navigation: function (keycode) {
|
||||
var $active = this.$('.o_mention_proposition.active');
|
||||
if (keycode === $.ui.keyCode.ENTER) { // selecting proposition
|
||||
$active.click();
|
||||
} else { // navigation in propositions
|
||||
var $to;
|
||||
if (keycode === $.ui.keyCode.DOWN) {
|
||||
$to = $active.nextAll('.o_mention_proposition').first();
|
||||
} else {
|
||||
$to = $active.prevAll('.o_mention_proposition').first();
|
||||
}
|
||||
if ($to.length) {
|
||||
$active.removeClass('active');
|
||||
$to.addClass('active');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Detects if the user is currently typing a mention word
|
||||
* @return the search string if it is, false otherwise
|
||||
*/
|
||||
detect_delimiter: function () {
|
||||
var self = this;
|
||||
var options = this.options;
|
||||
var text_val = this.composer.$input.val();
|
||||
var cursor_position = this._get_selection_positions().start;
|
||||
var left_string = text_val.substring(0, cursor_position);
|
||||
function validate_keyword (delimiter) {
|
||||
var search_str = text_val.substring(left_string.lastIndexOf(delimiter) - 1, cursor_position);
|
||||
var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))";
|
||||
var regex_start = new RegExp(pattern, "g");
|
||||
search_str = search_str.replace(/^\s\s*|^[\n\r]/g, '');
|
||||
if (regex_start.test(search_str) && search_str.length > options.min_length) {
|
||||
search_str = search_str.replace(pattern, '');
|
||||
return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
this.active_listener = undefined;
|
||||
for (var i=0; i<this.listeners.length; i++) {
|
||||
var listener = this.listeners[i];
|
||||
this.mention_word = validate_keyword(listener.delimiter);
|
||||
|
||||
if (this.mention_word !== false) {
|
||||
this.active_listener = listener;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.active_listener) {
|
||||
// start a timeout to fetch data with the current 'mention word'. The timer avoid to start
|
||||
// an RPC for each pushed key when the user is still typing.
|
||||
// The 'typing_speed' option should approach the time for a human to type a letter.
|
||||
clearTimeout(this.mention_fetch_timer);
|
||||
this.mention_fetch_timer = setTimeout(function () {
|
||||
$.when(listener.fetch_callback(self.mention_word, self.active_listener)).then(function (suggestions) {
|
||||
self.set('mention_suggestions', suggestions);
|
||||
});
|
||||
}, this.options.typing_speed);
|
||||
} else {
|
||||
this.set('mention_suggestions', []); // close the dropdown
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a listener's selection should be updated after DELETE or BACKSPACE keypress
|
||||
*/
|
||||
check_remove: function () {
|
||||
var self = this;
|
||||
var to_remove = [];
|
||||
var selection = this._get_selection_positions();
|
||||
var deleted_binf = selection.start;
|
||||
var deleted_bsup = selection.end;
|
||||
|
||||
_.each(this.listeners, function (listener) {
|
||||
var mention_selection = listener.selection;
|
||||
var matches = self._get_match(self.composer.$input.val(), listener);
|
||||
for (var i=0; i<matches.length; i++) {
|
||||
var m = matches[i];
|
||||
var m1 = m.index;
|
||||
var m2 = m.index + m[0].length;
|
||||
if (deleted_binf <= m2 && m1 < deleted_bsup) {
|
||||
to_remove.push(mention_selection[i]);
|
||||
}
|
||||
}
|
||||
if (to_remove.length) {
|
||||
self._update_listener_selection(listener, _.difference(mention_selection, to_remove));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces mentions appearing in the string 's' by html links with proper redirection
|
||||
*/
|
||||
generate_links: function (s) {
|
||||
var self = this;
|
||||
var mention_link = "<a href='#' class='%s' data-oe-id='%s' data-oe-model='%s'>%s%s</a>";
|
||||
_.each(this.listeners, function (listener) {
|
||||
var selection = listener.selection;
|
||||
if (selection.length) {
|
||||
var matches = self._get_match(s, listener);
|
||||
var substrings = [];
|
||||
var start_index = 0;
|
||||
for (var i=0; i<matches.length; i++) {
|
||||
var match = matches[i];
|
||||
var end_index = match.index + match[0].length;
|
||||
var match_name = match[0].substring(1);
|
||||
var processed_text = _.str.sprintf(mention_link, listener.redirect_classname, selection[i].id, listener.model, listener.delimiter, match_name);
|
||||
var subtext = s.substring(start_index, end_index).replace(match[0], processed_text);
|
||||
substrings.push(subtext);
|
||||
start_index = end_index;
|
||||
}
|
||||
substrings.push(s.substring(start_index, s.length));
|
||||
s = substrings.join('');
|
||||
}
|
||||
});
|
||||
return s;
|
||||
},
|
||||
|
||||
reset_selections: function () {
|
||||
var self = this;
|
||||
_.each(this.listeners, function (listener) {
|
||||
self._update_listener_selection(listener, []);
|
||||
});
|
||||
},
|
||||
|
||||
// Private functions
|
||||
/**
|
||||
* Returns the matches (as RexExp.exec does) for the mention in the input text
|
||||
* @param {String} input_text: the text to search matches
|
||||
* @param {Object} listener: the listener for which we want to find a match
|
||||
* @returns {Object[]} matches in the same format as RexExp.exec()
|
||||
*/
|
||||
_get_match: function (input_text, listener) {
|
||||
// create the regex of all mention's names
|
||||
var names = _.pluck(listener.selection, 'name');
|
||||
var escaped_names = _.map(names, function (str) {
|
||||
return "("+_.str.escapeRegExp(listener.delimiter+str)+")";
|
||||
});
|
||||
var regex_str = escaped_names.join('|');
|
||||
// extract matches
|
||||
var result = [];
|
||||
if (regex_str.length) {
|
||||
var myRegexp = new RegExp(regex_str, 'g');
|
||||
var match = myRegexp.exec(input_text);
|
||||
while (match !== null) {
|
||||
result.push(match);
|
||||
match = myRegexp.exec(input_text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
_render_suggestions: function () {
|
||||
if (_.flatten(this.get('mention_suggestions')).length) {
|
||||
this.$el.html(QWeb.render(this.active_listener.suggestions_template, {
|
||||
suggestions: this.get('mention_suggestions'),
|
||||
}));
|
||||
this.$el
|
||||
.addClass('open')
|
||||
.find('.o_mention_proposition').first().addClass('active');
|
||||
this.open = true;
|
||||
} else {
|
||||
this.$el.removeClass('open');
|
||||
this.$el.empty();
|
||||
this.open = false;
|
||||
}
|
||||
},
|
||||
_update_listener_selection: function (listener, selection) {
|
||||
listener.selection = selection;
|
||||
if (listener.notify_selection_changed) {
|
||||
this.trigger('selection_changed', listener);
|
||||
}
|
||||
},
|
||||
|
||||
// Cursor position and selection utils
|
||||
_get_selection_positions: function () {
|
||||
var el = this.composer.$input.get(0);
|
||||
return el ? {start: el.selectionStart, end: el.selectionEnd} : {start: 0, end: 0};
|
||||
},
|
||||
_set_cursor_position: function (pos) {
|
||||
this.composer.$input.each(function (index, elem) {
|
||||
if (elem.setSelectionRange){
|
||||
elem.setSelectionRange(pos, pos);
|
||||
}
|
||||
else if (elem.createTextRange){
|
||||
elem.createTextRange().collapse(true).moveEnd('character', pos).moveStart('character', pos).select();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
var Composer = Widget.extend({
|
||||
template: "mail.ChatComposer",
|
||||
|
||||
@@ -22,8 +317,6 @@ var Composer = Widget.extend({
|
||||
"click .o_composer_button_send": "send_message",
|
||||
"click .o_composer_button_add_attachment": "on_click_add_attachment",
|
||||
"click .o_attachment_delete": "on_attachment_delete",
|
||||
"click .o_mention_proposition": "on_click_mention_item",
|
||||
"mouseover .o_mention_proposition": "on_hover_mention_proposition",
|
||||
},
|
||||
|
||||
init: function (parent, options) {
|
||||
@@ -33,9 +326,6 @@ var Composer = Widget.extend({
|
||||
input_baseline: 18,
|
||||
input_max_height: 150,
|
||||
input_min_height: 28,
|
||||
mention_delimiter: '@',
|
||||
mention_min_length: 0,
|
||||
mention_typing_speed: 200,
|
||||
mention_fetch_limit: 8,
|
||||
get_channel_info: function () {},
|
||||
});
|
||||
@@ -48,10 +338,26 @@ var Composer = Widget.extend({
|
||||
this.set('attachment_ids', []);
|
||||
|
||||
// Mention
|
||||
this.mention_manager = new MentionManager(this);
|
||||
this.mention_manager.register({
|
||||
delimiter: '@',
|
||||
fetch_callback: this.mention_fetch_partners.bind(this),
|
||||
model: 'res.partner',
|
||||
notify_selection_changed: true,
|
||||
redirect_classname: 'o_mail_redirect',
|
||||
sort_selection: true,
|
||||
suggestions_template: 'mail.ChatComposer.MentionPartnerSuggestions',
|
||||
});
|
||||
this.mention_manager.register({
|
||||
delimiter: '#',
|
||||
fetch_callback: this.mention_fetch_channels.bind(this),
|
||||
model: 'mail.channel',
|
||||
redirect_classname: 'o_channel_redirect',
|
||||
suggestions_template: 'mail.ChatComposer.MentionChannelSuggestions'
|
||||
});
|
||||
this.mention_manager.on('selection_changed', this, this.mention_render_selection);
|
||||
this.PartnerModel = new Model('res.partner');
|
||||
this.mention_dropdown_open = false;
|
||||
this.set('mention_partners', []); // proposition of not-mention partner matching the mention_word
|
||||
this.set('mention_selected_partners', []); // contains the mention partners sorted as they appear in the input text
|
||||
this.ChannelModel = new Model('mail.channel');
|
||||
},
|
||||
|
||||
start: function () {
|
||||
@@ -60,7 +366,6 @@ var Composer = Widget.extend({
|
||||
this.$attachment_button = this.$(".o_composer_button_add_attachment");
|
||||
this.$attachments_list = this.$('.o_composer_attachments_list');
|
||||
this.$mention_partner_tags = this.$('.o_composer_mentioned_partners');
|
||||
this.$mention_dropdown = this.$('.o_composer_mention_dropdown');
|
||||
this.$input = this.$('.o_composer_input');
|
||||
this.resize_input();
|
||||
|
||||
@@ -86,8 +391,8 @@ var Composer = Widget.extend({
|
||||
});
|
||||
|
||||
// Mention
|
||||
this.on('change:mention_partners', this, this.render_mention_partners);
|
||||
this.on('change:mention_selected_partners', this, this.render_mention_selected_partners);
|
||||
this.mention_manager.prependTo(this.$('.o_composer'));
|
||||
|
||||
return this._super();
|
||||
},
|
||||
|
||||
@@ -96,9 +401,9 @@ var Composer = Widget.extend({
|
||||
// behavior for the chatter composer
|
||||
var value = this.$input.val().replace(/\n|\r/g, '<br/>');
|
||||
return $.when({
|
||||
content: this.mention_preprocess_message(value),
|
||||
content: this.mention_manager.generate_links(value),
|
||||
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
|
||||
partner_ids: _.pluck(this.get('mention_selected_partners'), 'id'),
|
||||
partner_ids: _.pluck(this.mention_manager.get_listener_selection('@'), 'id'),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -114,7 +419,7 @@ var Composer = Widget.extend({
|
||||
// Empty input, selected partners and attachments
|
||||
self.$input.val('');
|
||||
self.resize_input();
|
||||
self.set('mention_selected_partners', []);
|
||||
self.mention_manager.reset_selections();
|
||||
self.set('attachment_ids', []);
|
||||
|
||||
self.$input.focus();
|
||||
@@ -157,18 +462,18 @@ var Composer = Widget.extend({
|
||||
// UP, DOWN: prevent moving cursor if navigation in mention propositions
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.DOWN:
|
||||
if (this.mention_dropdown_open) {
|
||||
if (this.mention_manager.is_open()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
// BACKSPACE, DELETE: check if need to remove a mention
|
||||
case $.ui.keyCode.BACKSPACE:
|
||||
case $.ui.keyCode.DELETE:
|
||||
this.mention_check_remove();
|
||||
this.mention_manager.check_remove();
|
||||
break;
|
||||
// ENTER: submit the message only if the dropdown mention proposition is not displayed
|
||||
case $.ui.keyCode.ENTER:
|
||||
if (this.mention_dropdown_open) {
|
||||
if (this.mention_manager.is_open()) {
|
||||
event.preventDefault();
|
||||
} else if (!this.prevent_send(event)) {
|
||||
event.preventDefault();
|
||||
@@ -195,16 +500,11 @@ var Composer = Widget.extend({
|
||||
case $.ui.keyCode.ENTER:
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.DOWN:
|
||||
this.mention_proposition_navigation(event.which);
|
||||
this.mention_manager.proposition_navigation(event.which);
|
||||
break;
|
||||
// Otherwise, check if a mention is typed
|
||||
default:
|
||||
this.mention_word = this.mention_detect_delimiter();
|
||||
if (this.mention_word !== false) {
|
||||
this.mention_word_changed();
|
||||
} else {
|
||||
this.set('mention_partners', []); // close the dropdown
|
||||
}
|
||||
this.mention_manager.detect_delimiter();
|
||||
this.resize_input();
|
||||
}
|
||||
},
|
||||
@@ -294,226 +594,35 @@ var Composer = Widget.extend({
|
||||
},
|
||||
|
||||
// Mention
|
||||
on_click_mention_item: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
var text_input = this.$input.val();
|
||||
var partner_id = $(event.currentTarget).data('partner-id');
|
||||
var selected_partner = _.filter(_.flatten(this.get('mention_partners')), function (p) {
|
||||
return p.id === partner_id;
|
||||
})[0];
|
||||
|
||||
// add the mention partner to the list
|
||||
var mention_selected_partners = this.get('mention_selected_partners');
|
||||
if (mention_selected_partners.length) { // there are already mention partners
|
||||
// get mention matches (ordered by index in the text)
|
||||
var matches = this.mention_get_match(text_input);
|
||||
var index = this.mention_get_index(matches, this.get_selection_positions().start);
|
||||
mention_selected_partners.splice(index, 0, selected_partner);
|
||||
mention_selected_partners = _.clone(mention_selected_partners);
|
||||
} else { // this is the first mentionned partner
|
||||
mention_selected_partners = mention_selected_partners.concat([selected_partner]);
|
||||
}
|
||||
this.set('mention_selected_partners', mention_selected_partners);
|
||||
|
||||
// update input text, and reset dropdown
|
||||
var cursor_position = this.get_selection_positions().start;
|
||||
var text_left = text_input.substring(0, cursor_position-(this.mention_word.length+1));
|
||||
var text_right = text_input.substring(cursor_position, text_input.length);
|
||||
var text_input_new = text_left + this.options.mention_delimiter + selected_partner.name + ' ' + text_right;
|
||||
this.$input.val(text_input_new);
|
||||
this.set_cursor_position(text_left.length+selected_partner.name.length+2);
|
||||
this.set('mention_partners', []);
|
||||
},
|
||||
|
||||
on_hover_mention_proposition: function (event) {
|
||||
var $elem = $(event.currentTarget);
|
||||
this.$('.o_mention_proposition').removeClass('active');
|
||||
$elem.addClass('active');
|
||||
},
|
||||
|
||||
mention_proposition_navigation: function (keycode) {
|
||||
var $active = this.$('.o_mention_proposition.active');
|
||||
if (keycode === $.ui.keyCode.ENTER) { // selecting proposition
|
||||
$active.click();
|
||||
} else { // navigation in propositions
|
||||
var $to;
|
||||
if (keycode === $.ui.keyCode.DOWN) {
|
||||
$to = $active.nextAll('.o_mention_proposition').first();
|
||||
} else {
|
||||
$to = $active.prevAll('.o_mention_proposition').first();
|
||||
}
|
||||
if ($to.length) {
|
||||
$active.removeClass('active');
|
||||
$to.addClass('active');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the text attached to the mention delimiter
|
||||
* @returns {String|false}: the text right after the delimiter or false
|
||||
*/
|
||||
mention_detect_delimiter: function () {
|
||||
var options = this.options;
|
||||
var delimiter = options.mention_delimiter;
|
||||
var text_val = this.$input.val();
|
||||
var cursor_position = this.get_selection_positions().start;
|
||||
var left_string = text_val.substring(0, cursor_position);
|
||||
var search_str = text_val.substring(left_string.lastIndexOf(delimiter) - 1, cursor_position);
|
||||
|
||||
return validate_keyword(search_str);
|
||||
|
||||
function validate_keyword (search_str) {
|
||||
var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))";
|
||||
var regex_start = new RegExp(pattern, "g");
|
||||
search_str = search_str.replace(/^\s\s*|^[\n\r]/g, '');
|
||||
if (regex_start.test(search_str) && search_str.length > options.mention_min_length) {
|
||||
search_str = search_str.replace(pattern, '');
|
||||
return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
mention_word_changed: function () {
|
||||
var self = this;
|
||||
// start a timeout to fetch partner with the current 'mention word'. The timer avoid to start
|
||||
// an RPC for each pushed key when the user is typing the partner name.
|
||||
// The 'mention_typing_speed' option should approach the time for a human to type a letter.
|
||||
clearTimeout(this.mention_fetch_timer);
|
||||
this.mention_fetch_timer = setTimeout(function () {
|
||||
self.mention_fetch_partner(self.mention_word);
|
||||
}, this.options.mention_typing_speed);
|
||||
},
|
||||
|
||||
mention_fetch_partner: function (search) {
|
||||
var self = this;
|
||||
mention_fetch_channels: function (search) {
|
||||
var kwargs = {
|
||||
channel: this.get_channel_info(),
|
||||
exclude: _.pluck(this.get('mention_selected_partners'), 'id'),
|
||||
limit: this.options.mention_fetch_limit,
|
||||
search: search,
|
||||
};
|
||||
this.PartnerModel.call('get_mention_suggestions', kwargs).then(function (suggestions) {
|
||||
self.set('mention_partners', suggestions);
|
||||
});
|
||||
return this.ChannelModel
|
||||
.call('get_mention_suggestions', kwargs)
|
||||
.then(function (suggestions) {
|
||||
return _.partition(suggestions, function (suggestion) {
|
||||
return _.contains(['public', 'groups'], suggestion.public);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
mention_check_remove: function () {
|
||||
var mention_selected_partners = this.get('mention_selected_partners');
|
||||
var partners_to_remove = [];
|
||||
var selection = this.get_selection_positions();
|
||||
var deleted_binf = selection.start;
|
||||
var deleted_bsup = selection.end;
|
||||
|
||||
var matches = this.mention_get_match(this.$input.val());
|
||||
for (var i=0; i<matches.length; i++) {
|
||||
var m = matches[i];
|
||||
var m1 = m.index;
|
||||
var m2 = m.index + m[0].length;
|
||||
if (deleted_binf <= m2 && m1 < deleted_bsup) {
|
||||
partners_to_remove.push(mention_selected_partners[i]);
|
||||
}
|
||||
}
|
||||
this.set('mention_selected_partners', _.difference(mention_selected_partners, partners_to_remove));
|
||||
mention_fetch_partners: function (search, listener) {
|
||||
var kwargs = {
|
||||
channel: this.get_channel_info(),
|
||||
exclude: listener ? _.pluck(listener.selection, 'id') : [],
|
||||
limit: this.options.mention_fetch_limit,
|
||||
search: search,
|
||||
};
|
||||
return this.PartnerModel.call('get_mention_suggestions', kwargs);
|
||||
},
|
||||
|
||||
mention_preprocess_message: function (message) {
|
||||
var partners = this.get('mention_selected_partners');
|
||||
if (partners.length) {
|
||||
var matches = this.mention_get_match(message);
|
||||
var substrings = [];
|
||||
var start_index = 0;
|
||||
for (var i=0; i<matches.length; i++) {
|
||||
var match = matches[i];
|
||||
var end_index = match.index + match[0].length;
|
||||
var partner_name = match[0].substring(1);
|
||||
var processed_text = _.str.sprintf("<a href='#' class='o_mail_redirect' data-oe-model='res.partner' data-oe-id='%s'>@%s</a>", partners[i].id, partner_name);
|
||||
var subtext = message.substring(start_index, end_index).replace(match[0], processed_text);
|
||||
substrings.push(subtext);
|
||||
start_index = end_index;
|
||||
}
|
||||
substrings.push(message.substring(start_index, message.length));
|
||||
return substrings.join('');
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
render_mention_partners: function () {
|
||||
if (_.flatten(this.get('mention_partners')).length) {
|
||||
this.$mention_dropdown.html(QWeb.render('mail.ChatComposer.MentionMenu', {
|
||||
suggestions: this.get('mention_partners'),
|
||||
}));
|
||||
this.$mention_dropdown
|
||||
.addClass('open')
|
||||
.find('.o_mention_proposition').first().addClass('active');
|
||||
this.mention_dropdown_open = true;
|
||||
} else {
|
||||
this.$mention_dropdown.removeClass('open');
|
||||
this.$mention_dropdown.empty();
|
||||
this.mention_dropdown_open = false;
|
||||
}
|
||||
},
|
||||
|
||||
render_mention_selected_partners: function () {
|
||||
mention_render_selection: function (listener) {
|
||||
this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', {
|
||||
partners: this.get('mention_selected_partners'),
|
||||
partners: listener.selection,
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the matches (as RexExp.exec does) for the partner mention in the input text
|
||||
* @param {String} input_text: the text to search matches
|
||||
* @returns {Object[]} matches in the same format as RexExp.exec()
|
||||
*/
|
||||
mention_get_match: function (input_text) {
|
||||
var self = this;
|
||||
// create the regex of all mention partner name
|
||||
var partner_names = _.pluck(this.get('mention_selected_partners'), 'name');
|
||||
var escaped_partner_names = _.map(partner_names, function (str) {
|
||||
return "("+_.str.escapeRegExp(self.options.mention_delimiter+str)+")";
|
||||
});
|
||||
var regex_str = escaped_partner_names.join('|');
|
||||
// extract matches
|
||||
var result = [];
|
||||
if(regex_str.length){
|
||||
var myRegexp = new RegExp(regex_str, 'g');
|
||||
var match = myRegexp.exec(input_text);
|
||||
while (match !== null) {
|
||||
result.push(match);
|
||||
match = myRegexp.exec(input_text);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
mention_get_index: function (matches, cursor_position) {
|
||||
for (var i=0; i<matches.length; i++) {
|
||||
if (cursor_position <= matches[i].index) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
},
|
||||
|
||||
// Others
|
||||
get_selection_positions: function () {
|
||||
var el = this.$input.get(0);
|
||||
return el ? {start: el.selectionStart, end: el.selectionEnd} : {start: 0, end: 0};
|
||||
},
|
||||
|
||||
set_cursor_position: function (pos) {
|
||||
this.$input.each(function (index, elem) {
|
||||
if (elem.setSelectionRange){
|
||||
elem.setSelectionRange(pos, pos);
|
||||
}
|
||||
else if (elem.createTextRange){
|
||||
elem.createTextRange().collapse(true).moveEnd('character', pos).moveStart('character', pos).select();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
is_empty: function () {
|
||||
return !this.$input.val().trim() && !this.$('.o_attachments').children().length;
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ odoo.define('mail.systray', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Model = require('web.Model');
|
||||
var SystrayMenu = require('web.SystrayMenu');
|
||||
var web_client = require('web.web_client');
|
||||
var Widget = require('web.Widget');
|
||||
@@ -20,16 +19,6 @@ var NotificationTopButton = Widget.extend({
|
||||
events: {
|
||||
"click": "on_click",
|
||||
},
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
var ir_model = new Model("ir.model.data");
|
||||
var def1 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]);
|
||||
var def2 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]);
|
||||
return $.when(def1, def2, this._super()).then(function (menu_id, action_id) {
|
||||
self.discuss_menu_id = menu_id;
|
||||
self.client_action_id = action_id;
|
||||
});
|
||||
},
|
||||
start: function () {
|
||||
chat_manager.bus.on("update_needaction", this, this.update_counter);
|
||||
this.update_counter(chat_manager.get_needaction_counter());
|
||||
@@ -39,10 +28,10 @@ var NotificationTopButton = Widget.extend({
|
||||
this.$('.o_notification_counter').html(counter);
|
||||
},
|
||||
on_click: function (event) {
|
||||
var self = this;
|
||||
event.preventDefault();
|
||||
this.do_action(this.client_action_id, {clear_breadcrumbs: true}).then(function () {
|
||||
core.bus.trigger('change_menu_section', self.discuss_menu_id);
|
||||
var discuss_ids = chat_manager.get_discuss_ids();
|
||||
this.do_action(discuss_ids.action_id, {clear_breadcrumbs: true}).then(function () {
|
||||
core.bus.trigger('change_menu_section', discuss_ids.menu_id);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ var Thread = Widget.extend({
|
||||
|
||||
on_channel_redirect: function (event) {
|
||||
event.preventDefault();
|
||||
var channel_id = $(event.target).data('channel-id');
|
||||
var channel_id = $(event.target).data('oe-id');
|
||||
this.trigger('redirect_to_channel', channel_id);
|
||||
},
|
||||
|
||||
@@ -132,7 +132,8 @@ var Thread = Widget.extend({
|
||||
remove_message_and_render: function (message_id, messages, options) {
|
||||
var self = this;
|
||||
this.$('.o_thread_message[data-message-id=' + message_id + ']').fadeOut({
|
||||
done: function () { self.render(messages, options); }
|
||||
done: function () { self.render(messages, options); },
|
||||
duration: 200,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
.o-flex-flow(row, nowrap);
|
||||
|
||||
.o_mail_chat_sidebar {
|
||||
.o-flex(0, 0, 200px);
|
||||
.o-flex(0, 0, 220px);
|
||||
overflow: auto;
|
||||
padding: 8px 0;
|
||||
font-weight: 300;
|
||||
|
||||
@media (max-width: @screen-xs-max) {
|
||||
flex: none;
|
||||
@@ -25,34 +26,40 @@
|
||||
}
|
||||
|
||||
.o_mail_sidebar_title {
|
||||
.o-webclient-padding(@top: 5px, @bottom: 5px);
|
||||
.o-flex-display();
|
||||
padding: 5px 8px 5px @odoo-horizontal-padding;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.o_add {
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
.o_mail_chat_channel_item {
|
||||
width: 100%;
|
||||
.o-webclient-padding(@top: 5px, @bottom: 5px);
|
||||
padding-left: 1.5*@odoo-horizontal-padding; // larger padding than titles
|
||||
padding: 5px 8px 5px 1.5*@odoo-horizontal-padding; // larger padding-left than titles
|
||||
> .badge {
|
||||
margin: 0;
|
||||
}
|
||||
.o_mail_partner_unpin {
|
||||
padding: 2px 8px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
.o_mail_add_channel {
|
||||
width: 100%;
|
||||
.o-webclient-padding(@top: 5px, @bottom: 5px);
|
||||
padding-left: 1.5*@odoo-horizontal-padding; // larger padding than titles
|
||||
padding: 5px 8px 5px 1.5*@odoo-horizontal-padding; // larger padding-left than titles
|
||||
position: relative;
|
||||
|
||||
> input {
|
||||
padding: 2px 5px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
> span {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 27px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,8 +108,8 @@
|
||||
@sidebar-icon-opacity: 0.6;
|
||||
.o_mail_chat {
|
||||
.o_mail_chat_sidebar {
|
||||
background-color: @gray;
|
||||
color: @gray-lighter;
|
||||
background-color: darken(@gray, 8%);
|
||||
color: lighten(@gray, 50%);
|
||||
|
||||
.o_mail_chat_channel_item {
|
||||
overflow: hidden;
|
||||
@@ -110,13 +117,13 @@
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: darken(@gray, 8%);;
|
||||
background-color: darken(@gray, 12%);;
|
||||
.o_mail_partner_unpin {
|
||||
opacity: @sidebar-icon-opacity;
|
||||
}
|
||||
}
|
||||
&.o_active {
|
||||
background-color: @gray-dark;
|
||||
background-color: darken(@gray, 15%);
|
||||
}
|
||||
.o_mail_partner_unpin {
|
||||
opacity: 0;
|
||||
@@ -150,6 +157,9 @@
|
||||
> input {
|
||||
color: black;
|
||||
}
|
||||
> span {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_sidebar_title {
|
||||
|
||||
@@ -73,14 +73,12 @@
|
||||
.o_mail_mailto, .o_mail_mailto:hover {
|
||||
.text-muted();
|
||||
}
|
||||
.o_channel_redirect {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_message_star, .o_thread_message_needaction {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
padding: 4px;
|
||||
&.fa-star {
|
||||
opacity: @mail-thread-icon-opacity;
|
||||
color: gold;
|
||||
@@ -97,12 +95,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
border-bottom: 1px solid transparent;
|
||||
border-top: 1px solid transparent;
|
||||
&.o_thread_selected_message {
|
||||
border-bottom: 1px solid @gray-lighter;
|
||||
border-top: 1px solid @gray-lighter;
|
||||
|
||||
.o_thread_message_side_date {
|
||||
opacity: @mail-thread-side-date-opacity;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<t t-set="channel_title">Channels</t>
|
||||
</t>
|
||||
<t t-call="mail.chat.SidebarItems">
|
||||
<t t-set="display_hash">true</t>
|
||||
<t t-set="input_placeholder">Add a channel</t>
|
||||
</t>
|
||||
|
||||
@@ -41,6 +42,7 @@
|
||||
</t>
|
||||
<t t-call="mail.chat.SidebarItems">
|
||||
<t t-set="input_placeholder">Add a channel</t>
|
||||
<t t-set="display_hash">true</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
@@ -76,6 +78,7 @@
|
||||
</div>
|
||||
</t>
|
||||
<div t-if="!disable_add_channel" class="o_mail_add_channel" t-attf-data-type="#{channel_type}">
|
||||
<span t-if="display_hash">#</span>
|
||||
<input type="text" t-attf-placeholder="#{input_placeholder}"/>
|
||||
</div>
|
||||
</t>
|
||||
@@ -84,8 +87,11 @@
|
||||
<t t-name="mail.chat.ControlButtons">
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary btn-sm o_mail_toggle_channels visible-xs-inline" t-att-title='_t("Toggle Channels")'>Channels</button>
|
||||
<button type="button" class="btn btn-default btn-sm o_mail_chat_button_invite" t-att-title='_t("Invite people")'>Invite</button>
|
||||
<button type="button" class="btn btn-default btn-sm o_mail_chat_button_detach hidden-xs" t-att-title='_t("Detach the conversation")'>Detach</button>
|
||||
<button type="button" class="btn btn-primary btn-sm o_mail_chat_button_invite" t-att-title='_t("Invite people")'>Invite</button>
|
||||
<button type="button" class="btn btn-primary btn-sm o_mail_chat_button_detach hidden-xs" t-att-title='_t("Detach the conversation")'>Detach</button>
|
||||
<button type="button" class="btn btn-primary btn-sm o_mail_chat_button_mark_read" t-att-title='_t("Mark all as read")'>Mark all read</button>
|
||||
<button type="button" class="btn btn-default btn-sm o_mail_chat_button_unsubscribe" t-att-title='_t("Unsubscribe from channel")'>Unsubscribe</button>
|
||||
<button type="button" class="btn btn-default btn-sm o_mail_chat_button_settings" t-att-title='_t("Open channel settings")'>Settings</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
<div t-name="mail.ChatComposer" class="o_chat_composer">
|
||||
<div class="o_composer">
|
||||
<div class="dropup o_composer_mention_dropdown"/>
|
||||
<textarea class="o_composer_input" placeholder="Write something"/>
|
||||
<div class="btn-group o_composer_buttons">
|
||||
<button class="btn btn-sm btn-primary o_composer_button_send" type="button">Send</button>
|
||||
@@ -34,14 +33,14 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatComposer.MentionMenu">
|
||||
<t t-name="mail.ChatComposer.MentionPartnerSuggestions">
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
||||
<t t-set="show_divider" t-value="false"/>
|
||||
<t t-foreach="suggestions" t-as="group">
|
||||
<t t-foreach="group" t-as="partner">
|
||||
<li t-if="show_divider && (partner_index === 0)" class="divider"/>
|
||||
<t t-set="show_divider" t-value="true"/>
|
||||
<li class="o_mention_proposition" t-att-data-partner-id="partner.id">
|
||||
<li class="o_mention_proposition" t-att-data-id="partner.id">
|
||||
<a href="#">
|
||||
<t t-esc="partner.name"/> <t t-if="partner.email">(<t t-esc="partner.email"/>)</t>
|
||||
</a>
|
||||
@@ -51,6 +50,21 @@
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatComposer.MentionChannelSuggestions">
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
||||
<t t-set="show_divider" t-value="false"/>
|
||||
<t t-foreach="suggestions" t-as="group">
|
||||
<t t-foreach="group" t-as="channel">
|
||||
<li t-if="show_divider && (channel_index === 0)" class="divider"/>
|
||||
<t t-set="show_divider" t-value="true"/>
|
||||
<li class="o_mention_proposition" t-att-data-id="channel.id">
|
||||
<a href="#"><t t-esc="channel.name"/></a>
|
||||
</li>
|
||||
</t>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatComposer.MentionTags">
|
||||
<t t-if="partners.length">Mention(s): </t>
|
||||
<t t-foreach="partners" t-as="partner">
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
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>
|
||||
</t>
|
||||
<t t-if="message.origin_id && (message.origin_id !== options.channel_id)">
|
||||
(in channel <a class="o_channel_redirect" t-att-data-channel-id="message.origin_id" href="#"><t t-esc="message.origin_name"/>)</a>
|
||||
(from <a class="o_channel_redirect" t-att-data-oe-id="message.origin_id" href="#"><t t-esc="message.origin_name"/>)</a>
|
||||
</t>
|
||||
</p>
|
||||
<t t-if="message.tracking_value_ids && message.tracking_value_ids.length > 0">
|
||||
@@ -144,7 +144,7 @@
|
||||
|
||||
<t t-name="mail.ChatThread.LoadMore">
|
||||
<div t-if="!all_messages_loaded" class="o_thread_show_more">
|
||||
<button class="btn btn-sm btn-link">-------- Show more messages --------</button>
|
||||
<button class="btn btn-sm btn-link">-------- Show older messages --------</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class sale_order(osv.Model):
|
||||
document = self.browse(cr, uid, ids[0], context=context)
|
||||
contex_signup = dict(context, signup_valid=True)
|
||||
return self.pool['res.partner']._get_signup_url_for_action(
|
||||
cr, uid, [document.partner_id.id], action='mail.action_mail_redirect',
|
||||
cr, uid, [document.partner_id.id], action='/mail/view',
|
||||
model=self._name, res_id=document.id, context=contex_signup,
|
||||
)[document.partner_id.id]
|
||||
|
||||
@@ -52,7 +52,7 @@ class account_invoice(osv.Model):
|
||||
document = self.browse(cr, uid, ids[0], context=context)
|
||||
contex_signup = dict(context, signup_valid=True)
|
||||
return self.pool['res.partner']._get_signup_url_for_action(
|
||||
cr, uid, [document.partner_id.id], action='mail.action_mail_redirect',
|
||||
cr, uid, [document.partner_id.id], action='/mail/view',
|
||||
model=self._name, res_id=document.id, context=contex_signup,
|
||||
)[document.partner_id.id]
|
||||
|
||||
|
||||
@@ -13,6 +13,47 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_order_form_portal_sale" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.portal_sale</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="procurement_ids"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="product_id"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_delivered_updateable"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//label[@for="qty_delivered"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_delivered"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//label[@for="qty_invoiced"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_invoiced"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="price_unit"]' position="attributes">
|
||||
<attribute name="attrs">{}</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//field[@name="order_line"]/form//field[@name="tax_id"]' position="attributes">
|
||||
<attribute name="attrs">{}</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//field[@name="order_line"]/tree//field[@name="procurement_ids"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/tree//field[@name="product_id"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/tree//field[@name="qty_delivered_updateable"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/tree//field[@name="qty_delivered"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/tree//field[@name="qty_invoiced"]' position="replace"/>
|
||||
<xpath expr='//field[@name="order_line"]/tree//field[@name="price_unit"]' position="attributes">
|
||||
<attribute name="attrs">{}</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//field[@name="order_line"]/tree//field[@name="tax_id"]' position="attributes">
|
||||
<attribute name="attrs">{}</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//field[@name="order_line"]/kanban//field[@name="product_id"]' position="replace">
|
||||
<field name="name"/>
|
||||
</xpath>
|
||||
<xpath expr='//field[@name="order_line"]/kanban/templates/t/div//span/t' position="replace">
|
||||
<t t-esc="record.name.value"/>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
Override the original action to set another help field and/or
|
||||
another context field, more suited for portal members
|
||||
|
||||
+24
-18
@@ -556,29 +556,12 @@ class PurchaseOrderLine(models.Model):
|
||||
def onchange_product_id(self):
|
||||
result = {}
|
||||
if not self.product_id:
|
||||
return {}
|
||||
return result
|
||||
|
||||
if self.product_id.uom_id.category_id.id != self.product_uom.category_id.id:
|
||||
self.product_uom = self.product_id.uom_po_id
|
||||
result['domain'] = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
||||
|
||||
seller = self.product_id._select_seller(
|
||||
self.product_id,
|
||||
partner_id=self.partner_id,
|
||||
quantity=self.product_qty,
|
||||
date=self.order_id.date_order and self.order_id.date_order[:10],
|
||||
uom_id=self.product_uom)
|
||||
|
||||
fpos = self.order_id.fiscal_position_id
|
||||
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id)
|
||||
|
||||
price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, self.product_id.supplier_taxes_id, self.taxes_id) if seller else 0.0
|
||||
if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
|
||||
price_unit = seller.currency_id.compute(price_unit, self.order_id.currency_id)
|
||||
self.price_unit = price_unit
|
||||
if seller or not self.date_planned:
|
||||
self.date_planned = self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
product_lang = self.product_id.with_context({
|
||||
'lang': self.partner_id.lang,
|
||||
'partner_id': self.partner_id.id,
|
||||
@@ -587,6 +570,29 @@ class PurchaseOrderLine(models.Model):
|
||||
if product_lang.description_purchase:
|
||||
self.name += '\n' + product_lang.description_purchase
|
||||
|
||||
seller = self.product_id._select_seller(
|
||||
self.product_id,
|
||||
partner_id=self.partner_id,
|
||||
quantity=self.product_qty,
|
||||
date=self.order_id.date_order and self.order_id.date_order[:10],
|
||||
uom_id=self.product_uom)
|
||||
|
||||
if seller or not self.date_planned:
|
||||
self.date_planned = self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
if not seller:
|
||||
return result
|
||||
|
||||
fpos = self.order_id.fiscal_position_id
|
||||
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id)
|
||||
|
||||
price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, self.product_id.supplier_taxes_id, self.taxes_id) if seller else 0.0
|
||||
if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
|
||||
price_unit = seller.currency_id.compute(price_unit, self.order_id.currency_id)
|
||||
self.price_unit = price_unit
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ProcurementRule(models.Model):
|
||||
_inherit = 'procurement.rule'
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
</group>
|
||||
<group string="Quotations & Sales" id="sale">
|
||||
<field name="group_sale_delivery_address" widget="radio"/>
|
||||
<field name="group_display_incoterm" widget="radio"/>
|
||||
<field name="group_discount_per_so_line" widget="radio"/>
|
||||
<field name="module_website_quote" widget="radio"/>
|
||||
<field name="module_sale_margin" widget="radio"/>
|
||||
|
||||
@@ -303,6 +303,8 @@ class SaleOrder(models.Model):
|
||||
inv_data = order._prepare_invoice()
|
||||
invoice = inv_obj.create(inv_data)
|
||||
invoices[group_key] = invoice
|
||||
elif group_key in invoices and order.name not in invoices[group_key].origin.split(', '):
|
||||
invoices[group_key].write({'origin': invoices[group_key].origin + ', ' + order.name})
|
||||
if line.qty_to_invoice > 0:
|
||||
line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice)
|
||||
elif line.qty_to_invoice < 0 and final:
|
||||
@@ -385,6 +387,8 @@ class SaleOrder(models.Model):
|
||||
def action_confirm(self):
|
||||
for order in self:
|
||||
order.state = 'sale'
|
||||
if self.env.context.get('send_email'):
|
||||
self.force_quotation_send()
|
||||
order.order_line._action_procurement_create()
|
||||
if not order.project_id:
|
||||
for line in order.order_line:
|
||||
|
||||
@@ -7,6 +7,7 @@ access_account_invoice_salesman,account_invoice salesman,account.model_account_i
|
||||
access_account_invoice_manager,account_invoice manager,account.model_account_invoice,base.group_sale_manager,1,1,1,1
|
||||
access_account_invoice_line_salesman,account_invoice.line salesman,account.model_account_invoice_line,base.group_sale_salesman,1,1,1,0
|
||||
access_account_payment_term_salesman,account_payment_term salesman,account.model_account_payment_term,base.group_sale_salesman,1,0,0,0
|
||||
access_account_analytic_tag_sale_salesman,account.analytic.tag.sale.salesman,analytic.model_account_analytic_tag,base.group_sale_salesman,1,0,0,0
|
||||
access_account_analytic_account_salesman,account_analytic_account salesman,analytic.model_account_analytic_account,base.group_sale_salesman,1,1,1,0
|
||||
access_sale_order_manager,sale.order.manager,model_sale_order,base.group_sale_manager,1,1,1,1
|
||||
access_sale_order_accountant,sale.order.accountant,model_sale_order,account.group_account_user,1,1,0,0
|
||||
|
||||
|
@@ -73,7 +73,7 @@ class StockMove(models.Model):
|
||||
def _prepare_procurement_from_move(self, move):
|
||||
res = super(StockMove, self)._prepare_procurement_from_move(move)
|
||||
if res and move.procurement_id and move.procurement_id.property_ids:
|
||||
res['property_ids'] = [(6, 0, self.property_ids.ids)]
|
||||
res['property_ids'] = [(6, 0, move.procurement_id.property_ids.ids)]
|
||||
return res
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
<field name="group_route_so_lines" widget="radio"/>
|
||||
<field name="group_mrp_properties" widget="radio" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
<field name="group_sale_delivery_address" position="after">
|
||||
<field name="group_display_incoterm" widget="radio"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -92,7 +92,7 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
product_packaging = fields.Many2one('product.packaging', string='Packaging', default=False)
|
||||
route_id = fields.Many2one('stock.location.route', string='Route', domain=[('sale_selectable', '=', True)])
|
||||
product_tmpl_id = fields.Many2one('product.template', related='product_id.product_tmpl_id', string='Product Template')
|
||||
product_tmpl_id = fields.Many2one('product.template', related='product_id.product_tmpl_id', string='Product Template', readonly=True)
|
||||
|
||||
@api.multi
|
||||
@api.depends('product_id')
|
||||
@@ -119,7 +119,6 @@ class SaleOrderLine(models.Model):
|
||||
self.product_packaging = False
|
||||
return {}
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
self.product_tmpl_id = self.product_id.product_tmpl_id
|
||||
if self.product_id.type == 'product':
|
||||
product = self.product_id.with_context(
|
||||
lang=self.order_id.partner_id.lang,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<t t-set="hide_mark_as_done" t-value="True"/>
|
||||
<div class="o_planner_intro">
|
||||
<p>
|
||||
This guide will help you get started with Odoo Inventory.
|
||||
This guide helps getting started with Odoo Inventory.
|
||||
Once you are done, you will benefit from:
|
||||
</p>
|
||||
<ul>
|
||||
@@ -18,7 +18,7 @@
|
||||
<li>Accurate visibility on all your operations</li>
|
||||
</ul>
|
||||
<p>
|
||||
In short, you will get a more efficient warehouse management that will lead
|
||||
In short, you will get a more efficient warehouse management that leads
|
||||
to inventory reduction and better efficiencies in your daily operations.
|
||||
</p><p>
|
||||
If you need help, do not hesitate to contact our experts
|
||||
@@ -54,7 +54,7 @@
|
||||
<t t-call="web_planner.page">
|
||||
<t t-set="page_title">Your Vendors</t>
|
||||
<p class="text-muted">
|
||||
Unless you are starting a new business, you probably have a list of vendors you'd like to import.
|
||||
Unless you are starting a new business, you probably have a list of vendors you would like to import.
|
||||
</p>
|
||||
<t t-if="not is_module_installed('account') and not is_module_installed('purchase')">
|
||||
<div class="alert alert-warning">
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
If you have less than 50 vendors, we recommand you
|
||||
If you have less than 50 vendors, we recommend you
|
||||
to create them manually.
|
||||
</p>
|
||||
<ul><li>
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
We can handle the whole import process
|
||||
We handle the whole import process
|
||||
for you: simply send your Odoo project
|
||||
manager a CSV file containing all your
|
||||
data.
|
||||
@@ -110,9 +110,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<span>For developers, <strong>you can use our
|
||||
<span>If you are a developer <strong>you can use our
|
||||
API</strong> to load data automatically through
|
||||
scripts: take a look at our <a href="https://www.odoo.com/documentation/8.0/api_integration.html" target="_blank">API Documentation</a> for more
|
||||
scripts: take a look at our <a href="https://www.odoo.com/documentation/9.0/api_integration.html" target="_blank">API Documentation</a> for more
|
||||
information.</span>
|
||||
</div>
|
||||
</t>
|
||||
@@ -135,6 +135,7 @@
|
||||
<div class="panel-body">
|
||||
<li><strong>Stockable products</strong> are subject to the full inventory management system: minimum stock rules, automatic procurement, etc.</li>
|
||||
<li><strong>Consumable products</strong> are always assumed to be in sufficient quantity in your stock, therefore their available quantities are not tracked</li>
|
||||
<li><strong>Service products</strong> are non-material products provided by a company or an individual</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,11 +151,11 @@
|
||||
<div class="panel-body">
|
||||
<ul>
|
||||
<li><strong>Manufacture:</strong> the product is manufactured internally or the service is supplied from internal resources</li>
|
||||
<li><strong>Buy:</strong> the product is bought from a vendor, through a Purchase Order</li>
|
||||
<li><strong>Buy:</strong> the product is bought from a vendor through a Purchase Order</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><strong>Make to Stock:</strong> your customers are supplied from available stock. If the quantities in stock are too low to fulfill the order, a Purchase Order (according the minimum stock rules) will be generated in order to get the products required</li>
|
||||
<li><strong>Make to Order:</strong> the product is acquired only as demand requires (meaning each time a Sales Order is confirmed). This will not modify stock in the medium term because you restock with the exact amount that was ordered</li>
|
||||
<li><strong>Make to Stock:</strong> your customers are supplied from available stock. If the quantities in stock are too low to fulfill the order, a Purchase Order is generated according the minimum stock rules in order to get the products required</li>
|
||||
<li><strong>Make to Order:</strong> the product is acquired only as demand requires, each time a Sales Order is confirmed. This does not modify stock in the medium term because you restock with the exact amount that was ordered</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,10 +185,10 @@
|
||||
</div>
|
||||
<div id="o_concept_desc4" class="panel-collapse collapse" role="tabpanel" aria-labelledby="o_concept4">
|
||||
<div class="panel-body">
|
||||
<li>By default, Odoo measures products by 'units', which are a generic and can represent just about anything</li>
|
||||
<li>By default, Odoo measures products by 'units', which are generic and represent just about anything</li>
|
||||
<li>To use more precise units like pounds or kilograms, activate<i> Some products may be sold/purchased in different unit of measures (advanced)</i> in the <a t-att-href="prepare_backend_url('stock.action_stock_config_settings')">Inventory Settings</a></li>
|
||||
<li>Your can review and edit the pre-defined units via the <a t-attf-href="/web#view_type=list&model=product.uom&menu_id=#{uom_menu_id}">Units of Measures</a> configuration menu</li>
|
||||
<li>Of course, feel free to add your own; just remember that Odoo can convert units within the same category (for example, liters to gallons in the volume category)</li>
|
||||
<li>You can review and edit the predefined units via the <a t-att-href="prepare_backend_url('product.product_uom_form_action')">Units of Measures</a> configuration menu</li>
|
||||
<li>Of course, feel free to add your own. Please note that Odoo is able to convert units within the same category, for example, liters to gallons in the volume category</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,7 +221,7 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
We can handle the whole import process
|
||||
We handle the whole import process
|
||||
for you: simply send your Odoo project
|
||||
manager a CSV file containing all your
|
||||
products.
|
||||
@@ -243,18 +244,18 @@
|
||||
<t t-call="web_planner.page">
|
||||
<t t-set="page_title">Initial Inventory</t>
|
||||
<p>
|
||||
It's time to make your initial Inventory. To do so:
|
||||
It is time to make your initial Inventory. In order to do so:
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<ol>
|
||||
<li><a t-att-href="prepare_backend_url('stock.action_inventory_form')" >Create an Inventory Adjustment</a></li>
|
||||
<li>When everyting is set, click on <i>Start Inventory</i></li>
|
||||
<li>When everything is set, click on <i>Start Inventory</i></li>
|
||||
<li>Set the <i>Real Quantity</i> for each Product and <i>Validate the Inventory</i></li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>
|
||||
Now, all your products have their quantity set correctly.
|
||||
Now, all your product quantities are correctly set.
|
||||
</p>
|
||||
<div class="alert alert-info">
|
||||
<strong>Import data</strong>
|
||||
@@ -269,14 +270,14 @@
|
||||
<t t-call="web_planner.page">
|
||||
<t t-set="page_title">Sales Flow</t>
|
||||
<p class="text-muted">
|
||||
In Odoo, inventory management is fully integrated with the sales and
|
||||
invoicing process. That means you can automate everything from the initial
|
||||
quotation, to the goods delivery and final invoice.
|
||||
Odoo inventory management is fully integrated with sales and
|
||||
invoicing process. Everything is automated from the initial
|
||||
quotation to the delivery and the final invoice.
|
||||
</p>
|
||||
<div t-if="not is_module_installed('sale')" class="alert alert-warning">
|
||||
<span>You need to install the Sales Management app for this flow.</span>
|
||||
</div>
|
||||
<p>To better understand a complete sales flow, follow these steps:</p>
|
||||
<p>A classic sales flow looks like the following:</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center">
|
||||
@@ -287,11 +288,11 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div><span class="label label-default text-center odoo_purple"><a t-att-href="prepare_backend_url('sale.action_quotations')"> Create a Quotation</a></span><div> for a customer and add some products</div></div>
|
||||
<div><span class="label label-default text-center odoo_purple"><a t-att-href="prepare_backend_url('sale.action_quotations')"> Create a Quotation</a></span><div> for a customer and add products</div></div>
|
||||
<span class="fa fa-angle-down fa-2x"></span>
|
||||
<div><span class="label label-default text-center odoo_purple">Confirm Sale</span><div>a Sale Order and a Transfer Order have been created</div></div>
|
||||
<div><span class="label label-default text-center odoo_purple">Confirm Sale</span><div>The quotation becomes a Sale Order and a Transfer Order is created</div></div>
|
||||
<span class="fa fa-angle-down fa-2x"></span>
|
||||
<div><span class="label label-default text-center odoo_purple">Click on <span class="fa fa-truck"/> Delivery</span><div> on the form to get the Transfer Order</div></div>
|
||||
<div><span class="label label-default text-center odoo_purple">Click on <span class="fa fa-truck"/> Delivery</span><div> on the sale order form to see Transfer Order</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -303,9 +304,11 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div><span class="label label-default text-center odoo_purple">'Ready to Transfer'</span><div>shoud be displayed on the transfer if your products supply chain is properly configured. Otherwise, <strong>Check the availability</strong> manually</div></div>
|
||||
<div><span class="label label-default text-center odoo_purple">'Available'</span><div>is displayed on the transfer if your products supply chain is properly configured. Otherwise, <strong>Check the availability</strong> manually</div></div>
|
||||
<span class="fa fa-angle-down fa-2x"></span>
|
||||
<div><span class="label label-default text-center odoo_purple">Validate the Delivery</span><div>either by processing it one line at a time by clicking on the <span class="fa fa-gears"/> button, or as a whole with the <i>Validate</i> button</div></div>
|
||||
<div><span class="label label-default text-center odoo_purple">Process the products</span><div>either by manually updating the Done quantity on the product lines, or let Odoo do it automatically while validating</div></div>
|
||||
<span class="fa fa-angle-down fa-2x"></span>
|
||||
<div><span class="label label-default text-center odoo_purple">Validate the Delivery</span><div>with the <i>Validate</i> button</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,23 +317,29 @@
|
||||
|
||||
<t t-call="web_planner.page">
|
||||
<t t-set="page_title">Purchase Flow</t>
|
||||
<p class="text-muted">
|
||||
Like with the sales flow, Odoo inventory management is
|
||||
fully integrated with the purchase app.
|
||||
</p>
|
||||
<div t-if="not is_module_installed('purchase')" class="alert alert-warning">
|
||||
<span>You need to install the Purchases Management app for this flow.</span>
|
||||
</div>
|
||||
<p>Now a similar exercise but for a purchase flow.</p>
|
||||
<p>A classic purchase flow looks like the following:</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center">
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading">
|
||||
<span class="panel-title">
|
||||
<strong>From the Purchases application</strong>
|
||||
<strong>From the Purchase application</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div><span class="label label-default text-center odoo_purple"><a t-att-href="prepare_backend_url('purchase.purchase_rfq')">Create a RFQ</a></span><div> with the products and their desired quantities to your vendor</div></div>
|
||||
<div><span class="label label-default text-center odoo_purple"><a t-att-href="prepare_backend_url('purchase.purchase_rfq')">Create a RFQ</a></span><div> from your vendor with the products and the requested quantities</div></div>
|
||||
<span class="fa fa-angle-down fa-2x"></span>
|
||||
<div><span class="label label-default text-center odoo_purple">Confirm Order</span><div>a Purchase Order and a Transfer Order have been created</div></div>
|
||||
<div><span class="label label-default text-center odoo_purple">Confirm Order</span><div>The RFQ becomes a Purchase Order and a Transfer Order is created</div></div>
|
||||
<span class="fa fa-angle-down fa-2x"></span>
|
||||
<div><span class="label label-default text-center odoo_purple">Click on <span class="fa fa-truck"/> Shipment</span><div> on the purchase order form or click on <i>Receive Products</i> to see the Transfer Order</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,16 +353,13 @@
|
||||
<div class="panel-body">
|
||||
<div><span class="label label-default text-center odoo_purple">Find Incoming Shipments</span><div>via the <a t-att-href="prepare_backend_url('stock.stock_picking_type_action')">Dashboard</a> when you receive the ordered products</div></div>
|
||||
<span class="fa fa-angle-down fa-2x"></span>
|
||||
<div><span class="label label-default text-center odoo_purple">Process the products</span><div>either manually by clicking on the <span class="fa fa-gears"/> button on the product lines, or scan them with the Odoo Barcode app</div></div>
|
||||
<div><span class="label label-default text-center odoo_purple">Process the products</span><div>either by manually updating the Done quantity on the product lines, or scan them with the Odoo Barcode app, or let Odoo do it automatically while validating</div></div>
|
||||
<span class="fa fa-angle-down fa-2x"></span>
|
||||
<div><span class="label label-default text-center odoo_purple">Validate the Receipt Order</span><div>to mark the products as transferred to your stock location</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Back to the Purchases application, find the <strong>original Purchase Order</strong> <a t-att-href="prepare_backend_url('purchase.purchase_form_action')">in the list</a> and print the invoice.</p>
|
||||
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="web_planner.category">
|
||||
@@ -368,7 +374,9 @@
|
||||
space and may loose value).
|
||||
</p>
|
||||
<p>
|
||||
In Odoo, <strong>Reordering Rules</strong> can be used to replenish your products: Odoo will automatically propose a procurement to buy new products if you are running out of stock.
|
||||
In Odoo, <strong>Reordering Rules</strong> are used to replenish your products.
|
||||
Odoo will automatically propose a procurement to buy new products if you are
|
||||
running out of stock.
|
||||
</p>
|
||||
<p>
|
||||
To create them, click on <strong><span class="fa fa-refresh"/> Reordering</strong> on <a t-att-href="prepare_backend_url('product.product_template_action_product')">a stockable Product</a> and simply enter a minimum and maximum quantity.
|
||||
@@ -379,14 +387,14 @@
|
||||
<t t-set="page_title">Resolve Procurement Exceptions</t>
|
||||
<t t-set="page_menutitle">Procurement Exceptions</t>
|
||||
<p class="text-muted">
|
||||
If you configured automatic procurement, Odoo will automatically generate
|
||||
If you configured automatic procurement, Odoo automatically generates
|
||||
Procurements Orders. You usually don't need to worry about them, but
|
||||
sometimes, the system can remain blocked without generating a
|
||||
sometimes the system can remain blocked without generating a
|
||||
corresponding document, usually due to a configuration problem.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
It is therefore a good idea to check and try to resolve those procurement
|
||||
exceptions, accessible from the Schedulers menu (you need the Stock
|
||||
exceptions. These are accessible from the Schedulers menu (you need the Stock
|
||||
Manager role to see it).
|
||||
</p>
|
||||
<h4>Here are some usual problems and their solutions:</h4>
|
||||
@@ -399,40 +407,41 @@
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="web_planner.category">
|
||||
<t t-set="menu_categorytitle">Advanced'</t>
|
||||
<t t-set="menu_categorytitle">Advanced</t>
|
||||
<t t-set="menu_categoryclasses" t-value="'fa-bolt'"/>
|
||||
|
||||
<t t-call="web_planner.page">
|
||||
<t t-set="page_title">Traceability</t>
|
||||
<p>
|
||||
Odoo can run advanced traceability by using Product Lots and Serial
|
||||
Odoo is able to run advanced traceability by using Product Lots and Serial
|
||||
Numbers, usually identified by bar codes stuck on the products.
|
||||
</p>
|
||||
<p>
|
||||
Lots can be encoded on incoming shipments, internal transfers and
|
||||
outgoing deliveries according to the settings in the picking type.
|
||||
For every product, you can define if it does not do any lot tracking at all,
|
||||
if it tracks by lot, or by unique serial number.
|
||||
The tracking can be configured on every product: not any tracing at
|
||||
all, tracking by lot, or tracking by unique serial number.
|
||||
</p>
|
||||
<p>
|
||||
In case of unique serial numbers, each serial number will correspond
|
||||
In case of unique serial numbers, each serial number corresponds
|
||||
to exactly one piece. In case of lots, you need to supply the quantity
|
||||
for each lot when you move that product.
|
||||
</p>
|
||||
<h4>To use Lot Tracking:</h4>
|
||||
<h4>How to use Lot Tracking:</h4>
|
||||
<ul>
|
||||
<li><strong>Activate <i>Track lots or serial numbers</i></strong> in your <a t-att-href="prepare_backend_url('stock.action_stock_config_settings')">Inventory Settings</a>.</li>
|
||||
<li>Set the products you want to track with lots or serial numbers by setting the Tracking field on the product form</li>
|
||||
<li><strong>When you process an incoming shipment, internal transfer or delivery</strong>, assign a lot number or different lot numbers or serial numbers to a product by clicking on the <span class="fa fa-list"/> icon</li>
|
||||
</ul>
|
||||
<p>To check the trajectory of a lot, you can find it back in <strong><i>Inventory Control > Serial Numbers</i></strong>
|
||||
and then Traceability or you can filter the Quantitative Valuation of a product with a certain lot. </p>
|
||||
<p>To check the trajectory of a lot, find it back in <strong><i>Inventory Control > Serial Numbers / lots</i></strong>.
|
||||
Choose a Lo t in the list and click on <i>Traceability</i>i>. You may also
|
||||
filter the Quantitative Valuation of a product with a certain lot.</p>
|
||||
</t>
|
||||
|
||||
<t t-call="web_planner.page">
|
||||
<t t-set="page_title">Routes Management</t>
|
||||
<p>
|
||||
Odoo can manage <strong>advanced push/pull routes configuration</strong>, for example:
|
||||
Odoo handles <strong>advanced push/pull routes configuration</strong>, for example:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Manage product manufacturing chains</li>
|
||||
@@ -440,7 +449,7 @@
|
||||
<li>Define routes within your warehouse according to business needs, such as Quality Control, After Sales Services or Supplier Returns</li>
|
||||
<li>Help rental management, by generating automated return moves for rented products</li>
|
||||
</ul>
|
||||
<p>This is quite complex to set up, so <strong>contact your Project Manager</strong> to address this</p>
|
||||
<p>This is quite complex to set up, so <strong>contact your Project Manager</strong> to address this.</p>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="web_planner.page">
|
||||
@@ -452,8 +461,8 @@
|
||||
<div class="text-center">
|
||||
<h1>Congratulations!</h1>
|
||||
<h4>We hope this guide helped you implement Odoo Inventory.</h4>
|
||||
<p>Don't hesitate to send us an email to describe your experience or to suggest improvements!</p>
|
||||
<p>Once it's fully working, give us some feedback: we love to hear from our customer. It would be great if you can send a photo of your warehouse to <a href="mailto:feedback@mail.odoo.com?subject=Inventory%20Planner" target="_blank">feedback@mail.odoo.com</a></p>
|
||||
<p>Do not hesitate to send us an email to describe your experience or to suggest improvements!</p>
|
||||
<p>Once it's fully working, give us some feedback: we love to hear from our customer. It would be great if you could send us a photo of your warehouse to <a href="mailto:feedback@mail.odoo.com?subject=Inventory%20Planner" target="_blank">feedback@mail.odoo.com</a></p>
|
||||
<p><strong>Enjoy your Inventory management with Odoo!</strong></p>
|
||||
<p>- The Odoo Team</p>
|
||||
<img src="/web_planner/static/src/img/odoo_logo.png"/>
|
||||
|
||||
@@ -167,7 +167,7 @@ class stock_config_settings(osv.osv_memory):
|
||||
|
||||
def onchange_adv_location(self, cr, uid, ids, group_stock_adv_location, context=None):
|
||||
if group_stock_adv_location:
|
||||
return {'value': {'group_stock_multiple_locations': True}}
|
||||
return {'value': {'group_stock_multiple_locations': 1}}
|
||||
return {}
|
||||
|
||||
def _default_company(self, cr, uid, context=None):
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
<label for="group_stock_adv_location"/>
|
||||
<div>
|
||||
<field name="group_stock_adv_location" widget="radio"/>
|
||||
<field name="group_stock_adv_location" widget="radio" on_change="onchange_adv_location(group_stock_adv_location)"/>
|
||||
<div attrs="{'invisible': [('group_stock_adv_location', '=', 0)]}">
|
||||
<label for="propagation_minimum_delta"/>
|
||||
<field name="propagation_minimum_delta" class="oe_inline"/>
|
||||
|
||||
@@ -23,8 +23,8 @@ access_stock_location_sale_manager,stock.location sale manager,model_stock_locat
|
||||
access_stock_location_stock_manager,stock.location stock manager,model_stock_location,stock.group_stock_manager,1,0,0,0
|
||||
access_stock_lines_date_user,report.stock.lines.date user,model_report_stock_lines_date,stock.group_stock_user,1,0,0,0
|
||||
access_stock_lines_date_manager,report.stock.lines.date manager,model_report_stock_lines_date,stock.group_stock_manager,1,1,1,1
|
||||
access_product_product_stock_user,product_product_stock_user,product.model_product_product,stock.group_stock_user,1,1,0,0
|
||||
access_product_template_stock_user,product.template stock user,product.model_product_template,stock.group_stock_user,1,1,0,0
|
||||
access_product_product_stock_user,product_product_stock_user,product.model_product_product,stock.group_stock_user,1,1,1,0
|
||||
access_product_template_stock_user,product.template stock user,product.model_product_template,stock.group_stock_user,1,1,1,0
|
||||
access_product_uom_categ_stock_manager,product.uom.categ stock_manager,product.model_product_uom_categ,stock.group_stock_manager,1,1,1,1
|
||||
access_product_uom_stock_manager,product.uom stock_manager,product.model_product_uom,stock.group_stock_manager,1,1,1,1
|
||||
access_product_category_stock_manager,product.category stock_manager,product.model_product_category,stock.group_stock_manager,1,1,1,1
|
||||
|
||||
|
@@ -2548,7 +2548,7 @@ class stock_move(osv.osv):
|
||||
dest_package_id=quant_dest_package_id, context=context)
|
||||
if redo_false_quants:
|
||||
move_rec = self.pool['stock.move'].browse(cr, uid, move, context=context)
|
||||
false_quants_move = [(x, x.qty) for x in move_rec.reserved_quant_ids if not x.lot_id]
|
||||
false_quants_move = [x for x in move_rec.reserved_quant_ids if not x.lot_id]
|
||||
|
||||
def action_done(self, cr, uid, ids, context=None):
|
||||
""" Process completely the moves given as ids and if all moves are done, it will finish the picking.
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
<tree string="Stock Moves">
|
||||
<field name="name"/>
|
||||
<field name="product_id"/>
|
||||
<field name="qty" on_change="onchange_quantity(product_id, product_qty, product_uom)"/>
|
||||
<field name="qty"/>
|
||||
<field name="location_id" groups="stock.group_locations"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -2042,8 +2042,8 @@
|
||||
</record>
|
||||
|
||||
<!-- Procurements are located in Warehouse menu hierarchy, MRP users should come to Stock application to use it. -->
|
||||
<menuitem id="menu_stock_sched" name="Schedulers" parent="stock.menu_stock_root" sequence="50" groups="base.group_no_one"/>
|
||||
<menuitem action="action_procurement_compute" id="menu_procurement_compute" parent="menu_stock_sched" groups="base.group_no_one" sequence="2"/>
|
||||
<menuitem id="menu_stock_sched" name="Schedulers" parent="stock.menu_stock_root" groups="group_stock_manager" sequence="50"/>
|
||||
<menuitem action="action_procurement_compute" id="menu_procurement_compute" parent="menu_stock_sched" sequence="2"/>
|
||||
<menuitem action="procurement.action_compute_schedulers" id="menu_stock_proc_schedulers" parent="menu_stock_sched" sequence="1" groups="stock.group_stock_manager"/>
|
||||
<menuitem id="menu_stock_procurement" name="Automatic Procurements" parent="stock.menu_stock_config_settings" sequence="5"/>
|
||||
<menuitem id="stock.menu_warehouse_report" name="Reports" sequence="99" parent="stock.menu_stock_root" groups="group_stock_manager"/>
|
||||
|
||||
@@ -12,6 +12,6 @@ class PlannerInventory(models.Model):
|
||||
return planner
|
||||
|
||||
def _prepare_planner_inventory_data(self):
|
||||
# TDE note: void method kept (stable version), probably to remove in master
|
||||
return {
|
||||
'uom_menu_id': self.env.ref('stock.menu_stock_uom_form_action').id
|
||||
}
|
||||
|
||||
@@ -723,7 +723,7 @@ var ActionManager = Widget.extend({
|
||||
|
||||
return this.ir_actions_common({
|
||||
widget: function () {
|
||||
return new ClientWidget(self, action);
|
||||
return new ClientWidget(self, action, options);
|
||||
},
|
||||
action: action,
|
||||
klass: 'oe_act_client',
|
||||
|
||||
@@ -1068,6 +1068,10 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{
|
||||
e.stopPropagation();
|
||||
var $row = $(e.target).closest('tr');
|
||||
$(self).trigger('deleted', [[self.row_id($row)]]);
|
||||
// IE Edge go crazy when we use confirm dialog and remove the focused element
|
||||
if(document.hasFocus && !document.hasFocus()) {
|
||||
$('<input />').appendTo('body').focus().remove();
|
||||
}
|
||||
})
|
||||
.delegate('td.oe_list_field_cell button', 'click', function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -791,202 +791,6 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
/* ---- Default values ---- {{{ */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 1070;
|
||||
display: block;
|
||||
visibility: visible;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
opacity: 0;
|
||||
filter: alpha(opacity=0);
|
||||
}
|
||||
.tooltip.in {
|
||||
opacity: 0.9;
|
||||
filter: alpha(opacity=90);
|
||||
}
|
||||
.tooltip.bottom {
|
||||
margin-top: 3px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.tooltip.bottom .tooltip-arrow {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 0 5px 5px;
|
||||
border-bottom-color: #000;
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
max-width: 200px;
|
||||
padding: 3px 8px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
background-color: #000;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.fade:not(.modal-backdrop) {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.15s linear;
|
||||
-o-transition: opacity 0.15s linear;
|
||||
transition: opacity 0.15s linear;
|
||||
}
|
||||
|
||||
.fade.in:not(.modal-backdrop) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.center-block {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.pull-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.img-responsive {
|
||||
display: block;
|
||||
width: 100% \9;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.img-rounded {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.img-thumbnail {
|
||||
padding: 4px;
|
||||
line-height: 1.42857;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: inline-block;
|
||||
width: 100% \9;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.img-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.img-responsive {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.shadow {
|
||||
-moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
|
||||
-webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.fa-1x {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.fa-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.fa-3x {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.fa-4x {
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
.fa-5x {
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
.fa-6x {
|
||||
font-size: 6em;
|
||||
}
|
||||
|
||||
.fa-7x {
|
||||
font-size: 7em;
|
||||
}
|
||||
|
||||
.fa-8x {
|
||||
font-size: 8em;
|
||||
}
|
||||
|
||||
.fa-9x {
|
||||
font-size: 9em;
|
||||
}
|
||||
|
||||
.fa-10x {
|
||||
font-size: 10em;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #428bca;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #3c763d;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #31708f;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #8a6d3b;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #a94442;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
color: #fff;
|
||||
background-color: #428bca;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: #dff0d8;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: #d9edf7;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: #f2dede;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
/* ---- COLOR-PICKER ---- {{{ */
|
||||
.colorpicker {
|
||||
@@ -1086,10 +890,6 @@ html[data-translatable] .oe_field_translate {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.img-responsive {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.css_editable_display {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
@@ -683,132 +683,6 @@ $attachment-border-color: #848490
|
||||
|
||||
/* }}} */
|
||||
|
||||
/* ---- Default values ---- {{{ */
|
||||
|
||||
.tooltip
|
||||
position: absolute
|
||||
z-index: 1070
|
||||
display: block
|
||||
visibility: visible
|
||||
font-size: 12px
|
||||
line-height: 1.4
|
||||
opacity: 0
|
||||
filter: alpha(opacity=0)
|
||||
&.in
|
||||
opacity: .9
|
||||
filter: alpha(opacity=90)
|
||||
&.bottom
|
||||
margin-top: 3px
|
||||
padding: 5px 0
|
||||
.tooltip-arrow
|
||||
top: 0
|
||||
left: 50%
|
||||
margin-left: -5px
|
||||
border-width: 0 5px 5px
|
||||
border-bottom-color: #000
|
||||
.tooltip-arrow
|
||||
position: absolute
|
||||
width: 0
|
||||
height: 0
|
||||
border-color: transparent
|
||||
border-style: solid
|
||||
.tooltip-inner
|
||||
max-width: 200px
|
||||
padding: 3px 8px
|
||||
color: #fff
|
||||
text-align: center
|
||||
text-decoration: none
|
||||
background-color: #000
|
||||
border-radius: 4px
|
||||
.fade:not(.modal-backdrop)
|
||||
opacity: 0
|
||||
-webkit-transition: opacity .15s linear
|
||||
-o-transition: opacity .15s linear
|
||||
transition: opacity .15s linear
|
||||
.fade.in:not(.modal-backdrop)
|
||||
opacity: 1
|
||||
.pull-right
|
||||
float: right
|
||||
.center-block
|
||||
display: block
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
.pull-left
|
||||
float: left
|
||||
.img-responsive
|
||||
display: block
|
||||
width: 100% \9
|
||||
max-width: 100%
|
||||
height: auto
|
||||
.img-rounded
|
||||
border-radius: 6px
|
||||
.img-thumbnail
|
||||
padding: 4px
|
||||
line-height: 1.42857143
|
||||
background-color: #fff
|
||||
border: 1px solid #ddd
|
||||
border-radius: 4px
|
||||
-webkit-transition: all .2s ease-in-out
|
||||
-o-transition: all .2s ease-in-out
|
||||
transition: all .2s ease-in-out
|
||||
display: inline-block
|
||||
width: 100% \9
|
||||
max-width: 100%
|
||||
height: auto
|
||||
.img-circle
|
||||
border-radius: 50%
|
||||
.img-responsive
|
||||
text-align: center
|
||||
img.shadow
|
||||
-moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2)
|
||||
-webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2)
|
||||
box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2)
|
||||
.fa-1x
|
||||
font-size: 1em
|
||||
.fa-2x
|
||||
font-size: 2em
|
||||
.fa-3x
|
||||
font-size: 3em
|
||||
.fa-4x
|
||||
font-size: 4em
|
||||
.fa-5x
|
||||
font-size: 5em
|
||||
.fa-6x
|
||||
font-size: 6em
|
||||
.fa-7x
|
||||
font-size: 7em
|
||||
.fa-8x
|
||||
font-size: 8em
|
||||
.fa-9x
|
||||
font-size: 9em
|
||||
.fa-10x
|
||||
font-size: 10em
|
||||
.text-primary
|
||||
color: #428bca
|
||||
.text-success
|
||||
color: #3c763d
|
||||
.text-info
|
||||
color: #31708f
|
||||
.text-warning
|
||||
color: #8a6d3b
|
||||
.text-danger
|
||||
color: #a94442
|
||||
.bg-primary
|
||||
color: #fff
|
||||
background-color: #428bca
|
||||
.bg-success
|
||||
background-color: #dff0d8
|
||||
.bg-info
|
||||
background-color: #d9edf7
|
||||
.bg-warning
|
||||
background-color: #fcf8e3
|
||||
.bg-danger
|
||||
background-color: #f2dede
|
||||
.hidden
|
||||
display: none
|
||||
|
||||
/* }}} */
|
||||
|
||||
/* ---- COLOR-PICKER ---- {{{ */
|
||||
|
||||
.colorpicker
|
||||
@@ -897,9 +771,6 @@ html[data-translatable] .oe_field_translate
|
||||
.o_editable .fa
|
||||
cursor: pointer
|
||||
|
||||
.img-responsive
|
||||
text-align: center
|
||||
|
||||
.css_editable_display
|
||||
display: block !important
|
||||
|
||||
|
||||
@@ -62,3 +62,204 @@ body {
|
||||
.note-color-palette .colorpicker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---- Default bootstrap values ---- {{{ */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 1070;
|
||||
display: block;
|
||||
visibility: visible;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
opacity: 0;
|
||||
filter: alpha(opacity=0);
|
||||
}
|
||||
.tooltip.in {
|
||||
opacity: 0.9;
|
||||
filter: alpha(opacity=90);
|
||||
}
|
||||
.tooltip.bottom {
|
||||
margin-top: 3px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.tooltip.bottom .tooltip-arrow {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 0 5px 5px;
|
||||
border-bottom-color: #000;
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
max-width: 200px;
|
||||
padding: 3px 8px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
background-color: #000;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.fade:not(.modal-backdrop) {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.15s linear;
|
||||
-o-transition: opacity 0.15s linear;
|
||||
transition: opacity 0.15s linear;
|
||||
}
|
||||
|
||||
.fade.in:not(.modal-backdrop) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.center-block {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.pull-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.img-responsive {
|
||||
display: block;
|
||||
width: 100% \9;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.img-rounded {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.img-thumbnail {
|
||||
padding: 4px;
|
||||
line-height: 1.42857;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: inline-block;
|
||||
width: 100% \9;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.img-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.img-responsive {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.shadow {
|
||||
-moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
|
||||
-webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.fa-1x {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.fa-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.fa-3x {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.fa-4x {
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
.fa-5x {
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
.fa-6x {
|
||||
font-size: 6em;
|
||||
}
|
||||
|
||||
.fa-7x {
|
||||
font-size: 7em;
|
||||
}
|
||||
|
||||
.fa-8x {
|
||||
font-size: 8em;
|
||||
}
|
||||
|
||||
.fa-9x {
|
||||
font-size: 9em;
|
||||
}
|
||||
|
||||
.fa-10x {
|
||||
font-size: 10em;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #428bca;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #3c763d;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #31708f;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #8a6d3b;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #a94442;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
color: #fff;
|
||||
background-color: #428bca;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: #dff0d8;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: #d9edf7;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: #f2dede;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.img-responsive {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
@@ -93,6 +93,7 @@ editor.Class = Widget.extend({
|
||||
return _t('This document is not saved!');
|
||||
}
|
||||
};
|
||||
this.$('button[data-action="save"]').prop('disabled', true);
|
||||
return this._super();
|
||||
},
|
||||
display_placeholder: function () {
|
||||
@@ -119,8 +120,7 @@ editor.Class = Widget.extend({
|
||||
});
|
||||
},
|
||||
rte_changed: function () {
|
||||
// todo: use !rte.history.getEditableHasUndo().length (but phantomjs unknow error)
|
||||
this.$('button[data-action=save]').prop('disabled', !$('.o_dirty').length);
|
||||
this.$('button[data-action=save]').prop('disabled', !rte.history.getEditableHasUndo().length);
|
||||
},
|
||||
save: function () {
|
||||
return this.rte.save().then(function () {
|
||||
|
||||
@@ -51,10 +51,6 @@ var History = function History ($editable) {
|
||||
|
||||
$editable.trigger("content_changed");
|
||||
|
||||
if (!oSnap.bookmark || oSnap.event === "blur") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var r = oSnap.editable.innerHTML === "" ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
|
||||
r.select();
|
||||
@@ -105,7 +101,7 @@ var History = function History ($editable) {
|
||||
pos--;
|
||||
}
|
||||
this.applySnap(aUndo[Math.max(--pos,0)]);
|
||||
while (pos && (aUndo[pos].event === "blur" || aUndo[pos].event === "activate")) {
|
||||
while (pos && (aUndo[pos].event === "blur" || (aUndo[pos+1].editable === aUndo[pos].editable && aUndo[pos+1].contents === aUndo[pos].contents))) {
|
||||
this.applySnap(aUndo[--pos]);
|
||||
}
|
||||
};
|
||||
@@ -132,8 +128,8 @@ var History = function History ($editable) {
|
||||
this.redo = function () {
|
||||
if (!aUndo[pos+1]) { return; }
|
||||
this.applySnap(aUndo[++pos]);
|
||||
while (aUndo[pos+1] && (aUndo[pos].event === "blur" || aUndo[pos].event === "activate" || aUndo[pos].event === "undo")) {
|
||||
this.applySnap(aUndo[Math.max(++pos,aUndo.length-1)]);
|
||||
while (aUndo[pos+1] && aUndo[pos].event === "active") {
|
||||
this.applySnap(aUndo[pos++]);
|
||||
}
|
||||
};
|
||||
this.hasRedo = function () {
|
||||
@@ -300,9 +296,12 @@ var RTE = Widget.extend({
|
||||
|
||||
$.fn.carousel = this.edit_bootstrap_carousel;
|
||||
|
||||
$(document).on('keydown', this, this.onKeydown);
|
||||
$(document).on('mousedown activate', this, this.onMousedown);
|
||||
$(document).on('mouseup', this, this.onMouseup);
|
||||
this._onKeydown = _.bind(this.onKeydown, this);
|
||||
$(document).on('keydown', this, this._onKeydown);
|
||||
this._onMousedown = _.bind(this.onMousedown, this);
|
||||
$(document).on('mousedown activate', this, this._onMousedown);
|
||||
this._onMouseup = _.bind(this.onMouseup, this);
|
||||
$(document).on('mouseup', this, this._onMouseup);
|
||||
|
||||
$('.o_not_editable').attr("contentEditable", false);
|
||||
|
||||
@@ -329,7 +328,8 @@ var RTE = Widget.extend({
|
||||
}
|
||||
});
|
||||
|
||||
$('#wrapwrap, .o_editable').on('click', '*', this, this.onClick);
|
||||
this._onClick = _.bind(this.onClick, this);
|
||||
$('#wrapwrap, .o_editable').on('click', '*', this, this._onClick);
|
||||
|
||||
$('body').addClass("editor_enable");
|
||||
|
||||
@@ -420,14 +420,10 @@ var RTE = Widget.extend({
|
||||
});
|
||||
},
|
||||
|
||||
saveElement: function ($el, context) {
|
||||
// remove multi edition
|
||||
if ($el.data('oe-model')) {
|
||||
var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression');
|
||||
if (this.__saved[key]) return true;
|
||||
this.__saved[key] = true;
|
||||
}
|
||||
// escape text nodes for xml saving
|
||||
/**
|
||||
* Get HTML cloned element with text nodes escaped for XML storage
|
||||
*/
|
||||
getEscapedElement: function($el) {
|
||||
var escaped_el = $el.clone();
|
||||
var to_escape = escaped_el.find('*').addBack();
|
||||
to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack());
|
||||
@@ -436,7 +432,17 @@ var RTE = Widget.extend({
|
||||
this.nodeValue = $('<div />').text(this.nodeValue).html();
|
||||
}
|
||||
});
|
||||
var markup = escaped_el.prop('outerHTML');
|
||||
return escaped_el;
|
||||
},
|
||||
|
||||
saveElement: function ($el, context) {
|
||||
// remove multi edition
|
||||
if ($el.data('oe-model')) {
|
||||
var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression');
|
||||
if (this.__saved[key]) return true;
|
||||
this.__saved[key] = true;
|
||||
}
|
||||
var markup = this.getEscapedElement($el).prop('outerHTML');
|
||||
|
||||
return ajax.jsonRpc('/web/dataset/call', 'call', {
|
||||
model: 'ir.ui.view',
|
||||
@@ -458,12 +464,12 @@ var RTE = Widget.extend({
|
||||
|
||||
$.fn.carousel = this.init_bootstrap_carousel;
|
||||
|
||||
$(document).off('keydown', this.onKeydown);
|
||||
$(document).off('mousedown applySnap', this.onMousedown);
|
||||
$(document).off('mouseup', this.onMouseup);
|
||||
$(document).off('keydown', this._onKeydown);
|
||||
$(document).off('mousedown applySnap', this._onMousedown);
|
||||
$(document).off('mouseup', this._onMouseup);
|
||||
$('.o_not_editable').removeAttr("contentEditable");
|
||||
$(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
|
||||
$('#wrapwrap, .o_editable').off('click', this.onClick);
|
||||
$('#wrapwrap, .o_editable').off('click', this._onClick);
|
||||
$(document).tooltip('destroy');
|
||||
$('body').removeClass("editor_enable");
|
||||
this.trigger('rte:stop');
|
||||
@@ -506,10 +512,8 @@ var RTE = Widget.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
var rte = $editable.data('rte') || event.data;
|
||||
|
||||
if (rte && rte.$last && (!$editable.size() || rte.$last[0] != $editable[0])) {
|
||||
var $destroy = rte.$last;
|
||||
if (this && this.$last && (!$editable.size() || this.$last[0] != $editable[0])) {
|
||||
var $destroy = this.$last;
|
||||
history.splitNext();
|
||||
|
||||
setTimeout(function () {
|
||||
@@ -517,15 +521,15 @@ var RTE = Widget.extend({
|
||||
$destroy.destroy().removeData('note-id').removeAttr('data-note-id');
|
||||
$('#note-popover-'+id+', #note-handle-'+id+', #note-dialog-'+id+'').remove();
|
||||
},150); // setTimeout to remove flickering when change to editable zone (re-create an editor)
|
||||
rte.$last = null;
|
||||
this.$last = null;
|
||||
}
|
||||
if ($editable.size() && (!rte.$last || rte.$last[0] != $editable[0]) &&
|
||||
if ($editable.size() && (!this.$last || this.$last[0] != $editable[0]) &&
|
||||
($target.closest('[contenteditable]').attr('contenteditable') || "").toLowerCase() !== 'false') {
|
||||
|
||||
$editable.summernote(rte.config($editable));
|
||||
$editable.summernote(this.config($editable));
|
||||
|
||||
$editable.data('NoteHistory', history);
|
||||
rte.$last = $editable;
|
||||
this.$last = $editable;
|
||||
|
||||
// firefox & IE fix
|
||||
try {
|
||||
@@ -544,9 +548,14 @@ var RTE = Widget.extend({
|
||||
if (dom.isImg($target[0])) {
|
||||
$target.trigger('mousedown'); // for activate selection on picture
|
||||
}
|
||||
|
||||
this.onEnableEditableArea($editable);
|
||||
}
|
||||
},
|
||||
|
||||
onEnableEditableArea: function ($editable) {
|
||||
},
|
||||
|
||||
onMouseup: function (event) {
|
||||
var $target = $(event.target);
|
||||
var $editable = $target.closest('.o_editable');
|
||||
@@ -555,9 +564,9 @@ var RTE = Widget.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
var rte = event.data;
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
rte.historyRecordUndo($target, 'activate', true);
|
||||
self.historyRecordUndo($target, 'activate', true);
|
||||
},0);
|
||||
},
|
||||
|
||||
|
||||
@@ -258,13 +258,6 @@ eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
|
||||
|
||||
fn_popover_update.call(this, $popover, oStyle, isAirMode);
|
||||
|
||||
if (!isAirMode || (oStyle.range && $(oStyle.range.sc).closest('[data-oe-model]:not([data-oe-model="ir.ui.view"]):not([data-oe-type="html"])').length)) {
|
||||
$imagePopover.hide();
|
||||
$linkPopover.hide();
|
||||
$airPopover.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oStyle.image) {
|
||||
if (oStyle.image.parentNode.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
|
||||
oStyle.image = oStyle.image.parentNode;
|
||||
|
||||
@@ -461,7 +461,7 @@ var many2one = Option.extend({
|
||||
return;
|
||||
}
|
||||
if (isNaN(+name)) {
|
||||
if (this.Model === "res.partner") {
|
||||
if (this.Model !== "res.partner") {
|
||||
domain.push(['name', 'ilike', name]);
|
||||
} else {
|
||||
domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]);
|
||||
@@ -504,6 +504,7 @@ var many2one = Option.extend({
|
||||
this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID);
|
||||
|
||||
this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
|
||||
this.$target.trigger('content_changed');
|
||||
|
||||
if (self.$target.data('oe-type') === "contact") {
|
||||
$('[data-oe-contact-options]')
|
||||
|
||||
@@ -41,13 +41,14 @@ var RTE_Translate = rte.Class.extend({
|
||||
var key = 'translation:'+$el.data('oe-translation-id');
|
||||
if (this.__saved[key]) return true;
|
||||
this.__saved[key] = true;
|
||||
var translation_content = this.getEscapedElement($el).html();
|
||||
|
||||
return ajax.jsonRpc('/web/dataset/call', 'call', {
|
||||
model: 'ir.translation',
|
||||
method: 'write',
|
||||
args: [
|
||||
[+$el.data('oe-translation-id')],
|
||||
{value: $el.html(), state: 'translated'},
|
||||
{value: translation_content, state: 'translated'},
|
||||
context || base.get_context()
|
||||
],
|
||||
});
|
||||
|
||||
@@ -268,6 +268,7 @@ var ImageDialog = Widget.extend({
|
||||
this.accept = this.options.accept || this.options.document ? "*/*" : "image/*";
|
||||
this.domain = this.options.domain || ['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]];
|
||||
this.parent = parent;
|
||||
this.old_media = media;
|
||||
this.media = media;
|
||||
this.images = [];
|
||||
this.page = 0;
|
||||
@@ -597,7 +598,16 @@ rte.Class.include({
|
||||
init: function (EditorBar) {
|
||||
this._super.apply(this, arguments);
|
||||
computeFonts();
|
||||
}
|
||||
},
|
||||
onEnableEditableArea: function ($editable) {
|
||||
if ($editable.data('oe-type') === "monetary") {
|
||||
$editable.attr('contenteditable', false);
|
||||
$editable.find('.oe_currency_value').attr('contenteditable', true);
|
||||
}
|
||||
if ($editable.is('[data-oe-model]') && !$editable.is('[data-oe-model="ir.ui.view"]') && !$editable.is('[data-oe-type="html"]')) {
|
||||
$editable.data('layoutInfo').popover().find('.btn-group:not(.note-history)').remove();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/* list of font icons to load by editor. The icons are displayed in the media editor and
|
||||
|
||||
@@ -403,6 +403,10 @@ var PlannerDialog = Widget.extend({
|
||||
ev.preventDefault();
|
||||
this.$el.modal('hide');
|
||||
this.$el.detach();
|
||||
},
|
||||
destroy: function() {
|
||||
this.$el.modal('hide');
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from openerp import http
|
||||
from openerp.exceptions import AccessError
|
||||
from openerp.http import request
|
||||
|
||||
|
||||
@@ -8,21 +9,39 @@ class WebSettingsDashboard(http.Controller):
|
||||
|
||||
@http.route('/web_settings_dashboard/data', type='json', auth='user')
|
||||
def web_settings_dashboard_data(self, **kw):
|
||||
if not request.env.user.has_group('base.group_erp_manager'):
|
||||
raise AccessError("Access Denied")
|
||||
|
||||
installed_apps = request.env['ir.module.module'].search_count([
|
||||
('application', '=', True),
|
||||
('application', '=', True),
|
||||
('state', 'in', ['installed', 'to upgrade', 'to remove'])
|
||||
])
|
||||
active_users = request.env['res.users'].search_count([('active', '=', True), ('log_ids', '!=', False)])
|
||||
pending_users = request.env['res.users'].search([('log_ids', '=', False)], order="create_date desc")
|
||||
cr = request.cr
|
||||
cr.execute("""
|
||||
SELECT exists(SELECT 1 FROM res_users_log WHERE create_uid=u.id), count(1)
|
||||
FROM res_users u
|
||||
WHERE active=true
|
||||
GROUP BY 1
|
||||
""")
|
||||
counts = dict(cr.fetchall())
|
||||
|
||||
cr.execute("""
|
||||
SELECT id, login
|
||||
FROM res_users u
|
||||
WHERE active=true
|
||||
AND NOT exists(SELECT 1 FROM res_users_log WHERE create_uid=u.id)
|
||||
ORDER BY id desc
|
||||
LIMIT 10
|
||||
""")
|
||||
pending_users = cr.fetchall()
|
||||
return {
|
||||
'apps': {
|
||||
'installed_apps': installed_apps
|
||||
},
|
||||
'users_info': {
|
||||
'active_users': active_users,
|
||||
'pending_users': zip(pending_users.mapped('id'), pending_users.mapped('login')),
|
||||
'active_users': counts.get(True, 0),
|
||||
'pending_count': counts.get(False, 0),
|
||||
'pending_users': pending_users,
|
||||
'user_form_view_id': request.env['ir.model.data'].xmlid_to_res_id("base.view_users_form"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ var DashboardInvitations = Widget.extend({
|
||||
'click .o_web_settings_dashboard_invitations': 'send_invitations',
|
||||
'click .o_web_settings_dashboard_access_rights': 'on_access_rights_clicked',
|
||||
'click .o_web_settings_dashboard_user': 'on_user_clicked',
|
||||
'click .o_web_settings_dashboard_more': 'on_more',
|
||||
},
|
||||
init: function(parent, data){
|
||||
this.data = data;
|
||||
@@ -132,6 +133,20 @@ var DashboardInvitations = Widget.extend({
|
||||
on_reverse_breadcrumb: function(){ return self.reload();}
|
||||
});
|
||||
},
|
||||
on_more: function(e) {
|
||||
e.preventDefault();
|
||||
var action = {
|
||||
type: 'ir.actions.act_window',
|
||||
view_type: 'form',
|
||||
view_mode: 'tree,form',
|
||||
res_model: 'res.users',
|
||||
domain: [['log_ids', '=', false]],
|
||||
views: [[false, 'list'], [false, 'form']],
|
||||
};
|
||||
this.do_action(action,{
|
||||
on_reverse_breadcrumb: function(){ return self.reload();}
|
||||
});
|
||||
},
|
||||
reload:function(){
|
||||
return this.parent.load(['invitations']);
|
||||
},
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
<t t-foreach="pending_users" t-as="pending">
|
||||
<a href="#"><div class="o_web_settings_dashboard_user" t-att-data-user-id="pending[0]"> <t t-esc="pending[1]"/></div></a>
|
||||
</t>
|
||||
<t t-if="pending_users.length < widget.data.pending_count">
|
||||
<br/>
|
||||
<a href="#"><div class="o_web_settings_dashboard_more"><t t-esc="widget.data.pending_count - pending_users.length"/> more</div></a>
|
||||
</t>
|
||||
</t>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,8 @@ Tour.register({
|
||||
onload: function () {
|
||||
$("#wrap p:first").replaceWith('<p>Write one or <font style="background-color: yellow;">two paragraphs <b>describing</b></font> your product or\
|
||||
<font style="color: rgb(255, 0, 0);">services</font>. To be successful your content needs to be\
|
||||
useful to your <a href="/999">readers</a>.</p> <input placeholder="test translate placeholder"/>');
|
||||
useful to your <a href="/999">readers</a>.</p> <input placeholder="test translate placeholder"/>\
|
||||
<p><b></b> is an HTML tag & is empty</p>');
|
||||
$("#wrap img").attr("title", "test translate image title");
|
||||
}
|
||||
},
|
||||
@@ -99,6 +100,13 @@ Tour.register({
|
||||
waitFor: '#wrap .o_dirty font:first:contains(translated french text)',
|
||||
element: 'input:first',
|
||||
},
|
||||
{
|
||||
title: "translate text with special char",
|
||||
onload: function () {
|
||||
$('#wrap input + p').find(':last').prepend('<{translated}>')
|
||||
.closest('[data-oe-translation-id]').addClass('o_dirty').trigger('keyup');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "translate placeholder",
|
||||
element: '.modal.web_editor-dialog input:first',
|
||||
@@ -119,6 +127,10 @@ Tour.register({
|
||||
waitNot: 'button[data-action=save]',
|
||||
waitFor: '#wrap p font:first:contains(translated french text)',
|
||||
},
|
||||
{
|
||||
title: "check: content with special char is translated",
|
||||
waitFor: "#wrap input + p:contains(<{translated}><b></b> is an HTML\xa0tag & )",
|
||||
},
|
||||
{
|
||||
title: "check: placeholder translation",
|
||||
waitFor: 'input[placeholder="test french placeholder"]',
|
||||
|
||||
@@ -187,6 +187,7 @@ class website_sale(http.Controller):
|
||||
|
||||
attrib_list = request.httprequest.args.getlist('attrib')
|
||||
attrib_values = [map(int, v.split("-")) for v in attrib_list if v]
|
||||
attributes_ids = set([v[0] for v in attrib_values])
|
||||
attrib_set = set([v[1] for v in attrib_values])
|
||||
|
||||
domain = self._get_search_domain(search, category, attrib_values)
|
||||
@@ -231,7 +232,8 @@ class website_sale(http.Controller):
|
||||
products = product_obj.browse(cr, uid, product_ids, context=context)
|
||||
|
||||
attributes_obj = request.registry['product.attribute']
|
||||
attributes_ids = attributes_obj.search(cr, uid, [('attribute_line_ids.product_tmpl_id', 'in', product_ids)], context=context)
|
||||
if product_ids:
|
||||
attributes_ids = attributes_obj.search(cr, uid, [('attribute_line_ids.product_tmpl_id', 'in', product_ids)], context=context)
|
||||
attributes = attributes_obj.browse(cr, uid, attributes_ids, context=context)
|
||||
|
||||
from_currency = pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id
|
||||
@@ -615,9 +617,6 @@ class website_sale(http.Controller):
|
||||
partner_id = order.partner_id.id
|
||||
|
||||
# save partner informations
|
||||
if billing_info.get('country_id'):
|
||||
billing_info['property_account_position_id'] = request.registry['account.fiscal.position']._get_fpos_by_region(
|
||||
cr, SUPERUSER_ID, billing_info['country_id'], billing_info.get('state_id') or False, billing_info.get('zip'), billing_info.get('vat') and True or False)
|
||||
if partner_id and request.website.partner_id.id != partner_id:
|
||||
orm_partner.write(cr, SUPERUSER_ID, [partner_id], billing_info, context=context)
|
||||
else:
|
||||
|
||||
@@ -185,8 +185,9 @@ class website(orm.Model):
|
||||
:returns: pricelist recordset
|
||||
"""
|
||||
isocountry = request.session.geoip and request.session.geoip.get('country_code') or False
|
||||
user_id = self.pool['res.users'].browse(cr, uid, request.uid or uid, context=context)
|
||||
pl_ids = self._get_pl(cr, uid, isocountry, show_visible,
|
||||
request.website.user_id.partner_id.property_product_pricelist.id,
|
||||
user_id.partner_id.property_product_pricelist.id,
|
||||
request.session.get('website_sale_current_pl'),
|
||||
request.website.website_pricelist_ids)
|
||||
return self.pool['product.pricelist'].browse(cr, uid, pl_ids, context=context)
|
||||
|
||||
@@ -121,10 +121,10 @@
|
||||
<template id="products_add_to_cart" inherit_id="website_sale.products_item" active="False" customize_show="True" name="Add to Cart">
|
||||
<xpath expr="//div[@class='product_price']" position="inside">
|
||||
<input name="product_id" t-att-value="product.product_variant_ids[0].id" type="hidden"/>
|
||||
<t t-if="len(product.product_variant_ids) == 1">
|
||||
<t t-if="len(product.product_variant_ids) == 1 and not len(product.optional_product_ids) >= 1">
|
||||
<a class="btn btn-default btn-xs a-submit"><span class="fa fa-shopping-cart"/></a>
|
||||
</t>
|
||||
<t t-if="len(product.product_variant_ids) > 1">
|
||||
<t t-if="len(product.product_variant_ids) > 1 or len(product.optional_product_ids) >= 1">
|
||||
<a class="btn btn-default btn-xs" t-att-href="keep('/shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']>1 else None))"><span class="fa fa-shopping-cart"/></a>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from lxml.etree import XMLSyntaxError
|
||||
import unittest
|
||||
from openerp.tools.translate import quote, unquote, xml_translate, html_translate
|
||||
|
||||
@@ -41,6 +42,29 @@ class TranslationToolsTestCase(unittest.TestCase):
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', 'Blah blah blah', 'Put some more text here'])
|
||||
|
||||
def test_translate_xml_text(self):
|
||||
""" Test xml_translate() on plain text. """
|
||||
terms = []
|
||||
source = "Blah blah blah"
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms, [source])
|
||||
|
||||
def test_translate_xml_text_entity_unescaped(self):
|
||||
""" Test xml_translate() on plain text with HTML entities. """
|
||||
terms = []
|
||||
source = "Blah blah blah"
|
||||
with self.assertRaises(XMLSyntaxError):
|
||||
xml_translate(terms.append, source)
|
||||
|
||||
def test_translate_xml_text_entity(self):
|
||||
""" Test xml_translate() on plain text with HTML escaped entities. """
|
||||
terms = []
|
||||
source = "Blah&nbsp;blah&nbsp;blah"
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms, [source])
|
||||
|
||||
def test_translate_xml_inline1(self):
|
||||
""" Test xml_translate() with formatting elements. """
|
||||
terms = []
|
||||
|
||||
@@ -286,8 +286,7 @@ def html_translate(callback, value):
|
||||
trans = XMLTranslator(callback, 'html')
|
||||
wrapped = "<div>%s</div>" % encode(value)
|
||||
root = etree.fromstring(wrapped, etree.HTMLParser(encoding='utf-8'))
|
||||
# html > body > div
|
||||
trans.process(root[0][0])
|
||||
trans.process(root[0][0]) # html > body > div
|
||||
return trans.get_done()[5:-6] # remove tags <div> and </div>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user