diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-06-15 10:15:27 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-06-15 10:15:27 +0200 |
| commit | b170a753e18c96f59e8c99a4e1750a4b85628338 (patch) | |
| tree | 13c365d5e3db51e3b94def4e88968170743ce7ae | |
| parent | a3c9c286cc46e03040e954f7872ade3555d98ad9 (diff) | |
| parent | b05e4d5f958892a9ff644b28a4ee165aeb2ea247 (diff) | |
[MERGE] forward port branch 11.0 up to b05e4d5f958
38 files changed, 292 insertions, 97 deletions
diff --git a/addons/account/views/report_journal.xml b/addons/account/views/report_journal.xml index 06dbab99ae4..c642b89347a 100644 --- a/addons/account/views/report_journal.xml +++ b/addons/account/views/report_journal.xml @@ -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"> diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py index c35430d1f92..07c42694ef1 100644 --- a/addons/base_address_city/models/res_partner.py +++ b/addons/base_address_city/models/res_partner.py @@ -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') diff --git a/addons/event/models/event.py b/addons/event/models/event.py index faa4466b7c7..1c32394da62 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -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')) diff --git a/addons/event/models/res_partner.py b/addons/event/models/res_partner.py index 727de940fa1..009b182c1ae 100644 --- a/addons/event/models/res_partner.py +++ b/addons/event/models/res_partner.py @@ -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): diff --git a/addons/event/views/res_partner_views.xml b/addons/event/views/res_partner_views.xml index 59d096efcd1..22aeba8686a 100644 --- a/addons/event/views/res_partner_views.xml +++ b/addons/event/views/res_partner_views.xml @@ -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> diff --git a/addons/l10n_nl/data/account_tax_template.xml b/addons/l10n_nl/data/account_tax_template.xml index c26dc9a8345..e9b937b98f5 100644 --- a/addons/l10n_nl/data/account_tax_template.xml +++ b/addons/l10n_nl/data/account_tax_template.xml @@ -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"> diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js index 074d3d192d3..c4bb3303dde 100644 --- a/addons/mail/static/src/js/services/chat_manager.js +++ b/addons/mail/static/src/js/services/chat_manager.js @@ -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); diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index a764585eede..ffe9239673a 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -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> diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py index 1d03a688630..409e09b8a9e 100644 --- a/addons/payment_ogone/controllers/main.py +++ b/addons/payment_ogone/controllers/main.py @@ -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' diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index 105e90e9fba..1e94b2e2a04 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -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 diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 1bdf4b3bf94..397952619d1 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -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) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 97a9082d776..53111d925fe 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -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: diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml index 836e0d0e843..98d850db36c 100644 --- a/addons/stock/report/report_stockpicking_operations.xml +++ b/addons/stock/report/report_stockpicking_operations.xml @@ -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> diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index 53948b477f9..8025cf4a13c 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -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. diff --git a/addons/web/static/lib/qweb/qweb-test-widgets.xml b/addons/web/static/lib/qweb/qweb-test-widgets.xml new file mode 100644 index 00000000000..503880bcc34 --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-widgets.xml @@ -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> diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index 9fe38adbde6..9e010986061 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -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. diff --git a/addons/web/static/src/js/chrome/loading.js b/addons/web/static/src/js/chrome/loading.js index 0c1b5e5025f..6c7f102231b 100644 --- a/addons/web/static/src/js/chrome/loading.js +++ b/addons/web/static/src/js/chrome/loading.js @@ -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); diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js index d6c67eacaec..f1f73bb5a28 100644 --- a/addons/web/static/src/js/core/ajax.js +++ b/addons/web/static/src/js/core/ajax.js @@ -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; diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js index 4760867e99f..cc372877e6d 100644 --- a/addons/web/static/src/js/core/session.js +++ b/addons/web/static/src/js/core/session.js @@ -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; diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index a62043b28c8..fed57c8b178 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -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} + ) + ); }, /** diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Editor.js b/addons/web_editor/static/lib/summernote/src/js/module/Editor.js index b7ba30bfcd1..99947c346cf 100644 --- a/addons/web_editor/static/lib/summernote/src/js/module/Editor.js +++ b/addons/web_editor/static/lib/summernote/src/js/module/Editor.js @@ -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. diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js index 3de70e5fcda..614ab4bfcf0 100644 --- a/addons/web_editor/static/src/js/editor/rte.summernote.js +++ b/addons/web_editor/static/src/js/editor/rte.summernote.js @@ -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(/>/); diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index 51ff585d200..7aa300b5bbb 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -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) { diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 0e5443ad79e..aa703071d9b 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -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 diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index 38bd5b0ce7f..f3a8041d54e 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -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, }) diff --git a/doc/cla/corporate/eficent.md b/doc/cla/corporate/eficent.md index 284e2b7b691..70184f115c8 100644 --- a/doc/cla/corporate/eficent.md +++ b/doc/cla/corporate/eficent.md @@ -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 diff --git a/doc/howtos/themes.rst b/doc/howtos/themes.rst index 5252ed5b776..f9158631282 100644 --- a/doc/howtos/themes.rst +++ b/doc/howtos/themes.rst @@ -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> diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst index 4171f12c5e1..12cd5f7a9d8 100644 --- a/doc/reference/cmdline.rst +++ b/doc/reference/cmdline.rst @@ -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 ------------- diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index b77aa9f5612..3c782ed00a8 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -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/ diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index da42722d390..4f7eb1e2c25 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -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) diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index e555567c90e..3c358a5bc52 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -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 diff --git a/odoo/http.py b/odoo/http.py index f984bcb44df..09999066941 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -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): diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 3115470adeb..38f45249ebc 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -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): diff --git a/odoo/service/db.py b/odoo/service/db.py index ce868f693dd..8f3c24d7d8a 100644 --- a/odoo/service/db.py +++ b/odoo/service/db.py @@ -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 diff --git a/odoo/service/security.py b/odoo/service/security.py index 229e4b84077..54d108fb4b1 100644 --- a/odoo/service/security.py +++ b/odoo/service/security.py @@ -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 diff --git a/odoo/service/server.py b/odoo/service/server.py index c42c8d1343a..8899760ce91 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -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: diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 53efa5d458d..2dcfd121cd4 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -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) diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 09ad85d278b..725971e0143 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -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) |
