[MERGE] forward port branch saas-11.3 up to 53300648ed

This commit is contained in:
Christophe Simonis
2018-05-30 12:18:52 +02:00
42 changed files with 2183 additions and 155 deletions
+4 -1
View File
@@ -12,13 +12,14 @@
# Pedro M. Baeza <pedro.baeza@gmail.com>, 2017
# Nicolás Broggi <rnbroggi@gmail.com>, 2018
# RGB Consulting <odoo@rgbconsulting.com>, 2018
# Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-22 14:10+0000\n"
"PO-Revision-Date: 2018-03-22 14:10+0000\n"
"Last-Translator: RGB Consulting <odoo@rgbconsulting.com>, 2018\n"
"Last-Translator: Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018\n"
"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -174,6 +175,8 @@ msgid ""
"The invoice was not confirmed despite response from the acquirer (%s): "
"invoice amount is %r but acquirer replied with %r."
msgstr ""
"La factura no fue confirmada a pesar de la respuesta del cliente (%s): el "
"monto de la factura es %r pero el cliente respondió con %r."
#. module: account_payment
#: model:ir.ui.view,arch_db:account_payment.portal_invoice_error
+1 -1
View File
@@ -118,7 +118,7 @@ class ResUsers(models.Model):
if not values.get('login'):
raise ValueError(_('Signup: no login given for new user'))
if not values.get('partner_id') or not values.get('name'):
if not values.get('partner_id') and not values.get('name'):
raise ValueError(_('Signup: no name or partner given for new user'))
# create a copy of the template user (attached to a specific partner_id if given)
+6 -2
View File
@@ -17,13 +17,14 @@
# RGB Consulting <odoo@rgbconsulting.com>, 2017
# Pedro M. Baeza <pedro.baeza@gmail.com>, 2017
# e2f <projects@e2f.com>, 2018
# Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-22 14:10+0000\n"
"PO-Revision-Date: 2018-03-22 14:10+0000\n"
"Last-Translator: e2f <projects@e2f.com>, 2018\n"
"Last-Translator: Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018\n"
"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -154,7 +155,7 @@ msgstr "Separador de decimales:"
#: code:addons/base_import/static/src/xml/base_import.xml:76
#, python-format
msgid "Defer parent/child computation"
msgstr ""
msgstr "Diferir cálculo de padres e hijos"
#. module: base_import
#: model:ir.model.fields,field_description:base_import.field_base_import_import__display_name
@@ -331,6 +332,9 @@ msgid ""
" parent / child relation occurs on every line, and lead"
" to a slower import. Defering it can speed up import."
msgstr ""
"Si el modelo utiliza relaciones padre/hijo, se calcula la relación hija en "
"cada línea, lo que lleva a una importación lenta. Diferir el proceso de "
"cálculo puede acelerar la importación."
#. module: base_import
#. openerp-web
+82 -31
View File
@@ -14,9 +14,17 @@ var MASTER_TAB_HEARTBEAT_PERIOD = 1500; // 1.5 second
bus.ERROR_DELAY = 10000;
bus.Bus = Widget.extend({
init: function () {
/**
* @override
* @param {Object} params
* @param {Object} params.session the session to use to perform the poll RPC
* @param {string} params.pollRoute the longpolling route to use
*/
init: function (params) {
var self = this;
this._super();
this.pollRoute = params.pollRoute;
this.session = params.session;
this.options = {};
this.activated = false;
this.bus_id = _.uniqueId('bus');
@@ -77,7 +85,7 @@ bus.Bus = Widget.extend({
}
var data = {channels: self.channels, last: self.last, options: options};
// The backend has a maximum cycle time of 50 seconds so give +10 seconds
this._pollRpc = session.rpc('/longpolling/poll', data, {shadow : true, timeout: 60000});
this._pollRpc = session.rpc(this.pollRoute, data, {shadow : true, timeout: 60000});
this._pollRpc.then(function (result) {
self._pollRpc = false;
self.on_notification(result);
@@ -164,13 +172,17 @@ bus.Bus = Widget.extend({
* - bus.tab_list : list of opened tab ids
* - bus.tab_master : generated id of the master tab
*/
var CrossTabBus = bus.Bus.extend({
bus.CrossTabBus = bus.Bus.extend({
init: function () {
this._super.apply(this, arguments);
// used to prefix localStorage keys
this.sanitizedOrigin = this.session.origin.replace(/:\/{0,2}/g, '_');
this.is_master = false;
this.is_registered = false;
if (parseInt(getItem('bus.last_ts', 0)) + 50000 < new Date().getTime()) {
setItem('bus.last', -1);
if (parseInt(this._getItem('last_ts', 0)) + 50000 < new Date().getTime()) {
this._setItem('last', -1);
}
on("storage", this.on_storage.bind(this));
@@ -189,15 +201,15 @@ var CrossTabBus = bus.Bus.extend({
// Write last_presence in local storage if it has been updated since last heartbeat
var hb_period = this.is_master ? MASTER_TAB_HEARTBEAT_PERIOD : TAB_HEARTBEAT_PERIOD;
if (self.last_presence + hb_period > new Date().getTime()) {
setItem('bus.last_presence', self.last_presence);
self._setItem('last_presence', self.last_presence);
}
});
}, this._generateKey.bind(this));
if (this.is_master) {
setItem('bus.channels', this.channels);
setItem('bus.options', this.options);
this._setItem('channels', this.channels);
this._setItem('options', this.options);
} else {
this.channels = getItem('bus.channels', this.channels);
this.options = getItem('bus.options', this.options);
this.channels = this._getItem('channels', this.channels);
this.options = this._getItem('options', this.options);
}
return; // start_polling will be called again on tab registration
}
@@ -208,7 +220,7 @@ var CrossTabBus = bus.Bus.extend({
},
on_notification: function (notifications) {
if (this.is_master) { // broadcast to other tabs
var last = getItem('bus.last', -1);
var last = this._getItem('last', -1);
var max_id = Math.max(last, 0);
var new_notifications = _.filter(notifications, function (notif) {
max_id = Math.max(max_id, notif.id);
@@ -216,9 +228,9 @@ var CrossTabBus = bus.Bus.extend({
});
this.last = max_id;
if (new_notifications.length) {
setItem('bus.last', max_id);
setItem('bus.last_ts', new Date().getTime());
setItem('bus.notification', new_notifications);
this._setItem('last', max_id);
this._setItem('last_ts', new Date().getTime());
this._setItem('notification', new_notifications);
this._super(new_notifications);
}
} else {
@@ -230,45 +242,78 @@ var CrossTabBus = bus.Bus.extend({
// localStorage (avoid race condition)
var value = e.newValue;
// notifications changed
if (e.key === 'bus.notification') {
if (e.key === this._generateKey('notification')) {
var notifs = JSON.parse(value);
this.on_notification(notifs);
}
// update channels
if (e.key === 'bus.channels') {
if (e.key === this._generateKey('channels')) {
this.channels = JSON.parse(value);
}
// update options
if (e.key === 'bus.options') {
if (e.key === this._generateKey('options')) {
this.options = JSON.parse(value);
}
// update focus
if (e.key === 'bus.focus') {
if (e.key === this._generateKey('focus')) {
this.set('window_focus', JSON.parse(value));
}
},
add_channel: function () {
this._super.apply(this, arguments);
setItem('bus.channels', this.channels);
this._setItem('channels', this.channels);
},
delete_channel: function () {
this._super.apply(this, arguments);
setItem('bus.channels', this.channels);
this._setItem('channels', this.channels);
},
get_last_presence: function () {
return getItem('bus.last_presence') || new Date().getTime();
return this._getItem('last_presence') || new Date().getTime();
},
update_option: function () {
this._super.apply(this, arguments);
setItem('bus.options', this.options);
this._setItem('options', this.options);
},
delete_option: function () {
this._super.apply(this, arguments);
setItem('bus.options', this.options);
this._setItem('options', this.options);
},
focus_change: function (focus) {
this._super.apply(this, arguments);
setItem('bus.focus', focus);
this._setItem('focus', focus);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Generates localStorage keys prefixed by bus. (the name of this addon),
* and the sanitized origin, to prevent keys from conflicting when several
* bus instances (polling different origins) co-exist.
*
* @private
* @param {string} key
* @returns key prefixed with the origin
*/
_generateKey: function (key) {
return 'bus.' + this.sanitizedOrigin + '.' + key;
},
/**
* @private
* @param {string} key
* @param {*} defaultValue
*/
_getItem: function (key, defaultValue) {
return getItem(this._generateKey(key), defaultValue);
},
/**
* @private
* @param {string} key
* @param {*} value
*/
_setItem: function (key, value) {
setItem(this._generateKey(key), value);
},
});
@@ -291,14 +336,16 @@ function setItem(key, value) {
local_storage.setItem(key, JSON.stringify(value));
}
var tab_manager = {
peersKey: 'bus.peers',
masterKey: 'bus.master',
heartbeatKey: 'bus.heartbeat',
isMaster: false,
id: new Date().getTime() + ':' + (Math.random() * 1000000000 | 0),
register_tab: function (is_master_callback, is_no_longer_master, on_heartbeat_callback) {
register_tab: function (is_master_callback, is_no_longer_master, on_heartbeat_callback, generateKey) {
this.heartbeatKey = generateKey('heartbeat');
this.masterKey = generateKey('master');
this.peersKey = generateKey('peers');
this.is_master_callback = is_master_callback;
this.is_no_longer_master = is_no_longer_master || function () {};
this.on_heartbeat_callback = on_heartbeat_callback || function () {};
@@ -417,10 +464,14 @@ var tab_manager = {
// bus singleton, depending of the browser :
// if supporting LocalStorage, there will be only one tab polling
var params = {
pollRoute: '/longpolling/poll',
session: session,
};
if (typeof Storage !== "undefined") {
bus.bus = new CrossTabBus();
bus.bus = new bus.CrossTabBus(params);
} else {
bus.bus = new bus.Bus();
bus.bus = new bus.Bus(params);
}
return bus;
+6 -5
View File
@@ -34,13 +34,14 @@
# Juan José Scarafía <scarafia.juanjose@gmail.com>, 2017
# Pablo Rojas <rojaspablo88@gmail.com>, 2018
# José Matias Zuazo <jose.zuazo@tpsistemas.com.ar>, 2018
# Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-22 14:09+0000\n"
"PO-Revision-Date: 2018-03-22 14:09+0000\n"
"Last-Translator: José Matias Zuazo <jose.zuazo@tpsistemas.com.ar>, 2018\n"
"Last-Translator: Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018\n"
"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -1053,7 +1054,7 @@ msgstr "Forzar asignación"
#. module: crm
#: model:ir.ui.view,arch_db:crm.res_config_settings_view_form
msgid "Format phone numbers based on national conventions"
msgstr ""
msgstr "Dar formato a los números de teléfono basado en estándares nacionales"
#. module: crm
#: code:addons/crm/models/crm_lead.py:588
@@ -1461,12 +1462,12 @@ msgstr ""
#. module: crm
#: model:ir.actions.server,name:crm.action_mark_activities_done
msgid "Mark All Activities as Done"
msgstr ""
msgstr "Marcar Todas las Actividades como Hechas"
#. module: crm
#: model:ir.actions.server,name:crm.action_mark_late_activities_done
msgid "Mark Late Activities as Done"
msgstr ""
msgstr "Marcar Actividades Atrasadas como Hechas"
#. module: crm
#: model:ir.ui.view,arch_db:crm.crm_case_form_view_oppor
@@ -1912,7 +1913,7 @@ msgstr "Teléfono"
#. module: crm
#: model:ir.model.fields,field_description:crm.field_res_config_settings__module_crm_phone_validation
msgid "Phone Formatting"
msgstr ""
msgstr "Formateo de Números Telefónicos"
#. module: crm
#: code:addons/crm/models/crm_team.py:165
+4 -3
View File
@@ -25,13 +25,14 @@
# Christopher Ormaza <chris.ormaza@gmail.com>, 2017
# Nicolás Broggi <rnbroggi@gmail.com>, 2018
# e2f <projects@e2f.com>, 2018
# Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-22 14:10+0000\n"
"PO-Revision-Date: 2018-03-22 14:10+0000\n"
"Last-Translator: e2f <projects@e2f.com>, 2018\n"
"Last-Translator: Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018\n"
"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -751,7 +752,7 @@ msgstr "Peso del envío"
#: model:ir.ui.view,arch_db:delivery.report_package_barcode_delivery
#: model:ir.ui.view,arch_db:delivery.report_package_barcode_small_delivery
msgid "Shipping Weight:"
msgstr ""
msgstr "Peso del envío:"
#. module: delivery
#: model:ir.model.fields,help:delivery.field_product_product__hs_code
@@ -884,7 +885,7 @@ msgstr ""
#: model:ir.ui.view,arch_db:delivery.report_package_barcode_delivery
#: model:ir.ui.view,arch_db:delivery.report_package_barcode_small_delivery
msgid "Weight:"
msgstr ""
msgstr "Peso:"
#. module: delivery
#: model:ir.model.fields,field_description:delivery.field_product_packaging__width
+1 -1
View File
@@ -34,7 +34,7 @@ class DeliveryCarrier(models.Model):
# Internals for shipping providers #
# -------------------------------- #
name = fields.Char(required=True)
name = fields.Char(required=True, translate=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(help="Determine the display order", default=10)
# This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex')
+1 -2
View File
@@ -96,10 +96,9 @@ class SaleOrder(models.Model):
taxes_ids = self.fiscal_position_id.map_tax(taxes, carrier.product_id, self.partner_id).ids
# Create the sales order line
carrier_name = carrier.product_id.with_context(lang=self.partner_id.lang).name_get()[0][1]
values = {
'order_id': self.id,
'name': carrier_name,
'name': carrier.with_context(lang=self.partner_id.lang).name,
'product_uom_qty': 1,
'product_uom': carrier.product_id.uom_id.id,
'product_id': carrier.product_id.id,
+3 -1
View File
@@ -224,11 +224,13 @@ class Employee(models.Model):
def write(self, values):
res = super(Employee, self).write(values)
if 'parent_id' in values or 'department_id' in values:
holidays = self.env['hr.leave'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
hr_vals = {}
if values.get('parent_id') is not None:
hr_vals['manager_id'] = values['parent_id']
if values.get('department_id') is not None:
hr_vals['department_id'] = values['department_id']
holidays = self.env['hr.leave'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
holidays.write(hr_vals)
allocations = self.env['hr.leave.allocation'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
allocations.write(hr_vals)
return res
@@ -132,18 +132,6 @@
</tree>
</field>
</record>
<record id="hr_salary_rule_category_tree_view" model="ir.ui.view">
<field name="name">hr.salary.rule.category.tree.view</field>
<field name="model">hr.salary.rule.category</field>
<field name="field_parent">children_ids</field>
<field name="arch" type="xml">
<tree string="Salary Rule Categories">
<field name="name"/>
<field name="code"/>
<field name="parent_id" invisible="1"/>
</tree>
</field>
</record>
<record id="view_hr_salary_rule_category_filter" model="ir.ui.view">
<field name="name">hr.salary.rule.category.select</field>
@@ -169,20 +157,6 @@
sequence="11"
groups="base.group_no_one"
/>
<record id="action_hr_salary_rule_category_tree_view" model="ir.actions.act_window">
<field name="name">Salary Rule Categories Hierarchy</field>
<field name="res_model">hr.salary.rule.category</field>
<field name="view_type">tree</field>
<field name="domain">[('parent_id','=',False)]</field>
<field name="view_id" ref="hr_salary_rule_category_tree_view"/>
</record>
<menuitem
id="menu_hr_salary_rule_category_tree_view"
action="action_hr_salary_rule_category_tree_view"
parent="menu_hr_payroll_configuration"
sequence="12"
groups="base.group_no_one"
/>
<!--
Contribution Register
+2
View File
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
{
'name': 'Livechat Support',
'version': '1.0',
'category': 'Hidden',
'description': """
This module allows employee users to communicate with livechat operators
of another database on which a counterpart addon is installed.
""",
'author': 'Odoo SA',
'website': 'https://www.odoo.com',
'depends': [
'mail',
],
'data': [
'views/assets.xml',
],
'qweb': [
"static/src/xml/discuss.xml",
"static/src/xml/systray.xml",
],
'installable': True,
'auto_install': False,
}
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_http
+27
View File
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hmac
from hashlib import sha256
from odoo import models
from odoo.http import request
class Http(models.AbstractModel):
_inherit = 'ir.http'
def session_info(self):
result = super(Http, self).session_info()
if self.env.user.has_group('base.group_user'):
icp = request.env['ir.config_parameter'].sudo()
db_uuid = icp.get_param('database.uuid')
db_secret = icp.get_param('database.secret')
message = db_uuid + str(request.uid)
token = hmac.new(message.encode('utf-8'), db_secret.encode('utf-8'), sha256).hexdigest()
result['db_uuid'] = db_uuid
result['support_token'] = token
result['support_origin'] = False # must be overridden to specify the correct origin
return result
@@ -0,0 +1,455 @@
odoo.define('im_support.ChatManager', function (require) {
"use strict";
/**
* This module includes the ChatManager to handle the case of the Support
* channel, allowing the users of the current database to communicate with
* livechat operators from another database (the Support database).
*/
var ChatManager = require('mail.ChatManager');
var core = require('web.core');
var session = require('web.session');
var WebClient = require('web.WebClient');
var supportSession = require('im_support.SupportSession');
var supportBus = require('im_support.SupportBus');
var _t = core._t;
var CHANNEL_STATES = {
CLOSED: 'closed',
FOLDED: 'folded',
OPEN: 'open',
};
var ODOOBOT_ID = "ODOOBOT"; // FIXME: should be exported by the ChatManager
var POLL_TIMEOUT_DELAY = 1000 * 60 * 30; // 30 minutes
var POLL_TIMEOUT_KEY = 'im_support.poll_timeout';
var SUPPORT_CHANNEL_LIMIT = 30; // limit of messages to fetch
var SUPPORT_CHANNEL_STATE_KEY = 'im_support.channel_state';
ChatManager.include({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Overrides to prevent from calling the server with the Support channel
* (which is a record on the Support database).
*
* @override
*/
closeChatSession: function (channelID) {
if (channelID === this.supportChannelUUID) {
this.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, CHANNEL_STATES.CLOSED);
return $.when();
}
return this._super.apply(this, arguments);
},
/**
* Overrides to prevent from calling the server with the Support channel
* (which is a record on the Support database).
*
* @override
*/
detachChannel: function (channelID) {
if (channelID === this.supportChannelUUID) {
this.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, CHANNEL_STATES.OPEN);
return $.when();
}
return this._super.apply(this, arguments);
},
/**
* Overrides to prevent from calling the server with the Support channel
* (which is a record on the Support database).
*
* @override
*/
foldChannel: function (channelID, folded) {
if (channelID === this.supportChannelUUID) {
var value = folded ? CHANNEL_STATES.FOLDED : CHANNEL_STATES.OPEN;
this.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, value);
return $.when();
}
return this._super.apply(this, arguments);
},
/**
* Overrides to filter out the Support channel from the previews.
*
* @override
*/
getChannelsPreview: function () {
var self = this;
return this._super.apply(this, arguments).then(function (channelsPreview) {
return _.reject(channelsPreview, {id: self.supportChannelUUID});
});
},
/**
* Overrides to prevent from calling the server with the Support channel
* (which is a record on the Support database).
*
* @override
*/
getMentionPartnerSuggestions: function (channel) {
if (channel && channel.id === this.supportChannelUUID) {
return $.when();
}
return this._super.apply(this, arguments);
},
/**
* Initialises the Support: checks if there is a pending chat session
* between the user and Support, and if so, re-opens it.
* Note: we can't directly override init(), because it is already called
* when the include is applied, so we use this hook instead (called at
* webclient startup)
*/
initSupport: function () {
var self = this;
this.supportChannelDef = null;
this.supportChannelUUID = null;
this.pollTimeout = null;
this.pollingSupport = false;
// listen to notifications coming from Support longpolling
supportBus.on('notification', this, this._onSupportNotification);
// check if there is a pending chat session with the Support
var timeoutTimestamp = this.call('local_storage', 'getItem', POLL_TIMEOUT_KEY);
var pollingDelay = timeoutTimestamp && (JSON.parse(timeoutTimestamp) - Date.now());
if (pollingDelay && pollingDelay > 0) {
var channelState = this.call('local_storage', 'getItem', SUPPORT_CHANNEL_STATE_KEY);
this.startSupportLivechat(channelState).then(function () {
var supportChannel = self.getChannel(self.supportChannelUUID);
if (supportChannel.available) {
self._startPollingSupport(pollingDelay);
}
});
}
},
/**
* Overrides to prevent from calling the server with the Support channel
* (which is a record on the Support database).
*
* @override
*/
markChannelAsSeen: function (channel) {
if (channel.id === this.supportChannelUUID) {
this._updateChannelUnreadCounter(channel, 0);
return $.when();
}
return this._super.apply(this, arguments);
},
/**
* Overrides to call a specific route for messages posted in the Support
* channel.
*
* @override
*/
postMessage: function (data, options) {
if (options.channelID && options.channelID === this.supportChannelUUID) {
// ensure that the poll is active before posting the message
if (!this.pollingSupport) {
this._startPollingSupport();
}
return supportSession.rpc("/odoo_im_support/chat_post", {
uuid: this.supportChannelUUID,
message_content: data.content,
});
}
return this._super.apply(this, arguments);
},
/**
* Opens the support channel between a livechat operator from the Support
* database and the current user (if there is an available operator).
* Ensures to perform only once the request to create/retrieve the Support
* channel.
*
* @param {string} [channelState=CHANNEL_STATES.OPEN] state of the Support
* channel (see CHANNEL_STATES for accepted values)
* @returns {Deferred}
*/
startSupportLivechat: function (channelState) {
var self = this;
if (!this.supportChannelDef) {
// retrieve or create the channel
this.supportChannelDef = supportSession.rpc('/odoo_im_support/get_support_channel', {
channel_uuid: session.support_token,
db_uuid: session.db_uuid,
user_name: session.name,
});
}
return this.supportChannelDef.then(function (channel) {
if (!channel) {
// there is no channel (because there is no online operator, and
// no support channel has been created yet), so create one to
// open in a chat window
channel = {
available: false,
support_channel: true,
type: 'livechat',
uuid: "support_unavailable",
};
}
if (!channelState) {
channelState = self.discussOpen ? CHANNEL_STATES.CLOSED : CHANNEL_STATES.OPEN;
self.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, channelState);
}
if (!self.supportChannelUUID) {
// this part is only executed the first time the RPC is resolved
self.supportChannelUUID = channel.uuid;
// add the channel to the ChatManager
self._addChannel(_.extend(channel, {
id: channel.uuid,
is_minimized: _.contains([CHANNEL_STATES.OPEN, CHANNEL_STATES.FOLDED], channelState),
state: channelState,
}));
// display automatic messages in the channel
if (!channel.available) {
self._addSupportNotAvailableMessage();
} else {
self._addSupportWelcomeMessage();
}
} else {
// the channel has already been added to the ChatManager, so
// simply re-open it
channel = self.getChannel(self.supportChannelUUID);
if (self.discussOpen) {
self.openChannel(channel);
} else {
channel.is_folded = channelState === CHANNEL_STATES.FOLDED;
self.call('chat_window_manager', 'openChat', channel);
}
}
}).fail(function () {
self.do_warn(_t("The Support server can't be reached."));
});
},
/**
* Overrides to prevent from calling the server for messages of the Support
* channel (which are records on the Support database).
*
* @override
*/
toggleStarStatus: function (msgID) {
var message = this.getMessage(msgID);
if (_.contains(message.channel_ids, this.supportChannelUUID)) {
return $.when();
}
return this._super.apply(this, arguments);
},
/**
* Overrides to prevent from calling the server with the Support channel
* (which is a record on the Support database).
*
* @override
*/
unsubscribe: function (channel) {
if (channel.id === this.supportChannelUUID) {
return $.when();
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Adds a message in the Support channel indicating that this channel is
* not available for now.
*
* @private
*/
_addSupportNotAvailableMessage: function () {
var msg = {
author_id: ODOOBOT_ID,
body: _t("None of our operators are available. <a href='https://www.odoo.com/help' " +
"target='_blank'>Submit a ticket</a> to ask your question now."),
channel_ids: [this.supportChannelUUID],
id: Number.MAX_SAFE_INTEGER, // last message in the channel
};
this._addMessage(msg, {silent: true});
},
/**
* Adds a welcome message as first message of the Support channel.
*
* @private
*/
_addSupportWelcomeMessage: function () {
var channel = this.getChannel(this.supportChannelUUID);
if (channel.welcome_message) {
var msg = {
author_id: channel.operator,
body: channel.welcome_message,
channel_ids: [this.supportChannelUUID],
id: -1, // first message of the channel
};
this._addMessage(msg, {silent: true});
}
},
/**
* Overrides to handle the Support channel case: fetches the messages from
* the Support database in that case.
*
* @override
* @private
*/
_fetchFromChannel: function (channel, options) {
var self = this;
if (channel.id === this.supportChannelUUID) {
var domain = options && options.domain || [];
var cache = this._getChannelCache(channel, domain);
if (options && options.loadMore) {
var minMessageID = cache.messages[0].id;
domain = [['id', '<', minMessageID]].concat(domain);
}
return supportSession.rpc('/odoo_im_support/fetch_messages', {
domain: domain,
channel_uuid: session.support_token,
limit: SUPPORT_CHANNEL_LIMIT,
}).then(function (msgs) {
if (!cache.all_history_loaded) {
cache.all_history_loaded = msgs.length < SUPPORT_CHANNEL_LIMIT;
}
cache.loaded = true;
_.each(msgs, function (msg) {
_.extend(msg, {channel_ids: [self.supportChannelUUID]});
self._addMessage(msg, {
channel_id: self.supportChannelUUID,
silent: true,
domain: domain,
});
});
var channelCache = self._getChannelCache(channel, domain);
return channelCache.messages;
});
}
return this._super.apply(this, arguments);
},
/**
* @override
* @private
*/
_makeChannel: function (data) {
var channel = this._super.apply(this, arguments);
if (channel.id === this.supportChannelUUID) {
channel.supportChannel = true;
channel.name = _t('Support');
channel.available = data.available;
if (!channel.available) {
channel.name += _t(' (offline)');
}
channel.welcome_message = data.welcome_message;
channel.operator = data.operator;
}
return channel;
},
/**
* @override
* @private
*/
_makeMessage: function (data) {
if (_.contains(data.channel_ids, this.supportChannelUUID) && data.author_id !== ODOOBOT_ID) {
if (!data.author_id[0]) { // the author is the client
data.author_id = [session.partner_id, session.name];
} else { // the author is the operator
data.author_id[0] = -1; // prevent from conflicting with partners of this instance
var msg = this._super.apply(this, arguments);
msg.author_redirect = false;
msg.avatar_src = "/mail/static/src/img/odoo_o.png";
return msg;
}
}
return this._super.apply(this, arguments);
},
/**
* Automatically stop polling for Support messages after a given delay of
* inactivity.
*
* @private
* @param {integer} [pollingDelay=POLL_TIMEOUT_DELAY] the longpolling
* timeout delay to set
*/
_setPollTimeout: function (pollingDelay) {
pollingDelay = pollingDelay || POLL_TIMEOUT_DELAY;
clearTimeout(this.pollTimeout);
this.pollTimeout = setTimeout(this._stopPollingSupport.bind(this), pollingDelay);
// save the timeout expiration datetime into the LocalStorage so that
// we can re-open the Support channel if necessary on F5
var timeoutTimestamp = Date.now() + pollingDelay;
this.call('local_storage', 'setItem', POLL_TIMEOUT_KEY, timeoutTimestamp);
},
/**
* Initiates a longpoll with the server hosting the Support channel.
*
* @private
* @param {integer} [pollingDelay=POLL_TIMEOUT_DELAY] the longpolling
* timeout delay to set
*/
_startPollingSupport: function (pollingDelay) {
this.pollingSupport = true;
supportBus.add_channel(this.supportChannelUUID);
supportBus.start_polling();
this._setPollTimeout(pollingDelay);
},
/**
* Stops the longpoll with the server hosting the Support channel.
*
* @private
*/
_stopPollingSupport: function () {
this.pollingSupport = false;
supportBus.stop_polling();
this.call('local_storage', 'removeItem', POLL_TIMEOUT_KEY);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Handles poll notifications from the Support server.
*
* @private
* @param {Object[]} notifications
*/
_onSupportNotification: function (notifications) {
var self = this;
if (notifications && notifications.length) {
this._setPollTimeout();
}
_.each(notifications, function (notification) {
if (notification[1]._type === 'history_command') {
// ignore history requests
return;
}
var msg = _.extend(notification[1], {
channel_ids: [self.supportChannelUUID],
});
self._manageChannelNotification(msg);
});
},
});
// Unfortunately, we can't override init() of ChatManager because it is called
// before the include is applied, so we override the WebClient instead to call
// an initialization hook for Livechat Support in the ChatManager service.
WebClient.include({
/**
* Overrides to ask the ChatManager service to check whether there is a
* pending chat session with Support, and if so, to re-open it.
*
* @override
*/
show_application: function () {
this.call('chat_manager', 'initSupport');
return this._super.apply(this, arguments);
},
});
});
@@ -0,0 +1,65 @@
odoo.define('im_support.chat_discuss', function (require) {
"use strict";
/**
* This module includes Discuss to handle the case of the Support channel.
*/
var Discuss = require('mail.chat_discuss');
Discuss.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Overrides to hide stars in the Support channel, as this feature does not
* work in that channel.
*
* @override
* @private
*/
_getThreadRenderingOptions: function () {
var options = this._super.apply(this, arguments);
if (this.channel.supportChannel) {
options.display_stars = false;
}
return options;
},
/**
* Overrides to hide the attachment button in the composer of the Support
* channel, as this feature does not work in that channel. Also hides the
* composer when no operator is available.
*
* @override
* @private
*/
_setChannel: function (channel) {
var self = this;
return this._super.apply(this, arguments).then(function () {
var $buttonAddAttachment = self.basicComposer.$('.o_composer_button_add_attachment');
if (channel.supportChannel) {
if (!channel.available) {
self.basicComposer.do_hide();
}
$buttonAddAttachment.hide();
} else {
$buttonAddAttachment.show();
}
});
},
/**
* Overrides to handle the Support channel case (hide all buttons).
*
* @override
* @private
*/
_updateControlPanelButtons: function (channel) {
this._super.apply(this, arguments);
if (channel.supportChannel) {
this.$buttons.find('button').hide();
}
},
});
});
@@ -0,0 +1,47 @@
odoo.define('im_support.ExtendedChatWindow', function (require) {
"use strict";
/**
* This module includes ExtendedChatWindow to handle the case of the Support
* channel.
*/
var ExtendedChatWindow = require('mail.ExtendedChatWindow');
ExtendedChatWindow.include({
/**
* Overrides to tweak the options in the case of the Support channel (no
* star, and no input if no operator available).
*
* @override
*/
init: function () {
this._super.apply(this, arguments);
var channel = this.call('chat_manager', 'getChannel', this.channel_id);
if (channel && channel.supportChannel) {
this.options.display_stars = false;
if (!channel.available) {
this.options.input_less = true;
}
}
},
/**
* Overrides to remove the attachment button in the Support channel as this
* feature does not work in that channel.
*
* @note: this feature could be enabled by storing the attachments on the
* Support server.
* @override
*/
start: function () {
var def = this._super.apply(this, arguments);
var channel = this.call('chat_manager', 'getChannel', this.channel_id);
if (channel && channel.supportChannel) {
this.$('.o_composer .o_composer_button_add_attachment').remove();
}
return def;
},
});
});
@@ -0,0 +1,28 @@
odoo.define('im_support.SupportBus', function (require) {
"use strict";
/**
* This module instantiates and exports the instance of the Bus, parameterized
* to poll the Support server.
*/
var supportSession = require('im_support.SupportSession');
var bus = require('bus.bus');
var Bus;
if(typeof Storage !== "undefined"){
Bus = bus.CrossTabBus;
} else {
Bus = bus.Bus;
}
var params = {
session: supportSession,
pollRoute: '/longpolling/support_poll',
};
return new Bus(params);
});
@@ -0,0 +1,18 @@
odoo.define('im_support.SupportSession', function (require) {
"use strict";
/**
* This module returns an instance of Session which is linked to the Support
* server, allowing the current instance to communicate with the Support
* server (CORS).
*/
var session = require('web.session');
var Session = require('web.Session');
return new Session(null, session.support_origin, {
modules: odoo._modules,
use_cors: true,
});
});
@@ -0,0 +1,72 @@
odoo.define('im_support.systray', function (require) {
"use strict";
/**
* This module adds a fake Support channel in the messaging menu of the systray.
*/
var mailSystray = require('mail.systray');
var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var _t = core._t;
var MessagingMenu = mailSystray.MessagingMenu;
var SUPPORT_CHANNEL_ID = 'SupportChannel';
// Disable Support in mobile for design purposes (for now at least), and don't
// add it to the messaging dropdown if it isn't available
if (config.device.isMobile || !session.support_token || !session.support_origin) {
return;
}
MessagingMenu.include({
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.supportChannel = {
id: SUPPORT_CHANNEL_ID,
name: _t('Support'),
image_src: '/mail/static/src/img/odoo_o.png',
};
},
/**
* Overrides to add a className to the bottom part of the dropdown
* (containing the Support channel), so that the css rules apply. This
* className can't be added directly in the template, otherwise
* this.$channels_preview would be a nodeset containing the bottom part as
* well, and it will cause rendering issues when the dropdown is rerendered.
*
* @override
*/
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self.$('.o_mail_navbar_dropdown_bottom').addClass('o_mail_navbar_dropdown_channels');
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Overrides to handle the click on the fake Support channel.
*
* @override
* @private
*/
_onClickChannel: function (ev) {
var channelID = $(ev.currentTarget).data('channel_id');
if (channelID === SUPPORT_CHANNEL_ID) {
this.call('chat_manager', 'startSupportLivechat');
} else {
this._super.apply(this, arguments);
}
},
});
});
@@ -0,0 +1,13 @@
.o_mail_navbar_item {
.o_mail_navbar_dropdown {
.o_mail_navbar_dropdown_bottom {
border-top: 1px solid @gray-lighter-darker;
box-sizing: content-box;
.o_channel_info {
.o-flex-display();
.o-align-items(center);
}
}
}
}
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<t t-extend="mail.chat.Sidebar">
<t t-jquery="hr" t-operation="before">
<t t-foreach="channels" t-as="channel">
<t t-if="channel.type == 'livechat'">
<div t-attf-class="o_mail_chat_title_main o_mail_chat_channel_item #{(active_channel_id == channel.id) ? 'o_active': ''}"
t-att-data-channel-id="channel.id">
<span class="o_channel_name"><i class="fa fa-question-circle mr8"/>Odoo Support</span>
<t t-set="counter" t-value="0"/>
<t t-call="mail.chat.SidebarNeedaction"/>
</div>
</t>
</t>
</t>
</t>
</template>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-extend="mail.chat.MessagingMenu">
<t t-jquery=".o_mail_navbar_dropdown" t-operation="append">
<li t-if="widget.supportChannel" class="o_mail_navbar_dropdown_bottom">
<t t-set="channel" t-value="widget.supportChannel"/>
<t t-call="mail.chat.ChannelPreview"/>
</li>
</t>
</t>
</templates>
+16
View File
@@ -0,0 +1,16 @@
<openerp>
<data>
<template id="assets_backend" name="im_support_backend assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/im_support/static/src/less/systray.less" type="text/less"/>
<script type="text/javascript" src="/im_support/static/src/js/chat_manager.js"></script>
<script type="text/javascript" src="/im_support/static/src/js/discuss.js"></script>
<script type="text/javascript" src="/im_support/static/src/js/extended_chat_window.js"></script>
<script type="text/javascript" src="/im_support/static/src/js/support_bus.js"></script>
<script type="text/javascript" src="/im_support/static/src/js/support_session.js"></script>
<script type="text/javascript" src="/im_support/static/src/js/systray.js"></script>
</xpath>
</template>
</data>
</openerp>
+5
View File
@@ -11,6 +11,11 @@ from odoo.tools import consteq
class MassMailController(http.Controller):
@http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public')
def unsubscribe_placeholder_link(self, **post):
"""Dummy route so placeholder is not prefixed by language, MUST have multilang=False"""
raise werkzeug.exceptions.NotFound()
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing(self, mailing_id, email=None, res_id=None, token="", **post):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
+5 -5
View File
@@ -29,12 +29,12 @@ class StockWarehouse(models.Model):
return result
def _get_manufacture_route_id(self):
manufacture_route_id = self.env.ref('mrp.route_warehouse0_manufacture').id
if not manufacture_route_id:
manufacture_route_id = self.env['stock.location.route'].search([('name', 'like', _('Manufacture'))], limit=1).id
if not manufacture_route_id:
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture', raise_if_not_found=False)
if not manufacture_route:
manufacture_route = self.env['stock.location.route'].search([('name', 'like', _('Manufacture'))], limit=1)
if not manufacture_route:
raise exceptions.UserError(_('Can\'t find any generic Manufacture route.'))
return manufacture_route_id
return manufacture_route.id
def _get_manufacture_pull_rules_values(self, route_values):
if not self.manu_type_id:
+1 -1
View File
@@ -72,7 +72,7 @@
</page>
<page string="Messages">
<group>
<field name="pre_msg"/>
<field name="pre_msg" invisible="1"/>
<field name="post_msg"/>
<field name="pending_msg"/>
<field name="done_msg"/>
+4 -3
View File
@@ -140,9 +140,10 @@ img, .media_iframe_video, .o_image {
margin-right: $grid-gutter-width / 2;
}
}
img {
// Make all images responsive by default but with inline-block display
@include img-responsive(inline-block);
img.o_we_custom_image {
// Images added with the editor are .img-responsive by default but should
// still behave like inline content.
display: inline-block;
}
// Others
@@ -17,7 +17,9 @@
<tbody>
<tr style="width: 1in;">
<td style="border: 2px solid black;text-align: center; vertical-align: middle;" class="col-xs-5">
<img t-if="product.barcode" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<img t-if="product.barcode and len(product.barcode) == 13" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN13', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<img t-elif="product.barcode and len(product.barcode) == 8" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN8', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<img t-else="" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', product.barcode, 600, 150)" style="width:100%;height:20%;"/>
<span t-field="product.barcode"/>
</td>
<td style="border: 2px solid black; text-align: center;" class="col-xs-7">
+48 -1
View File
@@ -34,13 +34,14 @@
# David Arnold <blaggacao@users.noreply.github.com>, 2018
# Lina Maria Avendaño Carvajal <lina8823@gmail.com>, 2018
# Gustavo Valverde, 2018
# Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-22 14:10+0000\n"
"PO-Revision-Date: 2018-03-22 14:10+0000\n"
"Last-Translator: Gustavo Valverde, 2018\n"
"Last-Translator: Carlos Vásquez <carlos.vasquez@clearcorp.co.cr>, 2018\n"
"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -78,6 +79,32 @@ msgid ""
"% endif\n"
"</p>\n"
msgstr ""
"\n"
"<p>Estimado(a)\n"
"% if object.partner_id.is_company and object.partner_id.child_ids:\n"
" ${object.partner_id.child_ids[0].name}\n"
"% else :\n"
" ${object.partner_id.name}\n"
"% endif\n"
",</p><p>\n"
"Aqui se encuenta, en adjunto, una ${object.state in ('draft', 'sent') and 'solicitud de presupuesto' or 'confirmación de orden de compra'} <strong>${object.name}</strong>\n"
"% if object.partner_ref:\n"
" con referencia: ${object.partner_ref}\n"
"% endif\n"
"% if object.origin:\n"
" (SdP origen: ${object.origin})\n"
"% endif\n"
"con un monto de <strong>${format_amount(object.amount_total, object.currency_id)}</strong>\n"
"de ${object.company_id.name}.\n"
"</p>\n"
"\n"
"<p>No dude en contactarnos en caso de tener alguna inquietud.</p>\n"
"<p>Saludos cordiales,</p>\n"
"<p style=\"color:#888888;\">\n"
"% if object.user_id and object.user_id.signature:\n"
" ${object.user_id.signature | safe}\n"
"% endif\n"
"</p>\n"
#. module: purchase
#: model:mail.template,body_html:purchase.email_template_edi_purchase_done
@@ -103,6 +130,26 @@ msgid ""
"<p>You can reply to this email if you have any questions.</p>\n"
"<p>Thank you,</p>\n"
msgstr ""
"\n"
"<p>Estimado(a) ${object.partner_id.name} \n"
"% if object.partner_id.parent_id:\n"
" (<i>${object.partner_id.parent_id.name}</i>)\n"
"% endif\n"
",</p>\n"
"<p>\n"
"Adjunto le remito la <strong>${object.state in ('draft', 'sent') and 'solicitud de presupuesto' or 'confirmación de pedido de compra} ${object.name}</strong>\n"
"% if object.partner_ref:\n"
" con su referencia: ${object.partner_ref}\n"
"% endif\n"
"% if object.origin:\n"
" (SdP origen: ${object.origin})\n"
"% endif\n"
"con un monto de <strong>${object.amount_total} ${object.currency_id.name}</strong>\n"
"de ${object.company_id.name}.\n"
"</p>\n"
"\n"
"<p>Si tiene alguna duda, puede responder este email.</p>\n"
"<p>Gracias,</p>\n"
#. module: purchase
#: code:addons/purchase/models/stock.py:100
@@ -56,7 +56,6 @@ class MrpStockReport(models.TransientModel):
lines = move_line.search([
('lot_id', '=', context.get('lot_name') or rec_id),
('state', '=', 'done'),
('move_id.returned_move_ids', '=', False),
])
if model == 'stock.move.line':
for line in lines:
@@ -80,6 +80,9 @@ var ControlPanel = Widget.extend({
}
this.bus = new Bus(this);
// the updateIndex is used to prevent concurrent updates of the control
// panel depending on asynchronous code to be executed in the wrong order
this.bus.updateIndex = 0;
this.bus.on("update", this, this.update);
},
/**
@@ -126,6 +129,8 @@ var ControlPanel = Widget.extend({
* elements that are not in status.cp_content
*/
update: function(status, options) {
this.bus.updateIndex++;
this._toggle_visibility(!status.hidden);
// Don't update the ControlPanel in headless mode as the views have
@@ -235,11 +235,26 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
var self = this;
var shouldReload = (options && 'reload' in options) ? options.reload : true;
var def = shouldReload ? this.model.reload(this.handle, params) : $.when();
// we check here that the updateIndex of the control panel hasn't changed
// between the start of the update request and the moment the controller
// asks the control panel to update itself ; indeed, it could happen that
// another action/controller is executed during this one reloads itself,
// and if that one finishes first, it replaces this controller in the DOM,
// and this controller should no longer update the control panel.
// note that this won't be necessary as soon as each controller will have
// its own control panel
var cpUpdateIndex = this.cp_bus && this.cp_bus.updateIndex;
return this.dp.add(def).then(function (handle) {
if (self.cp_bus && cpUpdateIndex !== self.cp_bus.updateIndex) {
return;
}
self.handle = handle || self.handle; // update handle if we reloaded
var state = self.model.get(self.handle);
var localState = self.renderer.getLocalState();
return self.dp.add(self.renderer.updateState(state, params)).then(function () {
if (self.cp_bus && cpUpdateIndex !== self.cp_bus.updateIndex) {
return;
}
self.renderer.setLocalState(localState);
self._update(state);
});
@@ -886,6 +886,15 @@ var BasicModel = AbstractModel.extend({
// x2Many case: the new record has been stored in _changes, as a
// command so we remove the command(s) related to that record
parent._changes = _.filter(parent._changes, function (change) {
if (change.id === elementID &&
change.operation === 'ADD' && // For now, only an ADD command increases limits
parent.tempLimitIncrement) {
// The record will be deleted from the _changes.
// So we won't be passing into the logic of _applyX2ManyOperations anymore
// implying that we have to cancel out the effects of an ADD command here
parent.tempLimitIncrement--;
parent.limit--;
}
return change.id !== elementID;
});
} else {
@@ -3138,6 +3147,15 @@ var BasicModel = AbstractModel.extend({
// when sent to the server, the x2manys values must be a list
// of commands in a context, but the list of ids in a domain
ids.toJSON = _generateX2ManyCommands.bind(null, fieldName);
} else if (field.type === 'one2many') { // Ids are evaluated as a list of ids
/* Filtering out virtual ids from the ids list
* The server will crash if there are virtual ids in there
* The webClient doesn't do literal id list comparison like ids == list
* Only relevant in o2m: m2m does create actual records in db
*/
ids = _.filter(ids, function (id) {
return typeof id !== 'string';
});
}
context[fieldName] = ids;
}
@@ -661,15 +661,23 @@ var BasicRenderer = AbstractRenderer.extend({
// Prepare widget rendering and save the related deferred
var def = widget._widgetRenderAndInsert(function () {});
if (def.state() === 'pending') {
var async = def.state() === 'pending';
if (async) {
this.defs.push(def);
}
var $el = async ? $('<div>') : widget.$el;
// handle other attributes/modifiers
this._handleAttributes(widget.$el, node);
this._registerModifiers(node, record, widget);
widget.$el.addClass('o_widget');
return widget.$el;
var self = this;
def.then(function () {
self._handleAttributes(widget.$el, node);
self._registerModifiers(node, record, widget);
widget.$el.addClass('o_widget');
if (async) {
$el.replaceWith(widget.$el);
}
});
return $el;
},
/**
@@ -1559,6 +1559,66 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
QUnit.test('open a record while reloading the list view', function (assert) {
assert.expect(12);
var def;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route) {
var result = this._super.apply(this, arguments);
if (route === '/web/dataset/search_read') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view");
assert.strictEqual(actionManager.$('.o_list_view .o_data_row').length, 5,
"list view should contain 5 records");
assert.strictEqual($('.o_control_panel .o_list_buttons').length, 1,
"list view buttons should be displayed in control panel");
// reload (the search_read RPC will be blocked)
def = $.Deferred();
$('.o_control_panel .o_cp_switch_list').click(); // click on the switch button
assert.strictEqual(actionManager.$('.o_list_view .o_data_row').length, 5,
"list view should still contain 5 records");
assert.strictEqual($('.o_control_panel .o_list_buttons').length, 1,
"list view buttons should still be displayed in control panel");
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should display the form view");
assert.strictEqual($('.o_control_panel .o_list_buttons').length, 0,
"list view buttons should no longer be displayed in control panel");
assert.strictEqual($('.o_control_panel .o_form_buttons_view').length, 1,
"form view buttons should be displayed instead");
// unblock the search_read RPC
def.resolve();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should display the form view");
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should not display the list view");
assert.strictEqual($('.o_control_panel .o_list_buttons').length, 0,
"list view buttons should still not be displayed in control panel");
assert.strictEqual($('.o_control_panel .o_form_buttons_view').length, 1,
"form view buttons should still be displayed instead");
actionManager.destroy();
});
QUnit.module('Client Actions');
QUnit.test('can execute client actions from tag name', function (assert) {
@@ -2551,6 +2551,117 @@ QUnit.module('relational_fields', {
QUnit.module('FieldOne2Many');
QUnit.test('O2M List with pager, decoration and default_order: add and cancel adding', function (assert) {
assert.expect(3);
// The decoration on the list implies that its condition will be evaluated
// against the data of the field (actual records *displayed*)
// If one data is wrongly formed, it will crash
// This test adds then cancels a record in a paged, ordered, and decorated list
// That implies prefetching of records for sorting
// and evaluation of the decoration against *visible records*
this.data.partner.records[0].p = [2,4];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="p">' +
'<tree editable="bottom" limit="1" decoration-muted="foo != False" default_order="display_name">' +
'<field name="foo" invisible="1"/>' +
'<field name="display_name" />' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
form.$('.o_field_x2many_list .o_field_x2many_list_row_add a').click();
var $dataRows = form.$('.o_field_x2many_list .o_data_row');
assert.equal($dataRows.length, 2,
'There should be 2 rows');
var $expectedSelectedRow = $dataRows.eq(1);
var $actualSelectedRow = form.$('.o_selected_row');
assert.equal($actualSelectedRow[0], $expectedSelectedRow[0],
'The selected row should be the new one');
// Cancel Creation
var escapeKey = $.ui.keyCode.ESCAPE;
$actualSelectedRow.find('input').trigger(
$.Event('keydown', {which: escapeKey, keyCode: escapeKey}));
$dataRows = form.$('.o_field_x2many_list .o_data_row');
assert.equal($dataRows.length, 1,
'There should be 1 row');
form.destroy();
});
QUnit.test('O2M with parented m2o and domain on parent.m2o', function (assert) {
assert.expect(3);
/* records in an o2m can have a m2o pointing to themselves
* in that case, a domain evaluation on that field followed by name_search
* shouldn't send virtual_ids to the server
*/
this.data.turtle.fields.parent_id = {string: "Parent", type: "many2one", relation: 'turtle'};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="turtles">' +
'<tree>' +
'<field name="parent_id" />' +
'</tree>' +
'</field>' +
'</form>',
archs: {
'turtle,false,form': '<form><field name="parent_id" domain="[(\'id\', \'in\', parent.turtles)]"/></form>',
},
mockRPC: function(route, args) {
if (route === '/web/dataset/call_kw/turtle/name_search') {
// We are going to pass twice here
// First time, we really have nothing
// Second time, a virtual_id has been created
assert.deepEqual(args.kwargs.args, [['id', 'in', []]]);
}
return this._super(route, args);
}
});
form.$('.o_field_x2many_list[name=turtles] .o_field_x2many_list_row_add a').click();
var $modal = $('.modal-content');
var $turtleParent = $modal.find('.o_field_many2one input');
var $dropdown = $turtleParent.autocomplete('widget');
$turtleParent.click();
$dropdown.find('li.o_m2o_dropdown_option:contains(Create)').first().mouseenter().click();
$modal = $('.modal-content');
$modal.eq(1).find('.modal-footer .btn-primary').eq(0).click(); // Confirm new Record
$modal.eq(0).find('.modal-footer .btn-primary').eq(1).click(); // Save & New
assert.equal(form.$('.o_data_row').length, 1,
'The main record should have the new record in its o2m');
$modal = $('.modal-content');
$modal.find('.o_field_many2one input').click();
form.destroy();
});
QUnit.test('one2many list editable with cell readonly modifier', function (assert) {
assert.expect(4);
@@ -7248,6 +7248,35 @@ QUnit.module('Views', {
assert.ok("Behavior can't be tested");
});
QUnit.test('asynchronous rendering of a widget tag', function (assert) {
assert.expect(1);
var def1 = $.Deferred();
var MyWidget = Widget.extend({
willStart: function() {
return def1;
},
});
widgetRegistry.add('test', MyWidget);
createAsyncView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<widget name="test"/>' +
'</form>',
}).then(function(form) {
assert.strictEqual(form.$('div.o_widget').length, 1,
"there should be a div with widget class");
form.destroy();
delete widgetRegistry.map.test;
});
def1.resolve();
});
});
});
+10 -3
View File
@@ -304,9 +304,12 @@ var RTEWidget = Widget.extend({
});
// start element observation
$(document).on('content_changed', '.o_editable', function (event) {
self.trigger_up('rte_change', {target: event.target});
$(this).addClass('o_dirty');
$(document).on('content_changed', '.o_editable', function (ev) {
self.trigger_up('rte_change', {target: ev.target});
if (!ev.__isDirtyHandled) {
$(this).addClass('o_dirty');
ev.__isDirtyHandled = true;
}
});
$('#wrapwrap, .o_editable').on('click.rte', '*', this, this._onClick.bind(this));
@@ -424,6 +427,10 @@ var RTEWidget = Widget.extend({
save: function (context) {
var self = this;
$('.o_editable')
.destroy()
.removeClass('o_editable o_is_inline_editable');
var $dirty = $('.o_dirty');
$dirty
.removeAttr('contentEditable')
@@ -308,7 +308,10 @@ var ImageWidget = MediaWidget.extend({
img.src += _.str.sprintf('?access_token=%s', img.access_token);
}
if (!self.$media.is('img')) {
self._replaceMedia($('<img/>'));
// Note: by default the images receive the bootstrap opt-in
// img-responsive class. We cannot make them all responsive
// by design because of libraries and client databases img.
self._replaceMedia($('<img/>', {class: 'img-responsive o_we_custom_image'}));
}
self.$media.attr('src', img.src);
File diff suppressed because it is too large Load Diff
+8
View File
@@ -259,6 +259,14 @@
<rng:optional><rng:attribute name="avatar_field" /></rng:optional>
<rng:optional><rng:attribute name="write_model" /></rng:optional>
<rng:optional><rng:attribute name="write_field" /></rng:optional>
<rng:optional>
<rng:attribute name="force_save">
<rng:choice>
<rng:value>1</rng:value>
<rng:value>0</rng:value>
</rng:choice>
</rng:attribute>
</rng:optional>
<!-- Widget *static* options defined as an arbitrary JSON dict, with
widget-dependent parameters. To be ignored if widget/client does
not support them. -->