From 4c79aa8d9d57a4e7e5cde083d3f8fb755e0a8693 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 13 Mar 2017 15:56:45 +0100 Subject: [PATCH 01/22] [FIX] sale: translate default name at creation When a sale order is created from the interface, the name field is read-only so not given in the create call. However, if the create values are filled with the default value, the string 'New' is translated and the condition is not met in the create method. This commit makes the create consistent with the default `lambda self: _('New')` Fixes #15839 --- addons/sale/sale.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 83dcb3111df..22418745668 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -224,8 +224,8 @@ class SaleOrder(models.Model): @api.model def create(self, vals): - if vals.get('name', 'New') == 'New': - vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or 'New' + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or _('New') # Makes sure partner_invoice_id', 'partner_shipping_id' and 'pricelist_id' are defined if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']): From 93bb6f322d0bccbca5106ae5cf67ee26e02d0da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20L=C3=B3pez?= Date: Thu, 9 Mar 2017 10:12:56 -0600 Subject: [PATCH 02/22] [FIX] base: avoid variable referenced before assignment error If the company currency is not active (that should not happen but nothing prevents it, probably happening with some CoA installation), the variable company_currency_format is undefined and an error is raised. Closes #15799 --- odoo/addons/base/res/res_currency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo/addons/base/res/res_currency.py b/odoo/addons/base/res/res_currency.py index 5c91e55b3f0..aef7bb97866 100644 --- a/odoo/addons/base/res/res_currency.py +++ b/odoo/addons/base/res/res_currency.py @@ -177,7 +177,7 @@ class Currency(models.Model): function += "if (arguments[1] === %s) { %s }" % (currency.id, return_str) if (currency == company_currency): company_currency_format = return_str - function = "if (arguments[1] === false || arguments[1] === undefined) {" + company_currency_format + " }" + function + function = "if (arguments[1] === false || arguments[1] === undefined) {" + company_currency_format + " }" + function return function def _select_companies_rates(self): From 5b4f7c13f32f36d86805eb7865d8a077746181d8 Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Tue, 14 Mar 2017 11:41:48 +0100 Subject: [PATCH 03/22] [FIX] web_kanban: use string when group by selection In the kanban view, if we try to group by a selection field that is not defined in the arch, the key will be used as column name (instead of the string). A fix has already been pushed to load all the fields in this case (see 0e6eb96) but these fields were not correctly used in this particular case. --- addons/web_kanban/static/src/js/kanban_view.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js index ff669072413..a0ed0761dcf 100644 --- a/addons/web_kanban/static/src/js/kanban_view.js +++ b/addons/web_kanban/static/src/js/kanban_view.js @@ -65,7 +65,7 @@ var KanbanView = View.extend({ this.qweb.default_dict = _.clone(QWeb.default_dict); this.limit = this.options.limit || parseInt(this.fields_view.arch.attrs.limit, 10) || 40; - this.fields = {}; + this.fields = this.fields_view.fields; this.fields_keys = _.keys(this.fields_view.fields); this.grouped = undefined; this.group_by_field = undefined; @@ -106,7 +106,7 @@ var KanbanView = View.extend({ this.qweb.add_template(utils.json_node_to_xml(child)); break; } else if (child.tag === 'field') { - var ftype = child.attrs.widget || this.fields_view.fields[child.attrs.name].type; + var ftype = child.attrs.widget || this.fields[child.attrs.name].type; if(ftype === "many2many" && "context" in child.attrs) { this.m2m_context[child.attrs.name] = child.attrs.context; } @@ -123,7 +123,7 @@ var KanbanView = View.extend({ do_search: function(domain, context, group_by) { var self = this; var group_by_field = group_by[0] || this.default_group_by; - var field = this.fields_view.fields[group_by_field]; + var field = this.fields[group_by_field]; var options = {}; var fields_def; if (field === undefined) { @@ -247,7 +247,7 @@ var KanbanView = View.extend({ _.each(groups, function (group) { var value = group.attributes.value; group.id = value instanceof Array ? value[0] : value; - var field = self.fields_view.fields[options.group_by_field]; + var field = self.fields[options.group_by_field]; if (field && field.type === "selection") { value= _.find(field.selection, function (s) { return s[0] === group.id; }); } @@ -303,7 +303,7 @@ var KanbanView = View.extend({ return this._super(action); }, has_active_field: function() { - return this.fields_view.fields.active; + return this.fields.active; }, _is_quick_create_enabled: function() { if (!this.quick_creatable || !this.is_action_enabled('create')) @@ -376,7 +376,7 @@ var KanbanView = View.extend({ this.record_options = { editable: this.is_action_enabled('edit'), deletable: this.is_action_enabled('delete'), - fields: this.fields_view.fields, + fields: this.fields, qweb: this.qweb, model: this.model, read_only_mode: this.options.read_only_mode, @@ -427,7 +427,7 @@ var KanbanView = View.extend({ grouped_by_m2o: this.grouped_by_m2o, relation: this.relation, qweb: this.qweb, - fields: this.fields_view.fields, + fields: this.fields, quick_create: this._is_quick_create_enabled(), }; }, @@ -436,7 +436,7 @@ var KanbanView = View.extend({ var self = this; // Drag'n'drop activation/deactivation - var group_by_field_attrs = this.fields_view.fields[this.group_by_field] || this.fields[this.group_by_field]; + var group_by_field_attrs = this.fields[this.group_by_field]; // Deactivate the drag'n'drop if: // - field is a date or datetime since we group by month From d32e275ad1e31dc0c52d37563e406d052d3d8a32 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Tue, 14 Mar 2017 13:16:43 +0100 Subject: [PATCH 04/22] [FIX] web: force red color for invalid form elements When an input is saved with an invalid value, it gets the `o_form_invalid` class. This makes the border red. In enterprise, there was a bug as required inputs have also a different border color and the rule was more important. Unfortunately there is no avoiding the !important rule here. Indeed, other solutions would require to make more rules in enterprise or to change the order of files (which would create many problems). --- addons/web/static/src/less/form_view.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less index ef0bcb44be4..da425d5a69f 100644 --- a/addons/web/static/src/less/form_view.less +++ b/addons/web/static/src/less/form_view.less @@ -174,7 +174,7 @@ .o_form_invalid { &.o_form_input, .o_form_input, &.o_form_textarea, .o_form_textarea { - border-color: @brand-danger; + border-color: @brand-danger!important; // enterprise o_form_required rule overrides this without !important } } From 8adc2ef440aec35a3829ebf26b3759dbc0b39903 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 13 Mar 2017 09:38:42 +0100 Subject: [PATCH 05/22] [FIX] stock: do not write on picking type When creating a stock.picking, the related fields picking_type_entire_packs and picking_type_code were both updated. This was an issue was required write access on stock.picking.type (warehouse manager) to create a stock.picking (warehouse user). The fields were updated as they were present in the view and filled with the related value. Even if the value was unchanged, it was triggering a write anyway. The two fields were in invisible="1" and used only in attrs for other fields. Making these two fields in readonly does not prevent to use it in the attrs but avoid to send a value to the server and trigger a useless write. Fixes #15769 --- addons/stock/models/stock_picking.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 83a83c3e10b..dbf11a61dd9 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -245,8 +245,10 @@ class Picking(models.Model): picking_type_code = fields.Selection([ ('incoming', 'Vendors'), ('outgoing', 'Customers'), - ('internal', 'Internal')], related='picking_type_id.code') - picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs') + ('internal', 'Internal')], related='picking_type_id.code', + readonly=True) + picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs', + readonly=True) quant_reserved_exist = fields.Boolean( 'Has quants already reserved', compute='_compute_quant_reserved_exist', From a873890f2842cb238152717d83739465898acd68 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 14 Mar 2017 16:19:04 +0100 Subject: [PATCH 06/22] [FIX] mass_mailing: link in kanban - Go to Mass Mailing > Configuration > Mail Templates - In the Kanban view, find a template which contains a link (http, mailto, whatever) - Click on the link You are redirected to the link instead of being redirected to the record. opw-706047 --- addons/mass_mailing/static/src/css/email_template.css | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/mass_mailing/static/src/css/email_template.css b/addons/mass_mailing/static/src/css/email_template.css index f0b25ba612d..edd9c0c3f0d 100644 --- a/addons/mass_mailing/static/src/css/email_template.css +++ b/addons/mass_mailing/static/src/css/email_template.css @@ -9,6 +9,7 @@ } .kanban_html_preview { + pointer-events: none; width: 600px; -webkit-transform: scale(.50); -ms-transform: scale(.50); From 945679176a6e53cb7b20b1ba041323b9d1cd6a7e Mon Sep 17 00:00:00 2001 From: Pierre Masereel Date: Tue, 14 Mar 2017 16:46:42 +0100 Subject: [PATCH 07/22] [FIX] stock: warehouse check_reception_resupply When we are trying to change the reception route of a warehouse when there are existing resupply routes, we are getting a traceback. This bug comes from a migration error introduced in rev: https://github.com/odoo-dev/odoo/commit/70e1d5d5bc1a3cb325dbcb091fc2135e23084145 --- addons/stock/models/stock_warehouse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index a580f071848..40dfa3ace5c 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -573,7 +573,7 @@ class Warehouse(models.Model): routes = self.env['stock.location.route'].search([('supplied_wh_id', 'in', self.ids)]) self.env['procurement.rule'].search([ '&', ('route_id', 'in', routes.ids), - ('location_src_id.usage', '=', 'transit')]).write({'location_id': new_location}) + ('location_src_id.usage', '=', 'transit')]).write({'location_id': new_location.id}) @api.multi def _update_routes(self): From 9f9657c04b9ec1e7151f562f74b61efc92df58fa Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Tue, 14 Mar 2017 16:56:17 +0100 Subject: [PATCH 08/22] [FIX] base: unlinking ir.model.constraint When unlinking an ir.model, all the ir.model.constraint linked to this ir.model must be removed to avoid to call unexisting model. In stable version, the right fix would be to put an attribute ondelete=cascade on the field model of ir.model.constraint. In 10.0, the ir.model.constraint are created with module = the module where the related field has been declared. But when unlinking the ir.model in function _module_data_uninstall, the related ir.model.constraint to remove were chosen according to the removed modules. So with models declaring constraints on inherited fields, some problems occured because the model could be removed and not all the related constraints. opw:716032 --- odoo/addons/base/ir/ir_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 8627046790d..5e610641989 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -133,6 +133,9 @@ class IrModel(models.Model): # prevent screwing up fields that depend on these models' fields model.field_id._prepare_update() + imc = self.env['ir.model.constraint'].search([('model', 'in', self.ids)]) + imc.unlink() + self._drop_table() res = super(IrModel, self).unlink() From 73de39150a320786bd3e449363670b0a57e86c9d Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 15 Mar 2017 10:50:12 +0100 Subject: [PATCH 09/22] [FIX] crm: Translation in crm.opportunity.report The opportunity analyses displayed the stages always in English. opw:708380 --- addons/crm/report/crm_opportunity_report_views.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml index 27b96538cd5..7b5e3720a93 100644 --- a/addons/crm/report/crm_opportunity_report_views.xml +++ b/addons/crm/report/crm_opportunity_report_views.xml @@ -8,7 +8,7 @@ - + @@ -31,7 +31,7 @@ crm.opportunity.report - + From 27f656a23e92df52b2f78fdd7c31b8c045b1f39b Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Mon, 13 Mar 2017 10:59:59 +0100 Subject: [PATCH 10/22] [FIX] mrp: duplicated raw materials - Create Product A with a BOM which depends on a product with tracking "By Unique Serial Number" (Product B). - Create a MO for Product A - In the "Register lots" wizard, add two lines for "Product B", with the same serial number. Nothing prevents the user to do it, even when the inventory is posted. opw-709888 --- addons/mrp/i18n/mrp.pot | 6 ++++++ addons/mrp/models/stock_move.py | 10 ++++++++++ addons/mrp/wizard/mrp_product_produce.py | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/addons/mrp/i18n/mrp.pot b/addons/mrp/i18n/mrp.pot index 5123568e86d..c85998a4bc1 100644 --- a/addons/mrp/i18n/mrp.pot +++ b/addons/mrp/i18n/mrp.pot @@ -3249,6 +3249,12 @@ msgstr "" msgid "You cannot cancel a move move having already consumed material" msgstr "" +#. module: mrp +#: code:addons/mrp/models/stock_move.py:40 +#, python-format +msgid "You cannot use the same serial number in two different lines." +msgstr "" + #. module: mrp #: code:addons/mrp/wizard/change_production_qty.py:44 #, python-format diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 3c6a99fa6eb..608d11e9bce 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -28,6 +28,16 @@ class StockMoveLots(models.Model): done_move = fields.Boolean('Move Done', related='move_id.is_done', store=True) # TDE FIXME: naming plus_visible = fields.Boolean("Plus Visible", compute='_compute_plus') + @api.one + @api.constrains('lot_id') + def _check_lot_id(self): + if self.move_id.product_id.tracking == 'serial': + lots = set([]) + for move_lot in self.move_id.move_lot_ids.filtered(lambda r: not r.lot_produced_id): + if move_lot.lot_id in lots: + raise exceptions.UserError(_('You cannot use the same serial number in two different lines.')) + lots.add(move_lot.lot_id) + def _compute_plus(self): for movelot in self: if movelot.move_id.product_id.tracking == 'serial': diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index f6a70c4a1f6..b7cd863a5fc 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -125,8 +125,8 @@ class MrpProductProduce(models.TransientModel): #Possibly the entire move is selected remaining_qty = movelots.quantity - movelots.quantity_done if remaining_qty > 0: - new_move_lot = movelots.copy() - new_move_lot.write({'quantity':movelots.quantity_done, 'lot_produced_id': self.lot_id.id}) + default = {'quantity': movelots.quantity_done, 'lot_produced_id': self.lot_id.id} + new_move_lot = movelots.copy(default=default) movelots.write({'quantity': remaining_qty, 'quantity_done': 0}) else: movelots.write({'lot_produced_id': self.lot_id.id}) From cc1c15327cddf2b512e01e0b7b31e2aa6a1d34fe Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Wed, 15 Mar 2017 17:11:16 +0100 Subject: [PATCH 11/22] [FIX] expression: improve o2m with m2o integer We sometimes use in Odoo a One2many field with an inverse Integer field instead of a usual Many2one. This allow for example in several instances to have a "Many2one" which can be reference from several models, eg: Model Ranking: name = String field res_id = Integer field res_model = String field Model Toy: rank = One2many [inverse: Ranking -> res_id] [domain: res_model == Toy] Model Tool: rank = One2many [inverse: Ranking -> res_id] [domain: res_model == Tool] This enable us to have a shared feature between otherwise unrelated models. But there was several issue when searching on these One2many: 1) if the Integer Many2one was not stored (eg. it came from an inherits) on the searched model, this could lead to an error. 2) when we searched: - by IDs (rank in ['55']) with at least one id not respecting the domain - by IDs with a negative operator - with a negative operator on unfound string (rank != "no rank has this") - with a false value (rank = False) we would not apply the One2many domain (eg. res_model == Toy) and thus possibly getting Toy 3 errenously because a Tool 3 was found without the domain being applied. This fix modify the search on One2many and for: 1. if the inverse is an Integer not stored field instead of Many2one manage it. 2. if the field is an Integer field instead of Many2one and there is a domain on the One2many: apply the domain on the inverse model found. So only some search on One2many with a domain whose inverse field is an Integer could be impacted. This would also be nice to have for all One2many with a domain but the probability of it being useful versus risk for performance is not judged high enough. opw-710508 --- odoo/addons/test_new_api/ir.model.access.csv | 3 + odoo/addons/test_new_api/models.py | 27 ++++++++ .../test_new_api/tests/test_one2many.py | 67 +++++++++++++++++++ odoo/osv/expression.py | 32 ++++++--- 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/odoo/addons/test_new_api/ir.model.access.csv b/odoo/addons/test_new_api/ir.model.access.csv index 3862f4ce84f..0dd4efcda72 100644 --- a/odoo/addons/test_new_api/ir.model.access.csv +++ b/odoo/addons/test_new_api/ir.model.access.csv @@ -5,6 +5,9 @@ access_message,test_new_api_message,test_new_api.model_test_new_api_message,,1,1 access_emailmessage,test_new_api_emailmessage,test_new_api.model_test_new_api_emailmessage,,1,1,1,1 access_multi,test_new_api_multi,test_new_api.model_test_new_api_multi,,1,1,1,1 access_multi_line,test_new_api_multi_line,test_new_api.model_test_new_api_multi_line,,1,1,1,1 +access_creativework_edition,test_new_api_creativework_edition,model_test_new_api_creativework_edition,,1,1,1,1 +access_creativework_book,test_new_api_creativework_book,model_test_new_api_creativework_book,,1,1,1,1 +access_creativework_movie,test_new_api_creativework_movie,model_test_new_api_creativework_movie,,1,1,1,1 access_mixed,test_new_api_mixed,test_new_api.model_test_new_api_mixed,,1,1,1,1 access_domain_bool,access_domain_bool,model_domain_bool,,1,1,1,1 access_test_new_api_foo,access_test_new_api_foo,model_test_new_api_foo,,1,1,1,1 diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py index 0d2054e32e5..81e24fb5b5b 100644 --- a/odoo/addons/test_new_api/models.py +++ b/odoo/addons/test_new_api/models.py @@ -199,6 +199,33 @@ class MultiLine(models.Model): partner = fields.Many2one('res.partner') +class Edition(models.Model): + _name = 'test_new_api.creativework.edition' + + name = fields.Char() + res_id = fields.Integer(required=True) + res_model_id = fields.Many2one('ir.model', required=True) + res_model = fields.Char(related='res_model_id.model', store=True) + + +class Book(models.Model): + _name = 'test_new_api.creativework.book' + + name = fields.Char() + editions = fields.One2many( + 'test_new_api.creativework.edition', 'res_id', domain=[('res_model', '=', _name)] + ) + + +class Movie(models.Model): + _name = 'test_new_api.creativework.movie' + + name = fields.Char() + editions = fields.One2many( + 'test_new_api.creativework.edition', 'res_id', domain=[('res_model', '=', _name)] + ) + + class MixedModel(models.Model): _name = 'test_new_api.mixed' diff --git a/odoo/addons/test_new_api/tests/test_one2many.py b/odoo/addons/test_new_api/tests/test_one2many.py index 38dee13ab54..9ade2c0fe19 100644 --- a/odoo/addons/test_new_api/tests/test_one2many.py +++ b/odoo/addons/test_new_api/tests/test_one2many.py @@ -10,6 +10,36 @@ class One2manyCase(TransactionCase): "name": "What is up?" }) + # data for One2many with inverse field Integer + self.Edition = self.env["test_new_api.creativework.edition"] + self.Book = self.env["test_new_api.creativework.book"] + self.Movie = self.env["test_new_api.creativework.movie"] + + book_model_id = self.env['ir.model'].search([('model', '=', self.Book._name)]).id + movie_model_id = self.env['ir.model'].search([('model', '=', self.Movie._name)]).id + + books_data = ( + ('Imaginary book', ()), + ('Another imaginary book', ()), + ('Nineteen Eighty Four', ('First edition', 'Fourth Edition')) + ) + + movies_data = ( + ('The Gold Rush', ('1925 (silent)', '1942')), + ('Imaginary movie', ()), + ('Another imaginary movie', ()) + ) + + for name, editions in books_data: + book_id = self.Book.create({'name': name}).id + for edition in editions: + self.Edition.create({'res_model_id': book_model_id, 'name': edition, 'res_id': book_id}) + + for name, editions in movies_data: + movie_id = self.Movie.create({'name': name}).id + for edition in editions: + self.Edition.create({'res_model_id': movie_model_id, 'name': edition, 'res_id': movie_id}) + def operations(self): """Run operations on o2m fields to check all works fine.""" # Check the lines first @@ -63,3 +93,40 @@ class One2manyCase(TransactionCase): """Check lines created with RPC style and added in one step""" self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)] self.operations() + + def test_many2one_integer(self): + """Test several models one2many with same inverse Integer field""" + # utility function to convert records to tuples with id,name + t = lambda records: records.mapped(lambda r: (r.id, r.name)) + + books = self.Book.search([]) + books_with_edition = books.filtered(lambda r: r.editions) + movies = self.Movie.search([]) + movies_without_edition = movies.filtered(lambda r: not r.editions) + movies_with_edition = movies.filtered(lambda r: r.editions) + movie_editions = movies_with_edition.mapped('editions') + one_movie_edition = movie_editions[0] + + res_movies_without_edition = self.Movie.search([('editions', '=', False)]) + self.assertItemsEqual(t(res_movies_without_edition), t(movies_without_edition)) + + res_movies_with_edition = self.Movie.search([('editions', '!=', False)]) + self.assertItemsEqual(t(res_movies_with_edition), t(movies_with_edition)) + + res_books_with_movie_edition = self.Book.search([('editions', 'in', movie_editions.ids)]) + self.assertFalse(t(res_books_with_movie_edition)) + + res_books_without_movie_edition = self.Book.search([('editions', 'not in', movie_editions.ids)]) + self.assertItemsEqual(t(res_books_without_movie_edition), t(books_with_edition)) + + res_books_without_one_movie_edition = self.Book.search([('editions', 'not in', movie_editions[:1].ids)]) + self.assertItemsEqual(t(res_books_without_one_movie_edition), t(books_with_edition)) + + res_books_with_one_movie_edition_name = self.Book.search([('editions', '=', movie_editions[:1].name)]) + self.assertFalse(t(res_books_with_one_movie_edition_name)) + + res_books_without_one_movie_edition_name = self.Book.search([('editions', '!=', movie_editions[:1].name)]) + self.assertItemsEqual(t(res_books_without_one_movie_edition_name), t(books_with_edition)) + + res_movies_not_of_edition_name = self.Movie.search([('editions', '!=', one_movie_edition.name)]) + self.assertItemsEqual(t(res_movies_not_of_edition_name), t(movies.filtered(lambda r: one_movie_edition not in r.editions))) diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index da093dad323..eea14988eb7 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -920,19 +920,23 @@ class expression(object): elif field.type == 'one2many': call_null = True + domain = field.domain + if callable(domain): + domain = domain(model) + is_integer_m2o = comodel._fields[field.inverse_name].type == 'integer' if right is not False: if isinstance(right, basestring): op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator) - domain = field.domain - if callable(domain): - domain = domain(model) ids2 = [x[0] for x in comodel.name_search(right, domain or [], op, limit=None)] if ids2: operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - elif isinstance(right, collections.Iterable): - ids2 = right else: - ids2 = [right] + if isinstance(right, collections.Iterable): + ids2 = right + else: + ids2 = [right] + if ids2 and is_integer_m2o and domain: + ids2 = comodel.search([('id', 'in', ids2)] + domain).ids if not ids2: if operator in ['like', 'ilike', 'in', '=']: @@ -945,7 +949,9 @@ class expression(object): ids1 = select_from_where(cr, field.inverse_name, comodel._table, 'id', ids2, operator) else: recs = comodel.browse(ids2).sudo().with_context(prefetch_fields=False) - ids1 = recs.mapped(field.inverse_name).ids + ids1 = recs.mapped(field.inverse_name) + if not is_integer_m2o: + ids1 = ids1.ids if ids1: call_null = False o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' @@ -958,12 +964,16 @@ class expression(object): if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' # determine ids from field.inverse_name - if comodel._fields[field.inverse_name].store: + if comodel._fields[field.inverse_name].store and not (is_integer_m2o and domain): ids1 = select_distinct_from_where_not_null(cr, field.inverse_name, comodel._table) else: - domain = [(field.inverse_name, '!=', False)] - recs = comodel.search(domain).sudo().with_context(prefetch_fields=False) - ids1 = recs.mapped(field.inverse_name).ids + comodel_domain = [(field.inverse_name, '!=', False)] + if is_integer_m2o and domain: + comodel_domain += domain + recs = comodel.search(comodel_domain).sudo().with_context(prefetch_fields=False) + ids1 = recs.mapped(field.inverse_name) + if not is_integer_m2o: + ids1 = ids1.ids push(create_substitution_leaf(leaf, ('id', o2m_op, ids1), model)) elif field.type == 'many2many': From e542b450a34d5c3723c92cb95d7132a288c46e60 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 16 Mar 2017 13:15:46 +0100 Subject: [PATCH 12/22] [FIX] sale_stock: duplicate qty - Create a stockable product, create a SO selling 1 unit. - Confirm the SO, validate the Picking 1 => delivered qty is 1 - Return Picking 1 and validate Picking 2 => delivered qty is 1 - Return Picking 2 and validate Picking 3 => delivered qty is 2 The delivered quantity should remain 1. opw-715685 --- addons/sale_stock/sale_stock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 63f03f4a64f..2f817e09978 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -197,7 +197,8 @@ class SaleOrderLine(models.Model): #Note that we don't decrease quantity for customer returns on purpose: these are exeptions that must be treated manually. Indeed, #modifying automatically the delivered quantity may trigger an automatic reinvoicing (refund) of the SO, which is definitively not wanted if move.location_dest_id.usage == "customer": - qty += self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom) + if not move.origin_returned_move_id: + qty += self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom) return qty @api.multi From 0590af8b3f3f5c59ad93f405cfef392b5d525f1e Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 16 Mar 2017 11:41:49 +0100 Subject: [PATCH 13/22] [FIX] mrp_repair: tax included - Create a product with price tax included - Create Repair order - Add the product in tabs "Operation" or "Invoicing" The tax is added twice in the total. opw-709608 --- addons/mrp_repair/models/mrp_repair.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/mrp_repair/models/mrp_repair.py b/addons/mrp_repair/models/mrp_repair.py index 4273baf9271..5d2bcde290c 100644 --- a/addons/mrp_repair/models/mrp_repair.py +++ b/addons/mrp_repair/models/mrp_repair.py @@ -453,7 +453,7 @@ class RepairLine(models.Model): if not self.to_invoice: self.price_subtotal = 0.0 else: - taxes = self.env['account.tax'].compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id) + taxes = self.tax_id.compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id) self.price_subtotal = taxes['total_excluded'] @api.onchange('type', 'repair_id') @@ -536,7 +536,7 @@ class RepairFee(models.Model): if not self.to_invoice: self.price_subtotal = 0.0 else: - taxes = self.env['account.tax'].compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id) + taxes = self.tax_id.compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id) self.price_subtotal = taxes['total_excluded'] @api.onchange('repair_id', 'product_id', 'product_uom_qty') From be9dce625c55e1b2d6039573c7035d61f762edc8 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Mon, 13 Mar 2017 14:06:32 +0100 Subject: [PATCH 14/22] [FIX] stock: inventory adjustment and negative quants It is still possible to have negative and positive quants in the same location because of returns: if you send something to the customer that is not there and you return it, you will still be able to reserve the returned goods to send to another client. Before, if you would do an inventory adjustment, it would not take into account these returned quants and their negative counterpart, which made them difficult to get out of the system. This fix takes them into account by creating two movements for one inventory line: move the positive counterpart to the inventory location before getting back from this location the same quantity. This way, even if you have 0 as quantity on hand but you have those 2 quants, it will eliminate them. (if you are increasing the stock, part of the process might have done it automatically already). Test cases contributed by @jir-odoo --- addons/stock/models/stock_inventory.py | 58 +++++++---- addons/stock/tests/test_warehouse.py | 128 +++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 16 deletions(-) diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 5cc88ee7237..a95d394ce55 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -385,31 +385,57 @@ class InventoryLine(models.Model): ('owner_id', '=', self.partner_id.id), ('package_id', '=', self.package_id.id)]) + def _get_move_values(self, qty, location_id, location_dest_id): + self.ensure_one() + return { + 'name': _('INV:') + (self.inventory_id.name or ''), + 'product_id': self.product_id.id, + 'product_uom': self.product_uom_id.id, + 'product_uom_qty': qty, + 'date': self.inventory_id.date, + 'company_id': self.inventory_id.company_id.id, + 'inventory_id': self.inventory_id.id, + 'state': 'confirmed', + 'restrict_lot_id': self.prod_lot_id.id, + 'restrict_partner_id': self.partner_id.id, + 'location_id': location_id, + 'location_dest_id': location_dest_id, + } + + def _fixup_negative_quants(self): + """ This will handle the irreconciable quants created by a force availability followed by a + return. When generating the moves of an inventory line, we look for quants of this line's + product created to compensate a force availability. If there are some and if the quant + which it is propagated from is still in the same location, we move it to the inventory + adjustment location before getting it back. Getting the quantity from the inventory + location will allow the negative quant to be compensated. + """ + self.ensure_one() + for quant in self._get_quants().filtered(lambda q: q.propagated_from_id.location_id.id == self.location_id.id): + # send the quantity to the inventory adjustment location + move_out_vals = self._get_move_values(quant.qty, self.location_id.id, self.product_id.property_stock_inventory.id) + move_out = self.env['stock.move'].create(move_out_vals) + self.env['stock.quant'].quants_reserve([(quant, quant.qty)], move_out) + move_out.action_done() + + # get back the quantity from the inventory adjustment location + move_in_vals = self._get_move_values(quant.qty, self.product_id.property_stock_inventory.id, self.location_id.id) + move_in = self.env['stock.move'].create(move_in_vals) + move_in.action_done() + def _generate_moves(self): moves = self.env['stock.move'] Quant = self.env['stock.quant'] for line in self: + line._fixup_negative_quants() + if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0: continue diff = line.theoretical_qty - line.product_qty - vals = { - 'name': _('INV:') + (line.inventory_id.name or ''), - 'product_id': line.product_id.id, - 'product_uom': line.product_uom_id.id, - 'date': line.inventory_id.date, - 'company_id': line.inventory_id.company_id.id, - 'inventory_id': line.inventory_id.id, - 'state': 'confirmed', - 'restrict_lot_id': line.prod_lot_id.id, - 'restrict_partner_id': line.partner_id.id} if diff < 0: # found more than expected - vals['location_id'] = line.product_id.property_stock_inventory.id - vals['location_dest_id'] = line.location_id.id - vals['product_uom_qty'] = abs(diff) + vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id) else: - vals['location_id'] = line.location_id.id - vals['location_dest_id'] = line.product_id.property_stock_inventory.id - vals['product_uom_qty'] = diff + vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id) move = moves.create(vals) if diff > 0: diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index 6e71ec74276..ef18411a79e 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -103,6 +103,134 @@ class TestInventory(TestStockCommon): self.assertEqual(product.qty_available, 8.0) self.assertEqual(product.with_context(location=self.env.ref('stock.stock_location_customers').id).qty_available, 7.0) + def test_inventory_adjustment_and_negative_quants_1(self): + """Make sure negative quants from returns get wiped out with an inventory adjustment""" + productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'}) + stock_location = self.env.ref('stock.stock_location_stock') + customer_location = self.env.ref('stock.stock_location_customers') + location_loss = self.env.ref('stock.location_inventory') + + # Create a picking out and force availability + picking_out = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + self.env['stock.move'].create({ + 'name': productA.name, + 'product_id': productA.id, + 'product_uom_qty': 1, + 'product_uom': productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + picking_out.action_confirm() + picking_out.force_assign() + picking_out.do_transfer() + + # Create return picking for all goods + default_data = self.env['stock.return.picking']\ + .with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\ + .default_get([ + 'move_dest_exists', + 'original_location_id', + 'product_return_moves', + 'parent_location_id', + 'location_id', + ]) + return_wiz = self.env['stock.return.picking']\ + .with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\ + .create(default_data) + res = return_wiz.create_returns() + return_pick = self.env['stock.picking'].browse(res['res_id']) + return_pick.action_assign() + return_pick.do_transfer() + + # Make an inventory adjustment to set the quantity to 0 + inventory = self.env['stock.inventory'].create({ + 'name': 'Starting for product_1', + 'filter': 'product', + 'location_id': stock_location.id, + 'product_id': productA.id, + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.") + self.assertEqual(inventory.line_ids.theoretical_qty, 0, "Theoretical quantity should be zero.") + inventory.action_done() + + # The inventory adjustment should have created two moves + self.assertEqual(len(inventory.move_ids), 2) + quantity = inventory.move_ids.mapped('product_qty') + self.assertEqual(quantity, [1, 1], "Moves created with wrong quantity.") + location_ids = inventory.move_ids.mapped('location_id').ids + self.assertEqual(set(location_ids), {stock_location.id, location_loss.id}) + + # There should be no quant in the stock location + quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)]) + self.assertEqual(len(quants), 0) + + # There should be one quant in the inventory loss location + quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)]) + self.assertEqual(len(quant), 1) + self.assertEqual(quant.qty, 1) + + def test_inventory_adjustment_and_negative_quants_2(self): + """Make sure negative quants get wiped out with an inventory adjustment""" + productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'}) + stock_location = self.env.ref('stock.stock_location_stock') + customer_location = self.env.ref('stock.stock_location_customers') + location_loss = self.env.ref('stock.location_inventory') + + # Create a picking out and force availability + picking_out = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + self.env['stock.move'].create({ + 'name': productA.name, + 'product_id': productA.id, + 'product_uom_qty': 1, + 'product_uom': productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + picking_out.action_confirm() + picking_out.force_assign() + picking_out.do_transfer() + + # Make an inventory adjustment to set the quantity to 0 + inventory = self.env['stock.inventory'].create({ + 'name': 'Starting for product_1', + 'filter': 'product', + 'location_id': stock_location.id, + 'product_id': productA.id, + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.") + self.assertEqual(inventory.line_ids.theoretical_qty, -1, "Theoretical quantity should be -1.") + inventory.line_ids.product_qty = 0 # Put the quantity back to 0 + inventory.action_done() + + # The inventory adjustment should have created one + self.assertEqual(len(inventory.move_ids), 1) + quantity = inventory.move_ids.mapped('product_qty') + self.assertEqual(quantity, [1], "Moves created with wrong quantity.") + location_ids = inventory.move_ids.mapped('location_id').ids + self.assertEqual(set(location_ids), {location_loss.id}) + + # There should be no quant in the stock location + quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)]) + self.assertEqual(len(quants), 0) + + # There should be no quant in the inventory loss location + quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)]) + self.assertEqual(len(quant), 0) + class TestResupply(TestStockCommon): def setUp(self): From f50ba68be72c8b63af87e8dcd0e726e5e3b77092 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 16 Mar 2017 17:42:39 +0100 Subject: [PATCH 15/22] [FIX] web_editor: handle margin:initial correctly The transcoder (which transforms stylesheet rules to inline style) did some compression of the padding and margin css properties to not always have margin-top + margin-right + margin-bottom + margin-left. This could cause an issue when the value of margin or padding contained an initial or inherit value because browsers (at least firefox and google chrome) doesn't allow it. Thus the preview could for example have bigger veritcal spaces arround `

` tag than what was seen when editing. With this fix instead of invalid: margin:initial initial initial initial we have: margin:initial And instead of invalid: margin:initial 5px 6px 7px we have: margin-top: initial; margin-right: 5px; margin-bottom: 6px; margin-left: 7px; opw-706535 --- addons/web_editor/static/src/js/transcoder.js | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/addons/web_editor/static/src/js/transcoder.js b/addons/web_editor/static/src/js/transcoder.js index d327252b63d..306369cddfa 100644 --- a/addons/web_editor/static/src/js/transcoder.js +++ b/addons/web_editor/static/src/js/transcoder.js @@ -99,20 +99,28 @@ var getMatchedCSSRules = function (a) { if (style.display === 'block') { delete style.display; } - if (style['margin-top']) { - style.margin = (style['margin-top'] || 0) + ' ' + (style['margin-right'] || 0) + ' ' + (style['margin-bottom'] || 0) + ' ' + (style['margin-left'] || 0); - delete style['margin-top']; - delete style['margin-right']; - delete style['margin-bottom']; - delete style['margin-left']; - } - if (style['padding-top']) { - style.padding = (style['padding-top'] || 0) + ' ' + (style['padding-right'] || 0) + ' ' + (style['padding-bottom'] || 0) + ' ' + (style['padding-left'] || 0); - delete style['padding-top']; - delete style['padding-right']; - delete style['padding-bottom']; - delete style['padding-left']; - } + + _.each(['margin', 'padding'], function(p) { + if (style[p+'-top'] || style[p+'-right'] || style[p+'-bottom'] || style[p+'-left']) { + if (style[p+'-top'] === style[p+'-right'] && style[p+'-top'] === style[p+'-bottom'] && style[p+'-top'] === style[p+'-left']) { + // keep => property: [top/right/bottom/left value]; + style[p] = style[p+'-top']; + } + else { + // keep => property: [top value] [right value] [bottom value] [left value]; + style[p] = (style[p+'-top'] || 0) + ' ' + (style[p+'-right'] || 0) + ' ' + (style[p+'-bottom'] || 0) + ' ' + (style[p+'-left'] || 0); + if (style[p].indexOf('inherit') !== -1 || style[p].indexOf('initial') !== -1) { + // keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value]; + delete style[p]; + return; + } + } + delete style[p+'-top']; + delete style[p+'-right']; + delete style[p+'-bottom']; + delete style[p+'-left']; + } + }); return a.className ? cache[a.tagName + "." +a.className] = style : style; }; From fb05d8845d92b1ca9013307664fa6d52be64aa3d Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 16 Mar 2017 16:07:29 +0100 Subject: [PATCH 16/22] [FIX] web_calendar: events not appearing in arabic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fullcalendar expect as `start` and `end` property of an event object: > When specifying Event Objects for events or eventSources, you may > specify a string in IETF format (ex: "Wed, 18 Oct 2009 13:00:00 EST"), > a string in ISO8601 format (ex: "2009-11-05T13:15:30Z") or a UNIX > timestamp. (as seen in https://fullcalendar.io/docs1/event_data/Event_Object/) Before this commit we could for example in arabic provide an invalid date string "٢٠١٧-٠٣-١٦ ١٦:٠٠:٠٦" instead of "2017-03-16 16:00:06". This commit solves this by always providing fullcalendar an ISO8601 date string. opw-705752 --- addons/web_calendar/static/src/js/web_calendar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js index b98f155ad9a..89d125c48af 100644 --- a/addons/web_calendar/static/src/js/web_calendar.js +++ b/addons/web_calendar/static/src/js/web_calendar.js @@ -639,8 +639,8 @@ var CalendarView = View.extend({ date_stop = m_start.toDate(); } var r = { - 'start': moment(date_start).format('YYYY-MM-DD HH:mm:ss'), - 'end': moment(date_stop).format('YYYY-MM-DD HH:mm:ss'), + 'start': moment(date_start).toISOString(), + 'end': moment(date_stop).toISOString(), 'title': the_title, 'allDay': (this.fields[this.date_start].type == 'date' || (this.all_day && evt[this.all_day]) || false), 'id': evt.id, From 508a002e699f6e0935b43798637a6603823d9524 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Fri, 17 Mar 2017 07:39:39 +0100 Subject: [PATCH 17/22] [FIX] web_editor: glitch english several times When using the iframed editor with multilang selector (present if there is another lang installed than en_US) an "English" language was present before each translation language. opw-696891 --- addons/web_editor/static/src/xml/backend.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/web_editor/static/src/xml/backend.xml b/addons/web_editor/static/src/xml/backend.xml index 957f38dac3c..c5b668cc01a 100644 --- a/addons/web_editor/static/src/xml/backend.xml +++ b/addons/web_editor/static/src/xml/backend.xml @@ -38,11 +38,11 @@ -webkit-border-radius: 3px; border-radius: 3px; background: white;"> + - From af64780a2d8df2ec5ec91bab4bc0cc0a7f2c050a Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 16 Mar 2017 15:46:38 +0100 Subject: [PATCH 18/22] [FIX] base: no-break hyphen - Create an invoice with two products - Each line has a long description (spread on 2 lines) - The second line has a negative value - Print the invoice (PDF) The amount spreads on two lines, split as: $ - 1,000.00 This is due to the wkhtmltopdf version with patched QT. Unfortunately, upgrading to the latest version 0.12.4 doesn't solve the problem. The solution is to replace the '-' by a non-breakable hyphen. opw-711002 --- openerp/addons/base/ir/ir_qweb.py | 8 ++++---- openerp/addons/test_converter/tests/test_html.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 4d7e5a02061..ea996769f88 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -686,7 +686,7 @@ class IntegerConverter(osv.AbstractModel): context = {} lang_code = context.get('lang') or 'en_US' - return self.pool['res.lang'].format(cr, uid, [lang_code], '%d', value, grouping=True) + return self.pool['res.lang'].format(cr, uid, [lang_code], '%d', value, grouping=True).replace(r'-', u'\u2011') class FloatConverter(osv.AbstractModel): _name = 'ir.qweb.field.float' @@ -704,7 +704,7 @@ class FloatConverter(osv.AbstractModel): lang_code = context.get('lang') or 'en_US' lang = self.pool['res.lang'] - formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True) + formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True).replace(r'-', u'\u2011') # %f does not strip trailing zeroes. %g does but its precision causes # it to switch to scientific notation starting at a million *and* to @@ -889,7 +889,7 @@ class MonetaryConverter(osv.AbstractModel): lang = self.pool['res.lang'] formatted_amount = lang.format(cr, uid, [lang_code], fmt, Currency.round(cr, uid, display_currency, from_amount), - grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}') + grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'\u2011') pre = post = u'' if display_currency.position == 'before': @@ -1056,7 +1056,7 @@ class QwebWidgetMonetary(osv.AbstractModel): lang_code = qwebcontext.context.get('lang') or 'en_US' formatted_amount = self.pool['res.lang'].format( qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True - ).replace(r' ', u'\N{NO-BREAK SPACE}') + ).replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'\u2011') pre = post = u'' if display.position == 'before': pre = u'{symbol}\N{NO-BREAK SPACE}' diff --git a/openerp/addons/test_converter/tests/test_html.py b/openerp/addons/test_converter/tests/test_html.py index 2f50602c115..5810e24c288 100644 --- a/openerp/addons/test_converter/tests/test_html.py +++ b/openerp/addons/test_converter/tests/test_html.py @@ -67,8 +67,8 @@ class TestFloatExport(TestBasicExport): def test_float(self): converter = self.get_converter('float') - value = converter(42.0) - self.assertEqual(value, "42.0") + value = converter(-42.0) + self.assertEqual(value, u"\u201142.0") value = converter(42.0100) self.assertEqual(value, "42.01") @@ -120,7 +120,7 @@ class TestCurrencyExport(TestExport): def test_currency_post(self): currency = self.create(self.Currency, name="Test", symbol=u"test") - obj = self.create(self.Model, value=0.12) + obj = self.create(self.Model, value=-0.12) converted = self.convert(obj, dest=currency) @@ -129,7 +129,7 @@ class TestCurrencyExport(TestExport): '' - '0.12' + u'\u20110.12' u'\N{NO-BREAK SPACE}{symbol}'.format( obj=obj, symbol=currency.symbol.encode('utf-8') From 22e7b6befb77005d1b1945977191ca11739f38fa Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Fri, 17 Mar 2017 11:11:42 +0100 Subject: [PATCH 19/22] [FIX] web_kanban: only give fields in view to record This commit partially reverts 5b4f7c1 where `fields_view.fields` had been replaced by `fields`, which could have been set to all fields in the model. This modification can sometimes lead to a problem, during the fields value transformation. The ID, which wasn't given previously is now parsed. When creating a new record, the field ID is not an integer but a string (representing the virtual ID). If the field is in the arch, it will be postprocessed (see https://github.com/odoo/odoo/blob/saas-11/addons/web/static/src/js/framework/data_manager.js#L201) and its type will become `id`. As this postprocessing is not done if the field in the view, the parsing was on the original type (integer) and not the special id type. --- addons/web_kanban/static/src/js/kanban_view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js index a0ed0761dcf..30a3517a7bc 100644 --- a/addons/web_kanban/static/src/js/kanban_view.js +++ b/addons/web_kanban/static/src/js/kanban_view.js @@ -376,7 +376,7 @@ var KanbanView = View.extend({ this.record_options = { editable: this.is_action_enabled('edit'), deletable: this.is_action_enabled('delete'), - fields: this.fields, + fields: this.fields_view.fields, qweb: this.qweb, model: this.model, read_only_mode: this.options.read_only_mode, @@ -427,7 +427,7 @@ var KanbanView = View.extend({ grouped_by_m2o: this.grouped_by_m2o, relation: this.relation, qweb: this.qweb, - fields: this.fields, + fields: this.fields_view.fields, quick_create: this._is_quick_create_enabled(), }; }, From 7401807510a103374fa8eb524ae5a5698ae21a39 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Thu, 16 Mar 2017 17:08:04 +0100 Subject: [PATCH 20/22] [FIX] web_editor: copy/paste in the editor Commit 906875fc78cb80f6c3d5f1f14f997008cd406473 (followed by commit 76be5eed449c78eaf0b4208c381169132492ab4a) tried to tweak to copy/paste functionality of the editor. Indeed, in PDF file, when copying a paragraph, a line feed is added at each forced line break (page width end). This forced a line feed and a new paragraph in the pasted text. The goal was to get rid of those useless linebreaks when pasting. The ideal solution would be to detect that the text was copied from a PDF but it is not possible. However, this commit improves the heuristic: if this is not plain text, then nothing is done, the content is pasted normally. If it is plain text (like when copying from pdf), then each line break which is surrounded by letters or numbers is considered an useless line break and replaced by a space. This is obviously not perfect but the previous implementation was really wrong (it was removing line breaks which were not preceeded by punctuation in all cases, which was removing lots of useful line breaks). Note: this commit also fixes a bug -> text was not able to be pasted with Internet Explorer. opw-715920 --- addons/web_editor/static/src/js/summernote.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js index 9d7304caf75..71647828cc6 100644 --- a/addons/web_editor/static/src/js/summernote.js +++ b/addons/web_editor/static/src/js/summernote.js @@ -2313,11 +2313,19 @@ eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) { eventHandler.modules.clipboard.attach = function(layoutInfo) { var $editable = layoutInfo.editable(); $editable.on('paste', function(e) { - e.preventDefault(); - $editable.data('NoteHistory').recordUndo($editable); - var pastedText = ((e.originalEvent || e).clipboardData || window.clipboardData).getData('Text'); - var formattedText = pastedText.replace(/([^.!?:;])\r?\n/g, "$1").trim(); // Remove linebreaks which are not at the end of a sentence - document.execCommand("insertText", false, formattedText); + var clipboardData = ((e.originalEvent || e).clipboardData || window.clipboardData); + // Change nothing if pasting html (copy from text editor / web / ...) or + // if clipboardData is not available (IE / ...) + if (clipboardData && clipboardData.types && clipboardData.types.length === 1 && clipboardData.types[0] === "text/plain") { + e.preventDefault(); + $editable.data('NoteHistory').recordUndo($editable); // FIXME + var pastedText = clipboardData.getData("text/plain"); + // Try removing linebreaks which are not really linebreaks (in a PDF, + // when a sentence goes over the next line, copying it considers it + // a linebreak for example). + var formattedText = pastedText.replace(/([\w-])\r?\n([\w-])/g, "$1 $2").trim(); + document.execCommand("insertText", false, formattedText); + } }); }; From 13bfc8ee503924a7ea42962a84410e669304f360 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 17 Mar 2017 14:32:17 +0100 Subject: [PATCH 21/22] [FIX] crm: remove unused field from fields to merge The field email is no longer present on the crm.lead object opw-725112 --- addons/crm/models/crm_lead.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 55dd43323b2..061891ef8ba 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -32,7 +32,6 @@ CRM_LEAD_FIELDS_TO_MERGE = [ 'city', 'contact_name', 'description', - 'email', 'fax', 'mobile', 'partner_name', From 2c1257fe88a20d0f501977c02863f818ca7d1dd4 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 17 Mar 2017 14:39:48 +0100 Subject: [PATCH 22/22] wow --- addons/crm/models/crm_lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 061891ef8ba..676084daab8 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -486,7 +486,7 @@ class Lead(FormatAddress, models.Model): body = [title] fields = self.env['ir.model.fields'].search([('name', 'in', fields or []), ('model_id.model', '=', self._name)]) for field in fields: - value = self[field.name] + value = getattr(self, field.name, False) if field.ttype == 'selection': value = dict(field.get_values(self.env)).get(value, value) elif field.ttype == 'many2one':