[MERGE] forward port branch 11.0 up to b05e4d5f95
This commit is contained in:
@@ -50,7 +50,7 @@
|
||||
<td><span t-field="aml.date"/></td>
|
||||
<td><span t-field="aml.account_id.code"/></td>
|
||||
<td><span t-esc="aml.sudo().partner_id and aml.sudo().partner_id.name and aml.sudo().partner_id.name[:23] or ''"/></td>
|
||||
<td><span t-esc="aml.name[:35]"/></td>
|
||||
<td><span t-esc="aml.name and aml.name[:35]"/></td>
|
||||
<td><span t-esc="aml.debit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td>
|
||||
<td><span t-esc="aml.credit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td>
|
||||
<td t-if="data['form']['amount_currency'] and aml.amount_currency">
|
||||
|
||||
@@ -10,7 +10,7 @@ from odoo.tools.translate import _
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
country_enforce_cities = fields.Boolean(related='country_id.enforce_cities')
|
||||
country_enforce_cities = fields.Boolean(related='country_id.enforce_cities', readonly=True)
|
||||
city_id = fields.Many2one('res.city', string='City of Address')
|
||||
|
||||
@api.onchange('city_id')
|
||||
|
||||
@@ -443,9 +443,14 @@ class EventRegistration(models.Model):
|
||||
@api.multi
|
||||
def message_get_suggested_recipients(self):
|
||||
recipients = super(EventRegistration, self).message_get_suggested_recipients()
|
||||
public_users = self.env['res.users'].sudo()
|
||||
public_groups = self.env.ref("base.group_public", raise_if_not_found=False)
|
||||
if public_groups:
|
||||
public_users = public_groups.sudo().with_context(active_test=False).mapped("users")
|
||||
try:
|
||||
for attendee in self:
|
||||
if attendee.partner_id:
|
||||
is_public = attendee.sudo().with_context(active_test=False).partner_id.user_ids in public_users if public_users else False
|
||||
if attendee.partner_id and not is_public:
|
||||
attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer'))
|
||||
elif attendee.email:
|
||||
attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email'))
|
||||
|
||||
@@ -9,8 +9,10 @@ class ResPartner(models.Model):
|
||||
event_count = fields.Integer("Events", compute='_compute_event_count', help="Number of events the partner has participated.")
|
||||
|
||||
def _compute_event_count(self):
|
||||
if not self.user_has_groups('event.group_event_user'):
|
||||
return
|
||||
for partner in self:
|
||||
partner.event_count = self.env['event.event'].sudo().search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
|
||||
partner.event_count = self.env['event.event'].search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
|
||||
|
||||
@api.multi
|
||||
def action_event_view(self):
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<button class="oe_stat_button"
|
||||
type="object"
|
||||
icon="fa-ticket"
|
||||
groups="event.group_event_user"
|
||||
name="action_event_view" attrs="{'invisible': [('event_count','=', 0)]}">
|
||||
<field string="Events" name="event_count" widget="statinfo"/>
|
||||
</button>
|
||||
|
||||
@@ -593,7 +593,7 @@
|
||||
<field name="refund_account_id" ref="vat_refund_h_non_eu"/>
|
||||
<field name="account_id" ref="vat_refund_h_non_eu"/>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tag_nl_34')])]"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tag_nl_33')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_21_niet_eu"/>
|
||||
</record>
|
||||
<record id="btw_E2" model="account.tax.template">
|
||||
@@ -735,7 +735,7 @@
|
||||
<field name="refund_account_id" ref="vat_refund_h_d_non_eu"/>
|
||||
<field name="account_id" ref="vat_refund_h_d_non_eu"/>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tag_nl_34')])]"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('tag_nl_33')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_21_niet_eu"/>
|
||||
</record>
|
||||
<record id="btw_E2_d" model="account.tax.template">
|
||||
|
||||
@@ -1545,7 +1545,7 @@ var ChatManager = AbstractService.extend({
|
||||
}
|
||||
this._removeChannel(channel);
|
||||
this.chatBus.trigger("unsubscribe_from_channel", data.id);
|
||||
web_client.do_notify(_("Unsubscribed"), msg);
|
||||
web_client.do_notify(_t("Unsubscribed"), msg);
|
||||
}
|
||||
} else if (data.type === 'toggle_star') {
|
||||
this._manageToggleStarNotification(data);
|
||||
|
||||
@@ -202,12 +202,13 @@
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="time_ids" nolabel="1">
|
||||
<field name="time_ids" nolabel="1" context="{'default_workcenter_id': workcenter_id}">
|
||||
<tree>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="duration" widget="float_time" sum="Total duration"/>
|
||||
<field name="user_id"/>
|
||||
<field name="workcenter_id" invisible="1"/>
|
||||
<field name="loss_id" string="Efficiency"/>
|
||||
</tree>
|
||||
<form>
|
||||
@@ -219,6 +220,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="user_id"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="loss_id"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -110,7 +110,7 @@ class OgoneController(http.Controller):
|
||||
def feedback(self, **kwargs):
|
||||
try:
|
||||
tx = request.env['payment.transaction'].sudo()._ogone_form_get_tx_from_data(kwargs)
|
||||
tx._ogone_s2s_validate()
|
||||
tx._ogone_s2s_validate_tree(kwargs)
|
||||
except ValidationError:
|
||||
return 'ko'
|
||||
return 'ok'
|
||||
|
||||
@@ -143,7 +143,7 @@ class CustomerPortal(CustomerPortal):
|
||||
# pager
|
||||
pager = portal_pager(
|
||||
url="/my/tasks",
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby},
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby, 'search_in': search_in, 'search': search},
|
||||
total=task_count,
|
||||
page=page,
|
||||
step=self._items_per_page
|
||||
|
||||
@@ -536,7 +536,7 @@ class PurchaseOrder(models.Model):
|
||||
result['context'] = {}
|
||||
pick_ids = self.mapped('picking_ids')
|
||||
#choose the view_mode accordingly
|
||||
if len(pick_ids) > 1:
|
||||
if not pick_ids or len(pick_ids) > 1:
|
||||
result['domain'] = "[('id','in',%s)]" % (pick_ids.ids)
|
||||
elif len(pick_ids) == 1:
|
||||
res = self.env.ref('stock.view_picking_form', False)
|
||||
|
||||
@@ -661,7 +661,7 @@ class StockMove(models.Model):
|
||||
self.product_uom = product.uom_id.id
|
||||
return {'domain': {'product_uom': [('category_id', '=', product.uom_id.category_id.id)]}}
|
||||
|
||||
@api.onchange('date')
|
||||
@api.onchange('date_expected')
|
||||
def onchange_date(self):
|
||||
if self.date_expected:
|
||||
self.date = self.date_expected
|
||||
@@ -872,11 +872,6 @@ class StockMove(models.Model):
|
||||
package_id=package_id, owner_id=owner_id, strict=strict
|
||||
)
|
||||
except UserError:
|
||||
# If it raises here, it means that the `available_quantity` brought by a done move line
|
||||
# is not available on the quants itself. This could be the result of an inventory
|
||||
# adjustment that removed totally of partially `available_quantity`. When this happens, we
|
||||
# chose to do nothing. This situation could not happen on MTS move, because in this case
|
||||
# `available_quantity` is directly the quantity on the quants themselves.
|
||||
taken_quantity = 0
|
||||
|
||||
# Find a candidate move line to update or create a new one.
|
||||
@@ -992,7 +987,17 @@ class StockMove(models.Model):
|
||||
available_move_lines[(move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)] -= move_line.product_qty
|
||||
for (location_id, lot_id, package_id, owner_id), quantity in available_move_lines.items():
|
||||
need = move.product_qty - sum(move.move_line_ids.mapped('product_qty'))
|
||||
taken_quantity = move._update_reserved_quantity(need, quantity, location_id, lot_id, package_id, owner_id)
|
||||
# `quantity` is what is brought by chained done move lines. We double check
|
||||
# here this quantity is available on the quants themselves. If not, this
|
||||
# could be the result of an inventory adjustment that removed totally of
|
||||
# partially `quantity`. When this happens, we chose to reserve the maximum
|
||||
# still available. This situation could not happen on MTS move, because in
|
||||
# this case `quantity` is directly the quantity on the quants themselves.
|
||||
available_quantity = self.env['stock.quant']._get_available_quantity(
|
||||
move.product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
|
||||
if float_is_zero(available_quantity, precision_rounding=move.product_id.uom_id.rounding):
|
||||
continue
|
||||
taken_quantity = move._update_reserved_quantity(need, min(quantity, available_quantity), location_id, lot_id, package_id, owner_id)
|
||||
if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
|
||||
continue
|
||||
if need - taken_quantity == 0.0:
|
||||
|
||||
@@ -105,7 +105,9 @@
|
||||
<td>
|
||||
<t t-if="has_barcode">
|
||||
<span t-if="move.product_id and move.product_id.barcode">
|
||||
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-if="move.product_id.barcode and len(move.product_id.barcode) == 13" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN13', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-elif="move.product_id.barcode and len(move.product_id.barcode) == 8" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN8', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-else="" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
</span>
|
||||
</t>
|
||||
</td>
|
||||
|
||||
@@ -1751,7 +1751,7 @@ class StockMove(TransactionCase):
|
||||
ml.qty_done = 1
|
||||
res_dict_for_back_order = picking_pack_cust.button_validate()
|
||||
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
|
||||
backorder_wizard.with_context(debug=True).process()
|
||||
backorder_wizard.process()
|
||||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pack_cust.id)])
|
||||
backordered_move = backorder.move_lines
|
||||
|
||||
@@ -1782,6 +1782,57 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.customer_location), 3)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.pack_location), 0)
|
||||
|
||||
def test_link_assign_10(self):
|
||||
""" Test the assignment mechanism with partial availability.
|
||||
"""
|
||||
# make some stock:
|
||||
# stock location: 2.0
|
||||
# pack location: -1.0
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 2.0)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
|
||||
|
||||
move_out = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_out',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
})
|
||||
move_out._action_confirm()
|
||||
move_out._action_assign()
|
||||
move_out.quantity_done = 1.0
|
||||
move_out._action_done()
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.pack_location)), 1.0)
|
||||
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_1_1',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_1_2',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
move_stock_pack._action_assign()
|
||||
move_stock_pack.quantity_done = 2.0
|
||||
move_stock_pack._action_done()
|
||||
self.assertEqual(len(move_pack_cust.move_line_ids), 1)
|
||||
|
||||
self.assertAlmostEqual(move_pack_cust.reserved_availability, 1.0)
|
||||
self.assertEqual(move_pack_cust.state, 'partially_available')
|
||||
|
||||
def test_use_unreserved_move_line_1(self):
|
||||
""" Test that validating a stock move linked to an untracked product reserved by another one
|
||||
correctly unreserves the other one.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<templates>
|
||||
<t t-name="date-simple"><t t-esc='value' /></t>
|
||||
<params id="date-simple">{"value": "1988-09-16"}</params>
|
||||
<result id="date-simple">1988-09-16</result>
|
||||
|
||||
<t t-name="datetime-simple"><t t-esc='value' /></t>
|
||||
<params id="datetime-simple">{"value": "1988-09-16 14:00:00"}</params>
|
||||
<result id="datetime-simple">1988-09-16 14:00:00</result>
|
||||
|
||||
<t t-name="datetime-widget-datetime"><t t-esc='value' t-options="{'widget': 'datetime'}" /></t>
|
||||
<params id="datetime-widget-datetime">{"value": "1988-09-16 14:00:00"}</params>
|
||||
<result id="datetime-widget-datetime">09/16/1988 16:00:00</result>
|
||||
|
||||
<t t-name="datetime-widget-date"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
|
||||
<params id="datetime-widget-date">{"value": "1988-09-16 14:00:00"}</params>
|
||||
<result id="datetime-widget-date">09/16/1988</result>
|
||||
|
||||
<t t-name="datetime-widget-date-tz2"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
|
||||
<params id="datetime-widget-date-tz2">{"value": "1988-09-16 01:00:00"}</params>
|
||||
<result id="datetime-widget-date-tz2">09/16/1988</result>
|
||||
|
||||
<t t-name="datetime-widget-date-tz"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
|
||||
<params id="datetime-widget-date-tz">{"value": "1988-09-16 23:00:00"}</params>
|
||||
<result id="datetime-widget-date-tz">09/17/1988</result>
|
||||
|
||||
</templates>
|
||||
@@ -169,7 +169,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, {
|
||||
core.bus.on('connection_restored', this, this.on_connection_restored);
|
||||
|
||||
// crash manager integration
|
||||
session.on('error', crash_manager, crash_manager.rpc_error);
|
||||
core.bus.on('rpc_error', crash_manager, crash_manager.rpc_error);
|
||||
window.onerror = function (message, file, line, col, error) {
|
||||
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
|
||||
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
|
||||
|
||||
@@ -26,9 +26,9 @@ var Loading = Widget.extend({
|
||||
this._super(parent);
|
||||
this.count = 0;
|
||||
this.blocked_ui = false;
|
||||
session.on("request", this, this.request_call);
|
||||
session.on("response", this, this.response_call);
|
||||
session.on("response_failed", this, this.response_call);
|
||||
core.bus.on('rpc_request', this, this.request_call);
|
||||
core.bus.on("rpc_response", this, this.response_call);
|
||||
core.bus.on("rpc_response_failed", this, this.response_call);
|
||||
},
|
||||
destroy: function() {
|
||||
this.on_rpc_event(-this.count);
|
||||
|
||||
@@ -5,7 +5,12 @@ var core = require('web.core');
|
||||
var utils = require('web.utils');
|
||||
var time = require('web.time');
|
||||
|
||||
function genericJsonRpc (fct_name, params, fct) {
|
||||
function genericJsonRpc (fct_name, params, settings, fct) {
|
||||
var shadow = settings.shadow || false;
|
||||
delete settings.shadow;
|
||||
if (! shadow)
|
||||
core.bus.trigger('rpc_request');
|
||||
|
||||
var data = {
|
||||
jsonrpc: "2.0",
|
||||
method: fct_name,
|
||||
@@ -30,11 +35,49 @@ function genericJsonRpc (fct_name, params, fct) {
|
||||
});
|
||||
// FIXME: jsonp?
|
||||
result.abort = function () { if (xhr.abort) xhr.abort(); };
|
||||
return result;
|
||||
|
||||
var p = result.then(function (result) {
|
||||
if (!shadow) {
|
||||
core.bus.trigger('rpc_response');
|
||||
}
|
||||
return result;
|
||||
}, function (type, error, textStatus, errorThrown) {
|
||||
if (type === "server") {
|
||||
if (!shadow) {
|
||||
core.bus.trigger('rpc_response');
|
||||
}
|
||||
if (error.code === 100) {
|
||||
core.bus.trigger('invalidate_session');
|
||||
}
|
||||
return $.Deferred().reject(error, $.Event());
|
||||
} else {
|
||||
if (!shadow) {
|
||||
core.bus.trigger('rpc_response_failed');
|
||||
}
|
||||
var nerror = {
|
||||
code: -32098,
|
||||
message: "XmlHttpRequestError " + errorThrown,
|
||||
data: {
|
||||
type: "xhr"+textStatus,
|
||||
debug: error.responseText,
|
||||
objects: [error, errorThrown]
|
||||
},
|
||||
};
|
||||
return $.Deferred().reject(nerror, $.Event());
|
||||
}
|
||||
});
|
||||
return p.fail(function () { // Allow deferred user to disable rpc_error call in fail
|
||||
p.fail(function (error, event) {
|
||||
if (!event.isDefaultPrevented()) {
|
||||
core.bus.trigger('rpc_error', error, event);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function jsonRpc(url, fct_name, params, settings) {
|
||||
return genericJsonRpc(fct_name, params, function(data) {
|
||||
settings = settings || {};
|
||||
return genericJsonRpc(fct_name, params, settings, function(data) {
|
||||
return $.ajax(url, _.extend({}, settings, {
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
@@ -47,7 +90,7 @@ function jsonRpc(url, fct_name, params, settings) {
|
||||
|
||||
function jsonpRpc(url, fct_name, params, settings) {
|
||||
settings = settings || {};
|
||||
return genericJsonRpc(fct_name, params, function(data) {
|
||||
return genericJsonRpc(fct_name, params, settings, function(data) {
|
||||
var payload_str = JSON.stringify(data, time.date_to_utc);
|
||||
var payload_url = $.param({r:payload_str});
|
||||
var force2step = settings.force2step || false;
|
||||
|
||||
@@ -49,6 +49,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
this.qweb_mutex = new concurrency.Mutex();
|
||||
this.currencies = {};
|
||||
this._groups_def = {};
|
||||
core.bus.on('invalidate_session', this, this._onInvalidateSession);
|
||||
},
|
||||
setup: function (origin, options) {
|
||||
// must be able to customize server
|
||||
@@ -327,23 +328,17 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
rpc: function (url, params, options) {
|
||||
var self = this;
|
||||
options = _.clone(options || {});
|
||||
var shadow = options.shadow || false;
|
||||
options.headers = _.extend({}, options.headers);
|
||||
if (odoo.debug) {
|
||||
options.headers["X-Debug-Mode"] = $.deparam($.param.querystring()).debug;
|
||||
}
|
||||
|
||||
delete options.shadow;
|
||||
|
||||
return self.check_session_id().then(function () {
|
||||
// TODO: remove
|
||||
if (! _.isString(url)) {
|
||||
_.extend(options, url);
|
||||
url = url.url;
|
||||
}
|
||||
// TODO correct handling of timeouts
|
||||
if (! shadow)
|
||||
self.trigger('request');
|
||||
var fct;
|
||||
if (self.origin_server) {
|
||||
fct = ajax.jsonRpc;
|
||||
@@ -362,37 +357,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
url = self.url(url, null);
|
||||
options.session_id = self.session_id || '';
|
||||
}
|
||||
var p = fct(url, "call", params, options);
|
||||
p = p.then(function (result) {
|
||||
if (! shadow)
|
||||
self.trigger('response');
|
||||
return result;
|
||||
}, function (type, error, textStatus, errorThrown) {
|
||||
if (type === "server") {
|
||||
if (! shadow)
|
||||
self.trigger('response');
|
||||
if (error.code === 100) {
|
||||
self.uid = false;
|
||||
}
|
||||
return $.Deferred().reject(error, $.Event());
|
||||
} else {
|
||||
if (! shadow)
|
||||
self.trigger('response_failed');
|
||||
var nerror = {
|
||||
code: -32098,
|
||||
message: "XmlHttpRequestError " + errorThrown,
|
||||
data: {type: "xhr"+textStatus, debug: error.responseText, objects: [error, errorThrown] }
|
||||
};
|
||||
return $.Deferred().reject(nerror, $.Event());
|
||||
}
|
||||
});
|
||||
return p.fail(function () { // Allow deferred user to disable rpc_error call in fail
|
||||
p.fail(function (error, event) {
|
||||
if (!event.isDefaultPrevented()) {
|
||||
self.trigger('error', error, event);
|
||||
}
|
||||
});
|
||||
});
|
||||
return fct(url, "call", params, options);
|
||||
});
|
||||
},
|
||||
url: function (path, params) {
|
||||
@@ -418,6 +383,17 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
getTZOffset: function (date) {
|
||||
return -new Date(date).getTimezoneOffset();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onInvalidateSession: function () {
|
||||
this.uid = false;
|
||||
},
|
||||
});
|
||||
|
||||
return Session;
|
||||
|
||||
@@ -482,7 +482,13 @@ var FieldDate = InputField.extend({
|
||||
* @private
|
||||
*/
|
||||
_makeDatePicker: function () {
|
||||
return new datepicker.DateWidget(this, {defaultDate: this.value});
|
||||
return new datepicker.DateWidget(
|
||||
this,
|
||||
_.defaults(
|
||||
this.nodeOptions.datepicker || {},
|
||||
{defaultDate: this.value}
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -628,9 +628,9 @@ define([
|
||||
}
|
||||
|
||||
var anchors = [];
|
||||
// ODOO: adding this branch to modify existing anchor
|
||||
// ODOO: adding this branch to modify existing anchor if it fully contains the range
|
||||
var ancestor_anchor = dom.ancestor(rng.sc, dom.isAnchor);
|
||||
if(ancestor_anchor) {
|
||||
if(ancestor_anchor && ancestor_anchor === dom.ancestor(rng.ec, dom.isAnchor)) {
|
||||
anchors.push($(ancestor_anchor).html(linkText).get(0));
|
||||
} else if (isTextChanged) {
|
||||
// Create a new link when text changed.
|
||||
|
||||
@@ -474,7 +474,7 @@ function prettify_html(html) {
|
||||
while (i--) space += ' ';
|
||||
return space;
|
||||
},
|
||||
reg = /^<\/?(a|span|font|strong|u|i|strong|b)(\s|>)/i,
|
||||
reg = /^<\/?(a|span|font|u|em|i|strong|b)(\s|>)/i,
|
||||
inline_level = Infinity,
|
||||
tokens = _.compact(_.flatten(_.map(html.split(/</), function (value) {
|
||||
value = value.replace(/\s+/g, ' ').split(/>/);
|
||||
|
||||
@@ -1398,6 +1398,8 @@ var LinkDialog = Dialog.extend({
|
||||
if (dom.ancestor(nodes[i], dom.isImg)) {
|
||||
this.data.images.push(dom.ancestor(nodes[i], dom.isImg));
|
||||
text += '[IMG]';
|
||||
} else if (!is_link && nodes[i].nodeType === 1) {
|
||||
// just use text nodes from listBetween
|
||||
} else if (!is_link && i===0) {
|
||||
text += nodes[i].textContent.slice(so, Infinity);
|
||||
} else if (!is_link && i===nodes.length-1) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import traceback
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import pytz
|
||||
import werkzeug
|
||||
import werkzeug.routing
|
||||
import werkzeug.utils
|
||||
@@ -74,6 +75,10 @@ class Http(models.AbstractModel):
|
||||
context = {}
|
||||
if not request.context.get('tz'):
|
||||
context['tz'] = request.session.get('geoip', {}).get('time_zone')
|
||||
try:
|
||||
pytz.timezone(context['tz'] or '')
|
||||
except pytz.UnknownTimeZoneError:
|
||||
context.pop('tz')
|
||||
|
||||
request.website = request.env['website'].get_current_website() # can use `request.env` since auth methods are called
|
||||
context['website_id'] = request.website.id
|
||||
|
||||
@@ -261,6 +261,6 @@ class WebsiteEventController(http.Controller):
|
||||
Attendees._prepare_attendee_values(registration))
|
||||
|
||||
return request.render("website_event.registration_complete", {
|
||||
'attendees': Attendees,
|
||||
'attendees': Attendees.sudo(),
|
||||
'event': event,
|
||||
})
|
||||
|
||||
@@ -14,7 +14,6 @@ List of contributors:
|
||||
|
||||
Aarón Henríquez ahenriquez@eficent.com https://github.com/aheficent
|
||||
Lois Rilo lois.rilo@eficent.com https://github.com/lreficent
|
||||
Roser Garcia roser.garcia@eficent.com https://github.com/rgarnau
|
||||
Miquel Raich miquel.raich@eficent.com https://github.com/mreficent
|
||||
Jordi Ballester jordi.ballester@eficent.com https://github.com/jbeficent
|
||||
Hector Villarreal hector.villarreal@eficent.com https://github.com/hveficent
|
||||
|
||||
@@ -686,7 +686,7 @@ The previous code will create the snippet’s content, but we still need to plac
|
||||
.. code-block:: xml
|
||||
|
||||
<template id="place_into_bar" inherit_id="website.snippets" name="Place into bar">
|
||||
<xpath expr="//div[@id='snippet_content']/div[@class='o_panel_body']" position="inside">
|
||||
<xpath expr="//div[@id='snippet_structure']/div[@class='o_panel_body']" position="inside">
|
||||
<t t-snippet="theme_tutorial.snippet_testimonial"
|
||||
t-thumbnail="/theme_tutorial/static/src/img/ui/snippet_thumb.jpg"/>
|
||||
</xpath>
|
||||
|
||||
@@ -221,6 +221,45 @@ database
|
||||
'verify-ca' or 'verify-full'
|
||||
Default value is 'prefer'
|
||||
|
||||
.. _reference/cmdline/server/internationalisation:
|
||||
|
||||
Internationalisation
|
||||
--------------------
|
||||
|
||||
Use these options to translate Odoo to another language. See i18n section of
|
||||
the user manual. Option '-d' is mandatory. Option '-l' is mandatory in case
|
||||
of importation
|
||||
|
||||
.. option:: --load-language <languages>
|
||||
|
||||
specifies the languages (separated by commas) for the translations you
|
||||
want to be loaded
|
||||
|
||||
.. option:: -l, --language <language>
|
||||
|
||||
specify the language of the translation file. Use it with --i18n-export
|
||||
or --i18n-import
|
||||
|
||||
.. option:: --i18n-export <filename>
|
||||
|
||||
export all sentences to be translated to a CSV file, a PO file or a TGZ
|
||||
archive and exit.
|
||||
|
||||
.. option:: --i18n-import <filename>
|
||||
|
||||
import a CSV or a PO file with translations and exit. The '-l' option is
|
||||
required.
|
||||
|
||||
.. option:: --i18n-overwrite
|
||||
|
||||
overwrites existing translation terms on updating a module or importing
|
||||
a CSV or a PO file.
|
||||
|
||||
.. option:: --modules
|
||||
|
||||
specify modules to export. Use in combination with --i18n-export
|
||||
|
||||
|
||||
built-in HTTP
|
||||
-------------
|
||||
|
||||
|
||||
@@ -1329,11 +1329,27 @@ order.
|
||||
|
||||
- Supported field types: *date*, *datetime*
|
||||
|
||||
Options:
|
||||
|
||||
- datepicker: extra settings for the datepicker_ widget.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="datefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
|
||||
|
||||
- datetime (FieldDateTime)
|
||||
This is the default field type for fields of type *datetime*.
|
||||
|
||||
- Supported field types: *date*, *datetime*
|
||||
|
||||
Options:
|
||||
|
||||
- datepicker: extra settings for the datepicker_ widget.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="datefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
|
||||
|
||||
- monetary (FieldMonetary)
|
||||
This is the default field type for fields of type 'monetary'. It is used to
|
||||
display a currency. If there is a currency fields given in option, it will
|
||||
@@ -1814,4 +1830,6 @@ Relational fields
|
||||
.. _event delegation:
|
||||
http://api.jquery.com/delegate/
|
||||
|
||||
.. _datepicker: https://github.com/Eonasdan/bootstrap-datetimepicker
|
||||
|
||||
.. _deferred: http://api.jquery.com/category/deferred-object/
|
||||
|
||||
@@ -28,7 +28,7 @@ SAFE_EVAL_BASE = {
|
||||
def make_compute(text, deps):
|
||||
""" Return a compute function from its code body and dependencies. """
|
||||
func = lambda self: safe_eval(text, SAFE_EVAL_BASE, {'self': self}, mode="exec")
|
||||
deps = [arg.strip() for arg in (deps or "").split(",")]
|
||||
deps = [arg.strip() for arg in deps.split(",")] if deps else []
|
||||
return api.depends(*deps)(func)
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,19 @@ class TestFields(common.TransactionCase):
|
||||
self.assertTrue(field.store)
|
||||
self.assertTrue(field.readonly)
|
||||
|
||||
def test_10_computed_custom(self):
|
||||
""" check definition of custom computed fields """
|
||||
self.env['ir.model.fields'].create({
|
||||
'name': 'x_bool_false_computed',
|
||||
'model_id': self.env.ref('test_new_api.model_test_new_api_message').id,
|
||||
'field_description': 'A boolean computed to false',
|
||||
'compute': "for r in self: r['x_bool_false_computed'] = False",
|
||||
'store': False,
|
||||
'ttype': 'boolean'
|
||||
})
|
||||
field = self.env['test_new_api.message']._fields['x_bool_false_computed']
|
||||
self.assertFalse(field.depends)
|
||||
|
||||
def test_10_non_stored(self):
|
||||
""" test non-stored fields """
|
||||
# a field declared with store=False should not have a column
|
||||
|
||||
+5
-3
@@ -1038,7 +1038,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
|
||||
self.db = db
|
||||
self.uid = uid
|
||||
self.login = login
|
||||
self.session_token = uid and security.compute_session_token(self)
|
||||
self.session_token = uid and security.compute_session_token(self, request.env)
|
||||
request.uid = uid
|
||||
request.disable_db = False
|
||||
|
||||
@@ -1053,9 +1053,11 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
|
||||
"""
|
||||
if not self.db or not self.uid:
|
||||
raise SessionExpiredException("Session expired")
|
||||
|
||||
# We create our own environment instead of the request's one.
|
||||
# to avoid creating it without the uid since request.uid isn't set yet
|
||||
env = odoo.api.Environment(request.cr, self.uid, self.context)
|
||||
# here we check if the session is still valid
|
||||
if not security.check_session(self):
|
||||
if not security.check_session(self, env):
|
||||
raise SessionExpiredException("Session expired")
|
||||
|
||||
def logout(self, keep_db=False):
|
||||
|
||||
@@ -153,9 +153,7 @@ class Registry(Mapping):
|
||||
""" Delete the registry linked to a given database. """
|
||||
with cls._lock:
|
||||
if db_name in cls.registries:
|
||||
registry = cls.registries.pop(db_name)
|
||||
registry.clear_caches()
|
||||
registry.registry_invalidated = True
|
||||
cls.registries.pop(db_name)
|
||||
|
||||
@classmethod
|
||||
def delete_all(cls):
|
||||
|
||||
@@ -394,6 +394,7 @@ def list_db_incompatible(databases):
|
||||
incompatible_databases.append(database_name)
|
||||
else:
|
||||
incompatible_databases.append(database_name)
|
||||
for database_name in incompatible_databases:
|
||||
# release connection
|
||||
odoo.sql_db.close_db(database_name)
|
||||
return incompatible_databases
|
||||
|
||||
@@ -12,15 +12,13 @@ def check(db, uid, passwd):
|
||||
res_users = odoo.registry(db)['res.users']
|
||||
return res_users.check(db, uid, passwd)
|
||||
|
||||
def compute_session_token(session):
|
||||
with odoo.registry(session.db).cursor() as cr:
|
||||
self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid)
|
||||
return self._compute_session_token(session.sid)
|
||||
def compute_session_token(session, env):
|
||||
self = env['res.users'].browse(session.uid)
|
||||
return self._compute_session_token(session.sid)
|
||||
|
||||
def check_session(session):
|
||||
with odoo.registry(session.db).cursor() as cr:
|
||||
self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid)
|
||||
if odoo.tools.misc.consteq(self._compute_session_token(session.sid), session.session_token):
|
||||
return True
|
||||
self._invalidate_session_cache()
|
||||
return False
|
||||
def check_session(session, env):
|
||||
self = env['res.users'].browse(session.uid)
|
||||
if odoo.tools.misc.consteq(self._compute_session_token(session.sid), session.session_token):
|
||||
return True
|
||||
self._invalidate_session_cache()
|
||||
return False
|
||||
|
||||
@@ -832,7 +832,6 @@ class WorkerCron(Worker):
|
||||
|
||||
from odoo.addons import base
|
||||
base.models.ir_cron.ir_cron._acquire_job(db_name)
|
||||
odoo.modules.registry.Registry.delete(db_name)
|
||||
|
||||
# dont keep cursors in multi database mode
|
||||
if len(db_names) > 1:
|
||||
|
||||
@@ -367,7 +367,7 @@ class HttpCase(TransactionCase):
|
||||
session.db = db
|
||||
session.uid = uid
|
||||
session.login = user
|
||||
session.session_token = uid and security.compute_session_token(session)
|
||||
session.session_token = uid and security.compute_session_token(session, env)
|
||||
session.context = env['res.users'].context_get() or {}
|
||||
session.context['uid'] = uid
|
||||
session._fix_lang(session.context)
|
||||
|
||||
+7
-5
@@ -1177,13 +1177,15 @@ def format_date(env, value, lang_code=False, date_format=False):
|
||||
'''
|
||||
if not value:
|
||||
return ''
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = value.date()
|
||||
elif isinstance(value, pycompat.string_types):
|
||||
if isinstance(value, pycompat.string_types):
|
||||
if len(value) < DATE_LENGTH:
|
||||
return ''
|
||||
value = value[:DATE_LENGTH]
|
||||
value = datetime.datetime.strptime(value, DEFAULT_SERVER_DATE_FORMAT).date()
|
||||
if len(value) > DATE_LENGTH:
|
||||
# a datetime, convert to correct timezone
|
||||
value = odoo.fields.Datetime.from_string(value)
|
||||
value = odoo.fields.Datetime.context_timestamp(env['res.lang'], value)
|
||||
else:
|
||||
value = odoo.fields.Datetime.from_string(value)
|
||||
|
||||
lang = env['res.lang']._lang_get(lang_code or env.context.get('lang') or 'en_US')
|
||||
locale = babel.Locale.parse(lang.code)
|
||||
|
||||
Reference in New Issue
Block a user