From 7a63d471edec45a106fef5f783aeb7eaf43447f6 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Mon, 22 Jan 2018 12:04:27 +0100 Subject: [PATCH 01/55] [FIX] website_event_track: use event timezone Depending on the location, on the website different timezone were used to display tracks: - in `Agenda` day date (as of saas-14): the current user timezone - in `Agenda` track times: the event timezone - in `Talks` subtitle: the event timezone - in `Talks` tracks: the current user timezone - in a track page: the first user (admin) timezone With this change in all those instance the event timezone is used. There is still a usability hindrance in the backend: the timezone used when encoding track or event datetime is the browser timezone. So if the browser is GMT+2 and the event is GMT+6, encoding a 4h00 time would save it as 2h00 (in UTC) and display it on the website as 8h00. This would make sense to encode these datetime in user timezone but currently there is no framework option to do it. opw-805358 closes #22427 --- addons/website_event_track/controllers/main.py | 4 +++- .../views/website_event_track_templates.xml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index 296ba70ecc6..cf69304a87c 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -14,7 +14,7 @@ class WebsiteEventTrackController(http.Controller): @http.route(['''/event//track/
Date
-
+ + () +
Duration

Location
From 2d777d5ade94f4a80862608e2807660f289d9923 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 22 Jan 2018 17:17:52 +0100 Subject: [PATCH 02/55] [FIX] hr: extend employee privacy protection to a few fields A few fields present in the "Private Info" tab of the Employee form did not have the corresponding `groups` attribute. This could cause access rights problems. --- addons/hr/models/hr.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index da78143625a..7ff1b74a56a 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -111,9 +111,10 @@ class Employee(models.Model): active = fields.Boolean('Active', related='resource_id.active', default=True, store=True) # private partner address_home_id = fields.Many2one( - 'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.') + 'res.partner', 'Private Address', help='Enter here the private address of the employee, not the one linked to your company.', + groups="hr.group_hr_user") country_id = fields.Many2one( - 'res.country', 'Nationality (Country)') + 'res.country', 'Nationality (Country)', groups="hr.group_hr_user") gender = fields.Selection([ ('male', 'Male'), ('female', 'Female'), @@ -135,9 +136,9 @@ class Employee(models.Model): domain="[('partner_id', '=', address_home_id)]", groups="hr.group_hr_user", help='Employee bank salary account') - permit_no = fields.Char('Work Permit No') - visa_no = fields.Char('Visa No') - visa_expire = fields.Date('Visa Expire Date') + permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user") + visa_no = fields.Char('Visa No', groups="hr.group_hr_user") + visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user") # image: all image fields are base64 encoded and PIL-supported image = fields.Binary( From bb833825f2b9c87d0978224948c1aff7f9260b3e Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Mon, 22 Jan 2018 17:31:29 +0100 Subject: [PATCH 03/55] [FIX] web: set default you/all option flipped In 60d9f6fe the ir.default replaced uses of ir.values. But the [ir.default].set user_id has opposite meaning to [ir.values].set_default for_all_users. This was not taken into account when adapting javascript in 791152a87 and this commit fixes that. opw-805871 closes #22437 --- addons/web/static/src/js/widgets/debug_manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js index d6069f7d4ec..e70d901caef 100644 --- a/addons/web/static/src/js/widgets/debug_manager.js +++ b/addons/web/static/src/js/widgets/debug_manager.js @@ -446,7 +446,7 @@ DebugManager.include({ $defaults.parent().addClass('o_form_invalid'); return; } - var allUsers = d.$el.find('#formview_default_all').is(':checked'); + var selfUser = d.$el.find('#formview_default_self').is(':checked'); var condition = d.$el.find('#formview_default_conditions').val(); var value = _.find(self.fields, function (field) { return field.name === fieldToSet; @@ -458,7 +458,7 @@ DebugManager.include({ self._active_view.fields_view.model, fieldToSet, value, - allUsers, + selfUser, true, condition || false, ], From 16c6b520fb901491e09cd3b86ac5b49171014892 Mon Sep 17 00:00:00 2001 From: Swapnesh Shah Date: Mon, 22 Jan 2018 23:18:38 +0530 Subject: [PATCH 04/55] [FIX] sms: context is a dict not a callable Closes #22433 --- addons/sms/wizard/send_sms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py index a637b43133b..b1c5a486486 100644 --- a/addons/sms/wizard/send_sms.py +++ b/addons/sms/wizard/send_sms.py @@ -70,7 +70,7 @@ class SendSMS(models.TransientModel): no_phone_partners = [] for partner in partners: default_field_name = 'mobile' if partner.mobile else 'phone' - number = self._sms_sanitization(partner, self.env.context('field_name') or default_field_name) + number = self._sms_sanitization(partner, self.env.context.get('field_name') or default_field_name) if number: phone_numbers.append(number) else: From f68ad51520150cd721814555b9e025da310204eb Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 22 Jan 2018 10:17:40 +0100 Subject: [PATCH 05/55] [FIX] web: honor action's groupBy on reload Let's assume an action with a 'group_by' specified in its context. Unless another groupby is specified in the search view, the action groupby must be used. For list views, it was only working for the first rendering of the view. As soon as it was reloaded, the action groupby was lost. For instance, go to Project, click on a project in the dashboard [the action has 'group_by': 'stage_id' in its context], switch to list view [the list is correctly grouped by 'stage_id'], reload by clicking on the view switcher -> the view isn't grouped anymore. It was actually working by chance on other views because they have specific handling of the groupby (e.g. the kanban view allows the attribute 'default_group_by' on its arch), but by default, the action groupby must always be used at each load/reload if there is no other groupby specified. This rev. restores the behavior of saas-15, before the new views. --- .../web/static/src/js/chrome/view_manager.js | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js index 75029d4cc27..3f3cec9ec7f 100644 --- a/addons/web/static/src/js/chrome/view_manager.js +++ b/addons/web/static/src/js/chrome/view_manager.js @@ -87,12 +87,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, { this.active_view = null; this.registry = view_registry; this.title = this.action.name; - var actionGroupBy = self.action.context.group_by; - if (!actionGroupBy) { - actionGroupBy = []; - } else if (typeof actionGroupBy === 'string') { - actionGroupBy = [actionGroupBy]; - } _.each(views, function (view) { var view_type = view[1] || view.view_type; var View = self.registry.get(view_type); //.prototype.config.Controller; @@ -114,7 +108,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, { action: self.action, limit: self.action.limit, views: self.action.views, - groupBy: actionGroupBy, }, self.flags, self.flags[view_type], view.options), searchable: View.prototype.searchable, title: self.title, @@ -271,16 +264,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, { if (!view.loaded) { view_options = _.extend({}, view.options, view_options, self.env); - if (view_options.groupBy && !view_options.groupBy.length) { - var actionContext = view_options ? view_options.action.context : {}; - var actionGroupBy = actionContext.group_by; - if (!actionGroupBy) { - actionGroupBy = []; - } else if (typeof actionGroupBy === 'string') { - actionGroupBy = [actionGroupBy]; - } - view_options.groupBy = actionGroupBy; - } view.loaded = $.Deferred(); self.create_view(view, view_options).then(function(controller) { view.controller = controller; @@ -512,18 +495,15 @@ var ViewManager = Widget.extend(ControlPanelMixin, { throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s", JSON.stringify(results.error))); } - // this.dataset._model = new Model(this.dataset.model, results.context, results.domain); - // var groupby = results.group_by.length ? results.group_by : action_context.group_by; - // if (_.isString(groupby)) { - // groupby = [groupby]; - // } - // if (!controller.grouped && !_.isEmpty(groupby)){ - // this.dataset.set_sort([]); - // } + // FORWARDPORT THIS UP TO SAAS-11.1 ONLY, NOT LATER + var groupby = results.group_by.length ? results.group_by : action_context.group_by; + if (_.isString(groupby)) { + groupby = [groupby]; + } return { context: results.context, domain: results.domain, - groupBy: results.group_by, + groupBy: groupby || [], }; }, do_push_state: function(state) { From 779afb7ac534f003eee0cf3fd0afae401682095d Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Fri, 19 Jan 2018 15:47:02 +0100 Subject: [PATCH 06/55] [FIX] point_of_sale: pos manager should have unlink rights --- addons/point_of_sale/security/ir.model.access.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv index b83298d0c09..48dfef83314 100644 --- a/addons/point_of_sale/security/ir.model.access.csv +++ b/addons/point_of_sale/security/ir.model.access.csv @@ -28,7 +28,7 @@ access_product_category_manager,product.category manager,product.model_product_c access_product_pricelist_manager,product.pricelist manager,product.model_product_pricelist,group_pos_manager,1,0,0,0 access_pos_session_user,pos.session user,model_pos_session,group_pos_user,1,1,1,0 access_pos_config_user,pos.config user,model_pos_config,group_pos_user,1,0,0,0 -access_pos_config_manager,pos.config user,model_pos_config,group_pos_manager,1,1,1,0 +access_pos_config_manager,pos.config manager,model_pos_config,group_pos_manager,1,1,1,1 access_product_category_pos_manager,pos.category manager,model_pos_category,group_pos_manager,1,1,1,1 access_product_category_pos_user,pos.category user,model_pos_category,group_pos_user,1,0,0,0 access_barcode_nomenclature_pos_user,barcode.nomenclature.pos.user,barcodes.model_barcode_nomenclature,group_pos_user,1,0,0,0 From 296c5a2106950b57445c4fd1cb6cac4cd4be15af Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Wed, 17 Jan 2018 13:31:59 +0100 Subject: [PATCH 07/55] [FIX] point_of_sale: automatic reconciliation with returns In the case where: 1- Do an order on the pos selling a product A (amount = 10) 2- Do another order returning a certain quantity of A (with a quantity < 0) 3- Do a third order selling something else (amount = 5) Before this commit, the account move lines of the payment move and of the sale move of operation 2 weren't reconciled This was because the credit to the receivable account of the return was reconciled with the debit of the first operation, leaving the debit of the payment of the return alone (i. e. the lines number 1 & 3 & 5 were reconciled together, instead of 1 & 2 & 5, and 3 & 4) We end up in a situation like this (at validation time) RECEIVABLE D | C || OPERATION | Line Number ---------------------------------------------------------------------------- 15 | || (1 & 3-Engaging Sale) | 1 | 10 || (1-Payment) | 2 | 10 || (2-Engaging Return) | 3 10 | || (2-Payment Return) | 4 | 5 || (3-Payment) | 5 After this commit, we reconcile the returns for each order before reconciling other transaction chains The fix is limited in its scope as the automatic reconciliation for the pos transaction is fairly recent and a "nice to have" OPW 787298 --- addons/point_of_sale/models/pos_order.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 94d198c0f3d..ca379710e9d 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -339,15 +339,22 @@ class PosOrder(models.Model): for order in self: aml = order.statement_ids.mapped('journal_entry_ids').mapped('line_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id) + + # Reconcile returns first + # to avoid mixing up the credit of a payment and the credit of a return + # in the receivable account + aml_returns = aml.filtered(lambda l: (l.journal_id.type == 'sale' and l.credit) or (l.journal_id.type != 'sale' and l.debit)) try: - aml.reconcile() + aml_returns.reconcile() + (aml - aml_returns).reconcile() except: # There might be unexpected situations where the automatic reconciliation won't # work. We don't want the user to be blocked because of this, since the automatic # reconciliation is introduced for convenience, not for mandatory accounting # reasons. - _logger.error('Reconciliation did not work for order %s', order.name) - continue + # It may be interesting to have the Traceback logged anyway + # for debugging and support purposes + _logger.exception('Reconciliation did not work for order %s', order.name) def _default_session(self): return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) From b3b8a471cc3e02488dcc0da991fd1d828978220d Mon Sep 17 00:00:00 2001 From: Florent de Labarre Date: Thu, 21 Dec 2017 13:38:41 +0100 Subject: [PATCH 08/55] [FIX] stock: To avoid singleton error In this way, it ensures that a single record is used when calling the function _get_move_values opw:806668 --- addons/stock/models/stock_inventory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index c6bfc0549dd..81d273da48e 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -454,9 +454,9 @@ class InventoryLine(models.Model): continue diff = line.theoretical_qty - line.product_qty if diff < 0: # found more than expected - vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id) + vals = line._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id) else: - vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id) + vals = line._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id) move = moves.create(vals) if diff > 0: From eaa73beaaab3fd09198f915d53902563b73f404d Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Mon, 22 Jan 2018 13:25:54 +0100 Subject: [PATCH 09/55] [FIX] project: add project_time_mode_id When creating a new company, it was not possible to set the 'Project Time Unit'. Add the field in the settings and add a default value. opw-805533 --- addons/project/models/res_company.py | 1 + addons/project/models/res_config_settings.py | 5 +++++ addons/project/views/res_config_settings_views.xml | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/addons/project/models/res_company.py b/addons/project/models/res_company.py index 282b611ca75..bdcfa1b2c7e 100644 --- a/addons/project/models/res_company.py +++ b/addons/project/models/res_company.py @@ -8,6 +8,7 @@ class ResCompany(models.Model): _inherit = 'res.company' project_time_mode_id = fields.Many2one('product.uom', string='Project Time Unit', + default=lambda s: s.env.ref('product.product_uom_hour', raise_if_not_found=False), help="This will set the unit of measure used in projects and tasks.\n" "If you use the timesheet linked to projects, don't " "forget to setup the right unit of measure in your employees.") diff --git a/addons/project/models/res_config_settings.py b/addons/project/models/res_config_settings.py index f008f52a5aa..3778eeb8016 100644 --- a/addons/project/models/res_config_settings.py +++ b/addons/project/models/res_config_settings.py @@ -11,3 +11,8 @@ class ResConfigSettings(models.TransientModel): module_rating_project = fields.Boolean(string="Rating on Tasks") module_project_forecast = fields.Boolean(string="Forecasts") group_subtask_project = fields.Boolean("Sub-tasks", implied_group="project.group_subtask_project") + project_time_mode_id = fields.Many2one( + 'product.uom', related='company_id.project_time_mode_id', string='Project Time Unit', + help="This will set the unit of measure used in projects and tasks.\n" + "If you use the timesheet linked to projects, don't " + "forget to setup the right unit of measure in your employees.") diff --git a/addons/project/views/res_config_settings_views.xml b/addons/project/views/res_config_settings_views.xml index 058a8f55655..519bd7c9a3b 100644 --- a/addons/project/views/res_config_settings_views.xml +++ b/addons/project/views/res_config_settings_views.xml @@ -56,6 +56,10 @@
Track time spent on projects and tasks
+
+
From 070babf7e2c846215d94fcc9609518a01772aa65 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Mon, 22 Jan 2018 17:10:32 +0100 Subject: [PATCH 10/55] [FIX] web: css long word breaking out kanban tile When words are too long this may break out of a kanban tile and getting everywhere. This is not wanted and the content should be: - hidden if overflown or - long words should be wrapped or - too long content should be ellipsed The ellipsis is probably the best but this would affect the current behavior (we would have to specify a max-height which unexpectedly may be bothersome to currently existing data). So breaking long words has been choosen in this change. opw-806278 closes #22435 --- addons/web/static/src/less/kanban_view.less | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less index d95a518fdda..153f6997d4d 100644 --- a/addons/web/static/src/less/kanban_view.less +++ b/addons/web/static/src/less/kanban_view.less @@ -208,7 +208,9 @@ .o_kanban_record_headings { line-height: 1.2; .o-flex(1, 1, auto); - + // Ensure long word doesn't break out of container + word-wrap: break-word; + overflow: hidden; } } From baaee5bf6bcd2db1350f2bde20b2ae457ec51e67 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 23 Jan 2018 10:31:32 +0100 Subject: [PATCH 11/55] [FIX] website_sale_delivery: total on eCommerce Make sure to reflect the order information. opw-804374 --- addons/website_sale_delivery/controllers/main.py | 4 +++- .../static/src/js/website_sale_delivery.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py index 1e94a0362ce..0e86dba0307 100644 --- a/addons/website_sale_delivery/controllers/main.py +++ b/addons/website_sale_delivery/controllers/main.py @@ -67,4 +67,6 @@ class WebsiteSaleDelivery(WebsiteSale): 'carrier_id': carrier_id, 'new_amount_delivery': order.delivery_price, 'new_amount_untaxed': order.amount_untaxed, - 'new_amount_tax': order.amount_tax} + 'new_amount_tax': order.amount_tax, + 'new_amount_total': order.amount_total, + } diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index bbdece5fe55..3cb0df152c1 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -18,7 +18,7 @@ odoo.define('website_sale_delivery.checkout', function (require) { $amount_delivery.text(result.new_amount_delivery); $amount_untaxed.text(result.new_amount_untaxed); $amount_tax.text(result.new_amount_tax); - $amount_total.text(result.new_amount_delivery + result.new_amount_untaxed + result.new_amount_tax); + $amount_total.text(result.new_amount_total); $carrier_badge.children('span').text(result.new_amount_delivery); $carrier_badge.removeClass('hidden'); $compute_badge.addClass('hidden'); From 340d70338fb5796c1b290fd72cdf3755c3fcc49d Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 23 Jan 2018 10:39:52 +0100 Subject: [PATCH 12/55] [FIX] website_sale_delivery: round delivery price `delivery_price` is a float, therefore we round it to the currency since a carrier could send more than the expected number of decimals. opw-804374 --- addons/website_sale_delivery/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py index 0e86dba0307..36ee8bd651f 100644 --- a/addons/website_sale_delivery/controllers/main.py +++ b/addons/website_sale_delivery/controllers/main.py @@ -65,7 +65,7 @@ class WebsiteSaleDelivery(WebsiteSale): return {'status': order.delivery_rating_success, 'error_message': order.delivery_message, 'carrier_id': carrier_id, - 'new_amount_delivery': order.delivery_price, + 'new_amount_delivery': order.currency_id.round(order.delivery_price), 'new_amount_untaxed': order.amount_untaxed, 'new_amount_tax': order.amount_tax, 'new_amount_total': order.amount_total, From dec50fc19bdf8a5026081aa4826ad779cf0ea009 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Tue, 23 Jan 2018 09:22:26 +0100 Subject: [PATCH 13/55] [FIX] stock: _action_done Rev[1] fixed a bug where the order inside of the recordset could trigger side effects by merging move when creating the extra moves. However, after the patch, `moves_todo` contained moves that should be backordered (with no quantity done). This commit fixes this oversight. [1] b95cabef0b4478845a318edf86f0705a8c2907fa Fixes #22440 --- addons/stock/models/stock_move.py | 13 +++------ addons/stock/tests/test_move2.py | 45 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index c6aa9d39308..be50495305d 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1009,17 +1009,12 @@ class StockMove(models.Model): move._action_cancel() # Create extra moves where necessary - for move in moves.filtered(lambda m: m.state != 'cancel'): - # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However, - # it is possible that the user changed the value before posting the inventory by a value that should be - # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it - # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be - # consumed/produced and the moves are split correctly). - # FIXME: move rounding to move line - # rounding = move.product_uom.rounding - # move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP') + for move in moves: + if move.state == 'cancel' or move.quantity_done <= 0: + continue moves_todo |= move moves_todo |= move._create_extra_move() + # Split moves where necessary and move quants for move in moves_todo: rounding = move.product_uom.rounding diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py index a3a6b0ae46b..45e898856b7 100644 --- a/addons/stock/tests/test_move2.py +++ b/addons/stock/tests/test_move2.py @@ -743,6 +743,51 @@ class TestSinglePicking(TestStockCommon): backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)]) self.assertEqual(backorder.state, 'confirmed') + def test_backorder_3(self): + """ Check the good behavior of creating a backorder for an available move on a picking with + two available moves. + """ + delivery_order = self.env['stock.picking'].create({ + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_out, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': delivery_order.id, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productB.id, + 'product_uom_qty': 2, + 'product_uom': self.productB.uom_id.id, + 'picking_id': delivery_order.id, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + }) + + # make some stock + pack_location = self.env['stock.location'].browse(self.pack_location) + self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2) + self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2) + + # assign to partially available + delivery_order.action_confirm() + delivery_order.action_assign() + self.assertEqual(delivery_order.state, 'assigned') + + delivery_order.move_lines[0].move_line_ids[0].qty_done = 2 + delivery_order.do_transfer() + + backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery_order.id)]) + self.assertEqual(backorder.state, 'confirmed') + def test_extra_move_1(self): """ Check the good behavior of creating an extra move in a delivery order. This usecase simulates the delivery of 2 item while the initial stock move had to move 1 and there's From 822a5e9085ebb2a774c44ca9b5d91d006f379968 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Tue, 23 Jan 2018 13:37:06 +0100 Subject: [PATCH 14/55] [FIX] account: default FY day and month In any piece of code, execute the following: ``` self.env['account.config.settings'].create({}) ``` It overwrites the values of `fiscalyear_last_day` and `fiscalyear_last_month` to 31 and 12. Setting a default value on a related field will automatically write these values on the related. opw-805205 --- addons/account/models/res_config.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/account/models/res_config.py b/addons/account/models/res_config.py index a14a05dc265..1d10392719d 100644 --- a/addons/account/models/res_config.py +++ b/addons/account/models/res_config.py @@ -15,6 +15,12 @@ class AccountConfigSettings(models.TransientModel): _name = 'account.config.settings' _inherit = 'res.config.settings' + def _default_fiscalyear_last_day(self): + return self.env.user.company_id.fiscalyear_last_day or 31 + + def _default_fiscalyear_last_month(self): + return self.env.user.company_id.fiscalyear_last_month or 12 + @api.one @api.depends('company_id') def _get_currency_id(self): @@ -70,8 +76,10 @@ class AccountConfigSettings(models.TransientModel): the sales and purchase rates or use the usual m2o fields. This last choice assumes that the set of tax defined for the chosen template is complete''') - fiscalyear_last_day = fields.Integer(related='company_id.fiscalyear_last_day', default=31) - fiscalyear_last_month = fields.Selection([(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')], related='company_id.fiscalyear_last_month', default=12) + fiscalyear_last_day = fields.Integer(related='company_id.fiscalyear_last_day', + default=lambda self: self._default_fiscalyear_last_day()) + fiscalyear_last_month = fields.Selection([(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')], + related='company_id.fiscalyear_last_month', default=lambda self: self._default_fiscalyear_last_month()) period_lock_date = fields.Date(string="Lock Date for Non-Advisers", related='company_id.period_lock_date', help="Only users with the 'Adviser' role can edit accounts prior to and inclusive of this date. Use it for period locking inside an open fiscal year, for example.") fiscalyear_lock_date = fields.Date(string="Lock Date", related='company_id.fiscalyear_lock_date', help="No users, including Advisers, can edit accounts prior to and inclusive of this date. Use it for fiscal year locking for example.") From bc69b470731480d5add57c4441599a1f7d639a6c Mon Sep 17 00:00:00 2001 From: tbe-odoo Date: Tue, 26 Dec 2017 12:16:08 +0100 Subject: [PATCH 15/55] [FIX] http: Implicit session deactivation -> Explicit destruction - Replace the implicit session deactivation with explicit destruction. --- odoo/addons/base/res/res_users.py | 8 +++++++- odoo/http.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index 8a79813ba96..a68abf41026 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -14,6 +14,7 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro from odoo.osv import expression from odoo.service.db import check_super from odoo.tools import partition +from odoo.http import root _logger = logging.getLogger(__name__) @@ -372,6 +373,8 @@ class Users(models.Model): if any(key.startswith('context_') or key in ('lang', 'tz') for key in values): self.context_get.clear_cache(self) if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS): + # force deletion of all sessions for these users + root.session_store.delete_sessions_for_uids(self.ids) db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) @@ -385,7 +388,10 @@ class Users(models.Model): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) - return super(Users, self).unlink() + res = super(Users, self).unlink() + # force deletion of all sessions for these users + root.session_store.delete_sessions_for_uids(self.ids) + return res @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): diff --git a/odoo/http.py b/odoo/http.py index 56b1f8e6930..46d744f2f36 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -1040,7 +1040,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.db = db self.uid = uid self.login = login - self.password = password request.uid = uid request.disable_db = False @@ -1055,7 +1054,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): """ if not self.db or not self.uid: raise SessionExpiredException("Session expired") - security.check(self.db, self.uid, self.password) def logout(self, keep_db=False): for k in self.keys(): @@ -1068,7 +1066,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.setdefault("db", None) self.setdefault("uid", None) self.setdefault("login", None) - self.setdefault("password", None) self.setdefault("context", {}) def get_context(self): @@ -1281,6 +1278,17 @@ class DisableCacheMiddleware(object): start_response(status, new_headers) return self.app(environ, start_wrapped) +class OdooSessionStore(werkzeug.contrib.sessions.FilesystemSessionStore): + def delete_sessions_for_uids(self, uids): + # pretty expensive on large session stores, especially non-local! + uids = set(uids) + _logger.info('Deleting all HTTP sessions for UIDs %s', uids) + for sid in self.list(): + s = self.get(sid) + if s.uid and s.uid in uids: + _logger.debug('Deleting session %s', sid) + self.delete(s) + class Root(object): """Root WSGI application for the OpenERP Web Client. """ @@ -1292,7 +1300,7 @@ class Root(object): # Setup http sessions path = odoo.tools.config.session_dir _logger.debug('HTTP sessions stored in: %s', path) - return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession) + return OdooSessionStore(path, session_class=OpenERPSession) @lazy_property def nodb_routing_map(self): From b8f8012af8e9a7b9974a6578133e81ea890b0f23 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Mon, 22 Jan 2018 12:51:32 +0100 Subject: [PATCH 16/55] [FIX] sale_timesheet: recompute delivered qty - Create a product with 'Service Tracking' as 'Create a task in a new project' - Create a SO with this product, validate - In the created task, add a timesheet line The delivered quantity is not updated on the SO. In method `_analytic_compute_delivered_quantity`, the item is skipped if the field `product_uom_id` is not set on the analytic line. However, `product_uom_id` is set in method `_timesheet_postprocess`, which is called after `_analytic_compute_delivered_quantity`. Therefore, we move this part in `_timesheet_preprocess`. opw-805533 --- addons/sale_timesheet/models/account.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 733dad9a0a2..c519258b5b8 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -42,6 +42,11 @@ class AccountAnalyticLine(models.Model): if 'task_id' in values: task = self.env['project.task'].sudo().browse(values['task_id']) values['so_line'] = task.sale_line_id.id or values.get('so_line', False) + + # Set product_uom_id now so delivered qty is computed in SO line + if not 'product_uom_id' in values and all([v in values for v in ['employee_id', 'project_id']]): + employee = self.env['hr.employee'].sudo().browse(values['employee_id']) + values['product_uom_id'] = employee.company_id.project_time_mode_id.id return values @api.multi From c0f004205d9c5696584fbe79180342d7bece3082 Mon Sep 17 00:00:00 2001 From: tbe-odoo Date: Tue, 26 Dec 2017 12:16:08 +0100 Subject: [PATCH 17/55] [FIX] http: Implicit session deactivation -> Explicit destruction - Replace the implicit session deactivation with explicit destruction. --- odoo/addons/base/res/res_users.py | 8 +++++++- odoo/http.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index caf652d0545..3c3dcf010cc 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -14,6 +14,7 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro from odoo.osv import expression from odoo.service.db import check_super from odoo.tools import partition +from odoo.http import root _logger = logging.getLogger(__name__) @@ -373,6 +374,8 @@ class Users(models.Model): if any(key.startswith('context_') or key in ('lang', 'tz') for key in values): self.context_get.clear_cache(self) if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS): + # force deletion of all sessions for these users + root.session_store.delete_sessions_for_uids(self.ids) db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) @@ -386,7 +389,10 @@ class Users(models.Model): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) - return super(Users, self).unlink() + res = super(Users, self).unlink() + # force deletion of all sessions for these users + root.session_store.delete_sessions_for_uids(self.ids) + return res @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): diff --git a/odoo/http.py b/odoo/http.py index 8f9c5932f0f..4bb9253c885 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -1035,7 +1035,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.db = db self.uid = uid self.login = login - self.password = password request.uid = uid request.disable_db = False @@ -1050,7 +1049,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): """ if not self.db or not self.uid: raise SessionExpiredException("Session expired") - security.check(self.db, self.uid, self.password) def logout(self, keep_db=False): for k in self.keys(): @@ -1063,7 +1061,6 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.setdefault("db", None) self.setdefault("uid", None) self.setdefault("login", None) - self.setdefault("password", None) self.setdefault("context", {}) def get_context(self): @@ -1276,6 +1273,17 @@ class DisableCacheMiddleware(object): start_response(status, new_headers) return self.app(environ, start_wrapped) +class OdooSessionStore(werkzeug.contrib.sessions.FilesystemSessionStore): + def delete_sessions_for_uids(self, uids): + # pretty expensive on large session stores, especially non-local! + uids = set(uids) + _logger.info('Deleting all HTTP sessions for UIDs %s', uids) + for sid in self.list(): + s = self.get(sid) + if s.uid and s.uid in uids: + _logger.debug('Deleting session %s', sid) + self.delete(s) + class Root(object): """Root WSGI application for the OpenERP Web Client. """ @@ -1287,7 +1295,7 @@ class Root(object): # Setup http sessions path = odoo.tools.config.session_dir _logger.debug('HTTP sessions stored in: %s', path) - return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession) + return OdooSessionStore(path, session_class=OpenERPSession) @lazy_property def nodb_routing_map(self): From 2d3ecf29b3c089126d04978e512b16d0cd2ec585 Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Mon, 22 Jan 2018 16:35:49 +0100 Subject: [PATCH 18/55] [FIX] web: handle nested x2m default values A `default_get` for a x2many field can return a new list of commands for one of its x2many field, like [[0, 0, { 'groups_id': [[6, 0, [1, 2, 3]]], }]] where [1, 2, 3] is a list of existing ids. This use case was not correctly handled by the BasicModel because `_makeDefaultRecord` was not recursive. Closes #22401 --- .../static/src/js/views/basic/basic_model.js | 203 ++++++++++-------- .../tests/fields/relational_fields_tests.js | 32 +++ 2 files changed, 149 insertions(+), 86 deletions(-) diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 5296c6db7d5..b5b668a43a1 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -3238,92 +3238,7 @@ var BasicModel = AbstractModel.extend({ defs.push(self._fetchNameGet(dp)); record._changes[name] = dp.id; } else if (field.type === 'one2many' || field.type === 'many2many') { - var fieldInfo = record.fieldsInfo[record.viewType][name]; - var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; - var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo; - var fields = view ? view.fields : fieldInfo.relatedFields; - var viewType = view ? view.type : fieldInfo.viewType; - - var x2manyList = self._makeDataPoint({ - context: record.context, - fieldsInfo: fieldsInfo, - fields: fields, - limit: fieldInfo.limit, - modelName: field.relation, - parentID: record.id, - rawContext: fieldInfo && fieldInfo.context, - relationField: field.relation_field, - res_ids: [], - static: true, - type: 'list', - viewType: viewType, - }); - record._changes[name] = x2manyList.id; - x2manyList._changes = []; - var many2ones = {}; - var r; - result[name] = result[name] || []; // handle false value - var isCommandList = result[name].length && _.isArray(result[name][0]); - if (!isCommandList) { - result[name] = [[6, false, result[name]]]; - } - _.each(result[name], function (value) { - // value is a command - if (value[0] === 0) { - // CREATE - r = self._makeDataPoint({ - modelName: x2manyList.model, - context: x2manyList.context, - fieldsInfo: fieldsInfo, - fields: fields, - parentID: x2manyList.id, - viewType: viewType, - }); - x2manyList._changes.push({operation: 'ADD', id: r.id}); - x2manyList._cache[r.res_id] = r.id; - - // this is necessary so the fields are initialized - _.each(r.getFieldNames(), function (fieldName) { - r.data[fieldName] = null; - }); - - r._changes = _.defaults(value[2], r.data); - for (var name in r._changes) { - if (!r._changes[name]) { - continue; - } - var isFieldInView = name in r.fields; - if (isFieldInView && r.fields[name].type === 'many2one') { - var rec = self._makeDataPoint({ - context: r.context, - modelName: r.fields[name].relation, - data: {id: r._changes[name]}, - parentID: r.id, - }); - r._changes[name] = rec.id; - many2ones[name] = true; - } - } - } - if (value[0] === 6) { - // REPLACE_WITH - _.each(value[2], function (res_id) { - x2manyList._changes.push({operation: 'ADD', resID: res_id}); - }); - var def = self._readUngroupedList(x2manyList).then(function () { - return $.when( - self._fetchX2ManysBatched(x2manyList), - self._fetchReferencesBatched(x2manyList) - ); - }); - defs.push(def); - } - }); - - // fetch many2ones display_name - _.each(_.keys(many2ones), function (name) { - defs.push(self._fetchNameGets(x2manyList, name)); - }); + defs.push(self._processX2ManyCommands(record, name, result[name])); } else { record._changes[name] = self._parseServerValue(field, result[name]); } @@ -3497,6 +3412,122 @@ var BasicModel = AbstractModel.extend({ return record; }); }, + /** + * Process x2many commands in a default record by transforming the list of + * commands in operations (pushed in _changes) and fetch the related + * records fields. + * + * Note that this method can be called recursively. + * + * @todo in master: factorize this code with the postprocessing of x2many in + * _applyOnChange + * + * @private + * @param {Object} record + * @param {string} fieldName + * @param {Array[Array]} commands + * @returns {Deferred} + */ + _processX2ManyCommands: function (record, fieldName, commands) { + var self = this; + var defs = []; + var field = record.fields[fieldName]; + var fieldInfo = record.fieldsInfo[record.viewType][fieldName]; + var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; + var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo; + var fields = view ? view.fields : fieldInfo.relatedFields; + var viewType = view ? view.type : fieldInfo.viewType; + + var x2manyList = self._makeDataPoint({ + context: record.context, + fieldsInfo: fieldsInfo, + fields: fields, + limit: fieldInfo.limit, + modelName: field.relation, + parentID: record.id, + rawContext: fieldInfo && fieldInfo.context, + relationField: field.relation_field, + res_ids: [], + static: true, + type: 'list', + viewType: viewType, + }); + record._changes[fieldName] = x2manyList.id; + x2manyList._changes = []; + var many2ones = {}; + var r; + commands = commands || []; // handle false value + var isCommandList = commands.length && _.isArray(commands[0]); + if (!isCommandList) { + commands = [[6, false, commands]]; + } + _.each(commands, function (value) { + // value is a command + if (value[0] === 0) { + // CREATE + r = self._makeDataPoint({ + modelName: x2manyList.model, + context: x2manyList.context, + fieldsInfo: fieldsInfo, + fields: fields, + parentID: x2manyList.id, + viewType: viewType, + }); + x2manyList._changes.push({operation: 'ADD', id: r.id}); + x2manyList._cache[r.res_id] = r.id; + + // this is necessary so the fields are initialized + _.each(r.getFieldNames(), function (fieldName) { + r.data[fieldName] = null; + }); + + r._changes = _.defaults(value[2], r.data); + for (var fieldName in r._changes) { + if (!r._changes[fieldName]) { + continue; + } + var isFieldInView = fieldName in r.fields; + if (isFieldInView) { + var field = r.fields[fieldName]; + var fieldType = field.type; + if (fieldType === 'many2one') { + var rec = self._makeDataPoint({ + context: r.context, + modelName: field.relation, + data: {id: r._changes[fieldName]}, + parentID: r.id, + }); + r._changes[fieldName] = rec.id; + many2ones[fieldName] = true; + } else if (_.contains(['one2many', 'many2many'], fieldType)) { + var x2mCommands = commands[0][2][fieldName]; + defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands)); + } + } + } + } + if (value[0] === 6) { + // REPLACE_WITH + _.each(value[2], function (res_id) { + x2manyList._changes.push({operation: 'ADD', resID: res_id}); + }); + var def = self._readUngroupedList(x2manyList).then(function () { + return $.when( + self._fetchX2ManysBatched(x2manyList), + self._fetchReferencesBatched(x2manyList) + ); + }); + defs.push(def); + } + }); + + // fetch many2ones display_name + _.each(_.keys(many2ones), function (name) { + defs.push(self._fetchNameGets(x2manyList, name)); + }); + + return $.when.apply($, defs); + }, /** * Reads data from server for all missing fields. * diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index fcb852907e3..be922d3fa1a 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -6201,6 +6201,38 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('nested x2many default values', function (assert) { + assert.expect(2); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (args.model === 'partner' && args.method === 'default_get') { + return $.when({turtles: [[0, 0, { + partner_ids: [[6, 0, [4]]], + }]]}); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .badge').length, 1, + "m2mtags should contain one tag"); + assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .o_badge_text').text(), + 'aaa', "tag name should have been correctly loaded"); + + form.destroy(); + }); + QUnit.test('one2many (who contains display_name) with tree view and without form view', function (assert) { assert.expect(1); From 2bdbe40414083f4207218c4fa354a6c9cc108814 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Wed, 24 Jan 2018 11:05:10 +0100 Subject: [PATCH 19/55] [FIX] stock: scrap: constrain on product_uom_qty `precision_get` return digits, so use `precision_digits` in the call to `float_is_zero`. steps to reproduce: - set the digits of product unit of measures to 0 - try to scrap something - it'll fail because the default value of product_uom_qty is "0.0" --- addons/stock/models/stock_move_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index cb9244e4511..a0692a50688 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -90,7 +90,7 @@ class StockMoveLine(models.Model): @api.constrains('product_uom_qty') def check_reserved_done_quantity(self): for move_line in self: - if move_line.state == 'done' and not float_is_zero(move_line.product_uom_qty, precision_rounding=self.env['decimal.precision'].precision_get('Product Unit of Measure')): + if move_line.state == 'done' and not float_is_zero(move_line.product_uom_qty, precision_digits=self.env['decimal.precision'].precision_get('Product Unit of Measure')): raise ValidationError(_('A done move line should never have a reserved quantity.')) @api.onchange('product_id', 'product_uom_id') From 1ac7257dd24492a294f14903baaab68a1058a73a Mon Sep 17 00:00:00 2001 From: Sergei Ruzki Date: Wed, 17 Jan 2018 18:39:20 +0300 Subject: [PATCH 20/55] [CLA] signature for sergeiruzkiicode Done at #22307 --- doc/cla/individual/sergeiruzkiicode.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 doc/cla/individual/sergeiruzkiicode.md diff --git a/doc/cla/individual/sergeiruzkiicode.md b/doc/cla/individual/sergeiruzkiicode.md new file mode 100644 index 00000000000..44af0359a97 --- /dev/null +++ b/doc/cla/individual/sergeiruzkiicode.md @@ -0,0 +1,11 @@ +Belarus, 2017-01-17 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Sergei Ruzki sergei.ruzki@icode.by https://github.com/sergeiruzkiicode From 53e221fc169ba54c677b356379e93a933340883c Mon Sep 17 00:00:00 2001 From: Sergei Ruzki Date: Wed, 17 Jan 2018 18:22:25 +0300 Subject: [PATCH 21/55] [FIX] base: adding BYN to currenciest list From 2016-07-01 BYN is new currency sign for Belarus See https://en.wikipedia.org/wiki/Belarusian_ruble BYR is no longer used. Kept for backward compatibility and stability reason Closes #22307 --- odoo/addons/base/res/res_country_data.xml | 4 ++-- odoo/addons/base/res/res_currency_data.xml | 9 +++++++++ odoo/addons/base/res/res_currency_demo.xml | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/odoo/addons/base/res/res_country_data.xml b/odoo/addons/base/res/res_country_data.xml index 59808f3d6ab..5da077bc962 100644 --- a/odoo/addons/base/res/res_country_data.xml +++ b/odoo/addons/base/res/res_country_data.xml @@ -258,8 +258,8 @@ Belarus by - - + + Belize diff --git a/odoo/addons/base/res/res_currency_data.xml b/odoo/addons/base/res/res_currency_data.xml index 8b108fa978d..6785bc98c00 100644 --- a/odoo/addons/base/res/res_currency_data.xml +++ b/odoo/addons/base/res/res_currency_data.xml @@ -406,6 +406,15 @@ BR 0.01 + Ruble BYR + Kapeyka + + + + BYN + Br + 0.01 + diff --git a/odoo/addons/base/res/res_currency_demo.xml b/odoo/addons/base/res/res_currency_demo.xml index a6e406e783d..ba316d0a7db 100644 --- a/odoo/addons/base/res/res_currency_demo.xml +++ b/odoo/addons/base/res/res_currency_demo.xml @@ -349,6 +349,12 @@ 10228.19 + + + 2016-07-01 + 2.23151 + + 2010-01-01 From b79d05fff0cacb4d99ebc1b60f44d8dab757b806 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 24 Jan 2018 11:25:50 +0100 Subject: [PATCH 22/55] [FIX] sms: switch `phonenumbers` warning to INFO Having it in INFO should be sufficient for its purpose, and will avoid impacting all CI builds done on a system that does not have the lib installed. For the record, this is not a hard requirement because the lib was not available in Debian stable packages at the time of release. It is only enabled on demand for those who want the feature and can install it manually. Also slightly improved the message. Fixes #22426 Closes #22459 --- addons/sms/wizard/send_sms.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py index b1c5a486486..844933d092a 100644 --- a/addons/sms/wizard/send_sms.py +++ b/addons/sms/wizard/send_sms.py @@ -15,8 +15,10 @@ try: except ImportError: _sms_phonenumbers_lib_imported = False - _logger.warning( - "The `phonenumbers` Python module is not installed. Try: pip install phonenumbers." + _logger.info( + "The `phonenumbers` Python module is not available. " + "Phone number validation will be skipped. " + "Try `pip3 install phonenumbers` to install it." ) From 4fa16b195deace02edc05270f3c8357c840df71c Mon Sep 17 00:00:00 2001 From: "Yenthe V.G" Date: Wed, 24 Jan 2018 11:37:03 +0100 Subject: [PATCH 23/55] [CLA] add end date of Yenthe666 for VanRoey Closes #22326 --- doc/cla/corporate/VanRoey.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/cla/corporate/VanRoey.md b/doc/cla/corporate/VanRoey.md index 6fd5828e02c..d5fc3b3cfb8 100644 --- a/doc/cla/corporate/VanRoey.md +++ b/doc/cla/corporate/VanRoey.md @@ -12,7 +12,7 @@ Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666 List of contributors: -Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666 +Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666 (up to 2018-01-26) Vincent Adriaensen vincent.adriaensen@vanroey.be https://github.com/vincentadriaensen Pol Van Dingenen pol.vandingenen@vanroey.be https://github.com/Cyhexy Ward Marissen Ward.Marissen@vanroey.be https://github.com/Wardm95 From ad5a6c7fbde92de2672e4a6aa726985d2976d527 Mon Sep 17 00:00:00 2001 From: Erick Birbe Date: Wed, 24 Jan 2018 06:37:41 -0400 Subject: [PATCH 24/55] [CLA] Update Vauxoo's CLA Incorporate Erick Birbe (ebirbe) as Vauxoo's contributor Closes #21533 --- doc/cla/corporate/vauxoo.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md index ff9f5036b40..f250546a6ef 100644 --- a/doc/cla/corporate/vauxoo.md +++ b/doc/cla/corporate/vauxoo.md @@ -41,3 +41,4 @@ Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto Edgar Rivero edgar@vauxoo.com https://github.com/egrivero Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906 +Erick Birbe erick@vauxoo.com https://github.com/ebirbe From 0ac6043ec746ef4c4d4ddb66edc93c0ae098ca0b Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 24 Jan 2018 11:39:56 +0100 Subject: [PATCH 25/55] [FIX] base: bad back-port 53e221fc was intended for 11.0 and backported too quickly in 10.0 Remove fields that do not exist yet --- odoo/addons/base/res/res_currency_data.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/odoo/addons/base/res/res_currency_data.xml b/odoo/addons/base/res/res_currency_data.xml index 6785bc98c00..8746f4d0b1b 100644 --- a/odoo/addons/base/res/res_currency_data.xml +++ b/odoo/addons/base/res/res_currency_data.xml @@ -406,8 +406,6 @@ BR 0.01 - Ruble BYR - Kapeyka From ffaca543c47adf5d0286ff8e9eeb99b193948679 Mon Sep 17 00:00:00 2001 From: Olivier Colson Date: Wed, 24 Jan 2018 11:40:28 +0100 Subject: [PATCH 26/55] [FIX] account: properly allow switching between CoA when no move has been done A required field was missing at the creation of the wizard doing this. --- addons/account/models/res_config_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 304fc0d54a3..f55acc40d8e 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -68,6 +68,7 @@ class ResConfigSettings(models.TransientModel): 'company_id': self.company_id.id, 'chart_template_id': self.chart_template_id.id, 'transfer_account_id': self.chart_template_id.transfer_account_id.id, + 'code_digits': self.chart_template_id.code_digits, 'sale_tax_rate': 15.0, 'purchase_tax_rate': 15.0, 'complete_tax_set': self.chart_template_id.complete_tax_set, From bf5c90d0be7808a08ad8b1b676bcd698181e1d7b Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 24 Jan 2018 13:13:22 +0100 Subject: [PATCH 27/55] [FIX] web_editor: strange horizontal scroll on mousedown Before this commit, when pressing the mouse button on the end of the ace editor, it scrolled to the right, selecting the whole sentence end. This strange behavior was due to some initialization mistake made by commit https://github.com/odoo/odoo/commit/5a030db3eb77b6d80187f8ae1a64303b2f50391d --- addons/web_editor/static/src/js/ace.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/web_editor/static/src/js/ace.js b/addons/web_editor/static/src/js/ace.js index b33b3c479be..7629d1465c8 100644 --- a/addons/web_editor/static/src/js/ace.js +++ b/addons/web_editor/static/src/js/ace.js @@ -107,12 +107,11 @@ var ViewEditor = Widget.extend({ */ start: function () { this.$viewEditor = this.$('#ace-view-editor'); - this.$editor = this.$(".ace_editor"); this.$viewList = this.$('#ace-view-list'); this.$viewID = this.$('#ace-view-id'); - this.aceEditor = window.ace.edit(this.$viewEditor[0]); this.aceEditor.setTheme("ace/theme/monokai"); + this.$editor = this.$(".ace_editor"); var refX = 0; var resizing = false; From 894229c26855fc177d223f7736ce5ffb2b773cde Mon Sep 17 00:00:00 2001 From: RomainLibert Date: Tue, 23 Jan 2018 17:48:21 +0100 Subject: [PATCH 28/55] [FIX] resource: Fix annoying bug with timezones When encoding global leaves, we put the timezone of this particular leave to False, which raises an exception when given directly to pytz. Simple solution is to pass 'UTC' in case we receive False. This behaviour might be improved in master later. --- addons/resource/models/resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index ddb3238320a..ddf68e079bf 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -34,7 +34,7 @@ def to_naive_utc(datetime, record): def to_tz(datetime, tz_name): - tz = pytz.timezone(tz_name) + tz = pytz.timezone(tz_name) if tz_name else pytz.UTC return pytz.UTC.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(tz).replace(tzinfo=None) From 34bfadb2f2f2f57f4f72515fbc610a521e8f4619 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 24 Jan 2018 04:12:39 -0800 Subject: [PATCH 29/55] [FIX] web: suggested date - Set the OS to a GMT- timezone (e.g. 'America/Los Angeles') - Set the format of the date to '%d/%m/%Y' - Go to 'Account > Adviser > Journal Entries' - Type '02-01-2018' The suggested date is 31/01/2018. Setting the moment object as UTC will lead to a conversion of the date to the OS timezone when calling `toDate`, e.g.: Mon Jan 31 2018 16:00:00 GMT-0800 (PST) UTC should only be used in case of datetime, not date. opw-803466 --- addons/web/static/src/js/views/search_inputs.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/search_inputs.js b/addons/web/static/src/js/views/search_inputs.js index 6459fba77b8..91d85a1901a 100644 --- a/addons/web/static/src/js/views/search_inputs.js +++ b/addons/web/static/src/js/views/search_inputs.js @@ -354,7 +354,11 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{ } // THIS SHOULD BE FORWARDPORTED UP TO SAAS-15, NOT LATER - var m = moment.utc(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'); + if (t === 'datetime') { + var m = moment.utc(v, 'YYYY-MM-DD HH:mm:ss'); + } else { + var m = moment(v, 'YYYY-MM-DD'); + } if (!m.isValid()) { return $.when(null); } var d = m.toDate(); var date_string = formats.format_value(d, this.attrs); From 50beec7fa0029f6ece11df02a26e333a8430ee95 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Tue, 12 Dec 2017 11:49:42 +0100 Subject: [PATCH 30/55] [FIX] payment: improve invalid input management on payment form The different fields weren't correctly checked on a payment form. This commit improve the error messages and display it for each field. It adds too a verification on the fields in the case of the field are filled automatically by Firefox on a refresh (F5). --- addons/payment/static/src/js/payment_form.js | 125 ++++++++++-------- .../payment/static/src/js/payment_portal.js | 3 + 2 files changed, 75 insertions(+), 53 deletions(-) diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js index dd643840e95..21a105da3fa 100644 --- a/addons/payment/static/src/js/payment_form.js +++ b/addons/payment/static/src/js/payment_form.js @@ -57,25 +57,29 @@ odoo.define('payment.payment_form', function (require) { console.warn('payment_form: unset partner_id when adding new token; things could go wrong'); } var form_data = this.getFormData(inputs_form); - var empty_inputs = false; + var wrong_input = false; inputs_form.toArray().forEach(function (element) { - if (element.dataset.isRequired) { - if (element.value.length === 0) { + //skip the check of non visible inputs + if ($(element).attr('type') == 'hidden') { + return true; + } + $(element).closest('div.form-group').removeClass('has-error'); + $(element).siblings( ".o_invalid_field" ).remove(); + //force check of forms validity (useful for Firefox that refill forms automatically on f5) + $(element).trigger("focusout"); + if (element.dataset.isRequired && element.value.length === 0) { $(element).closest('div.form-group').addClass('has-error'); - empty_inputs = true; - } - else { - $(element).closest('div.form-group').removeClass('has-error'); - } + $(element).closest('div.form-group').append('
' + _.str.escapeHTML("The value is invalid.") + '
'); + wrong_input = true; + } + else if ($(element).closest('div.form-group').hasClass('has-error')) { + wrong_input = true; + $(element).closest('div.form-group').append('
' + _.str.escapeHTML("The value is invalid.") + '
'); } }); - if (empty_inputs) { - this.displayError( - _t('Missing values'), - _t('

Please fill all the inputs required.

') - ); + if (wrong_input) { return; } @@ -106,7 +110,7 @@ odoo.define('payment.payment_form', function (require) { else { self.displayError( _t('Server Error'), - _t("

We are not able to add your payment method at the moment.

")); + _t('e.g. Your credit card details are wrong. Please verify.')); } // here we remove the 'processing' icon from the 'add a new payment' button $(button).attr('disabled', false); @@ -118,10 +122,8 @@ odoo.define('payment.payment_form', function (require) { self.displayError( _t('Server Error'), - _t("

We are not able to add your payment method at the moment.

") + - "

" + - (core.debug ? (data.data.message.replace(/\n/g, "
")): '') + - "

" + _t("We are not able to add your payment method at the moment.") + + data.data.message ); }); } @@ -159,16 +161,14 @@ odoo.define('payment.payment_form', function (require) { else { self.displayError( _t('Server Error'), - _t("

We are not able to redirect you to the payment form.

") + _t("We are not able to redirect you to the payment form.") ); } }).fail(function (message, data) { self.displayError( _t('Server Error'), - _t("

We are not able to redirect you to the payment form.

") + - "

" + - (core.debug ? (data.data.message.replace(/\n/g, "
")): '') + - "

" + _t("We are not able to redirect you to the payment form.<") + + data.data.message ); }); } @@ -176,7 +176,7 @@ odoo.define('payment.payment_form', function (require) { // we append the form to the body and send it. this.displayError( _t("Cannot set-up the payment"), - _t("

We're unable to process your payment.

") + _t("We're unable to process your payment.") ); } } @@ -187,7 +187,7 @@ odoo.define('payment.payment_form', function (require) { else { this.displayError( _t('No payment method selected'), - _t('

Please select a payment method.

') + _t('Please select a payment method.') ); } }, @@ -209,25 +209,31 @@ odoo.define('payment.payment_form', function (require) { var inputs_form = $('input', acquirer_form); var form_data = this.getFormData(inputs_form); var ds = $('input[name="data_set"]', acquirer_form)[0]; - var empty_inputs = false; - + var wrong_input = false; + inputs_form.toArray().forEach(function (element) { - if (element.dataset.isRequired) { - if (element.value.length === 0) { + //skip the check of non visible inputs + if ($(element).attr('type') == 'hidden') { + return true; + } + $(element).closest('div.form-group').removeClass('has-error'); + $(element).siblings( ".o_invalid_field" ).remove(); + //force check of forms validity (useful for Firefox that refill forms automatically on f5) + $(element).trigger("focusout"); + if (element.dataset.isRequired && element.value.length === 0) { $(element).closest('div.form-group').addClass('has-error'); - empty_inputs = true; - } - else { - $(element).closest('div.form-group').removeClass('has-error'); - } + var message = '
' + _.str.escapeHTML("The value is invalid.") + '
'; + $(element).closest('div.form-group').append(message); + wrong_input = true; + } + else if ($(element).closest('div.form-group').hasClass('has-error')) { + wrong_input = true; + var message = '
' + _.str.escapeHTML("The value is invalid.") + '
'; + $(element).closest('div.form-group').append(message); } }); - if (empty_inputs) { - this.displayError( - _t('Missing values'), - _t('

Please fill all the inputs required.

') - ); + if (wrong_input) { return; } // We add a 'processing' icon into the 'add a new payment' button @@ -274,17 +280,15 @@ odoo.define('payment.payment_form', function (require) { self.displayError( _t('Server error'), - _t("

We are not able to add your payment method at the moment.

") + - "

" + - (core.debug ? (data.data.message.replace(/\n/g, "
")): '') + - "

" + _t("We are not able to add your payment method at the moment.

") + + data.data.message ); }); } else { this.displayError( _t('No payment method selected'), - _t('

Please select the option to add a new payment method.

') + _t('Please select the option to add a new payment method.') ); } }, @@ -308,7 +312,7 @@ odoo.define('payment.payment_form', function (require) { }, function () { self.displayError( _t('Server Error'), - _t("

We are not able to delete your payment method at the moment.

") + _t("We are not able to delete your payment method at the moment.") ); }); }; @@ -322,7 +326,7 @@ odoo.define('payment.payment_form', function (require) { // if there's records linked to this payment method var content = ''; result[pm_id].forEach(function (sub) { - content += '

' + _.str.escapeHTML(sub.name) + '

'; + content += '

' + sub.name + '

'; }); content = $('

').html(_t('

This card is currently linked to the following records:

') + content); @@ -343,7 +347,7 @@ odoo.define('payment.payment_form', function (require) { }, function (type, err) { self.displayError( _t('Server Error'), - _t("

We are not able to delete your payment method at the moment.

") + (core.debug ? err.data.message : '') + _t("We are not able to delete your payment method at the moment.") + err.data.message ); }); }, @@ -391,12 +395,27 @@ odoo.define('payment.payment_form', function (require) { return $(element).data('acquirer-id'); }, displayError: function (title, message) { - return new Dialog(null, { - title: _t('Error: ') + title, - size: 'medium', - $content: message || "", - buttons: [ - {text: _t('Ok'), close: true}]}).open(); + var $checkedRadio = this.$('input[type="radio"]:checked'), + acquirerID = this.getAcquirerIdFromRadio($checkedRadio[0]), + $acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID); + + if ($checkedRadio.length === 0) { + return new Dialog(null, { + title: _t('Error: ') + _.str.escapeHTML(title), + size: 'medium', + $content: "

" + (_.str.escapeHTML(message) || "") + "

" , + buttons: [ + {text: _t('Ok'), close: true}]}).open(); + } else { + // removed if exist error message + this.$('#payment_error').remove(); + var messageResult = '
'; + if (title != '') { + messageResult = messageResult + '' + _.str.escapeHTML(title) + ':
'; + } + messageResult = messageResult + _.str.escapeHTML(message) + '
'; + $acquirerForm.append(messageResult); + } }, getFormData: function ($form) { var unindexed_array = $form.serializeArray(); diff --git a/addons/payment/static/src/js/payment_portal.js b/addons/payment/static/src/js/payment_portal.js index d7397f1b04d..efab5f0a8e0 100644 --- a/addons/payment/static/src/js/payment_portal.js +++ b/addons/payment/static/src/js/payment_portal.js @@ -17,6 +17,7 @@ $(document).ready(function () { if (valid_value) { $(this).parent('.form-group').addClass('has-success'); $(this).parent('.form-group').removeClass('has-error'); + $(this).siblings('.o_invalid_field').remove(); } else { $(this).parent('.form-group').addClass('has-error'); @@ -31,6 +32,7 @@ $(document).ready(function () { if (valid_value) { $(this).parent('.form-group').addClass('has-success'); $(this).parent('.form-group').removeClass('has-error'); + $(this).siblings('.o_invalid_field').remove(); } else { $(this).parent('.form-group').addClass('has-error'); @@ -46,6 +48,7 @@ $(document).ready(function () { if (valid_value) { $(this).parent('.form-group').addClass('has-success'); $(this).parent('.form-group').removeClass('has-error'); + $(this).siblings('.o_invalid_field').remove(); } else { $(this).parent('.form-group').addClass('has-error'); From daf6ab1c872ed9a04517dccd2b32db8dd4855163 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Wed, 13 Dec 2017 16:07:59 +0100 Subject: [PATCH 31/55] [FIX] Payment, payment_authorize: show an error if customer doesn't have a complete profil on authorize.net payment. If the user has no Zip code, country or city, authorize refuse the payment, but Odoo doens't show any error. The commit invite the user to log in in this case or to fill his missing information --- addons/payment/static/src/js/payment_form.js | 25 +++++++++++++------ addons/payment_authorize/controllers/main.py | 25 +++++++++++++++++-- .../models/authorize_request.py | 5 ++++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js index 21a105da3fa..bf555254d5e 100644 --- a/addons/payment/static/src/js/payment_form.js +++ b/addons/payment/static/src/js/payment_form.js @@ -108,9 +108,15 @@ odoo.define('payment.payment_form', function (require) { } // if the server has returned false, we display an error else { - self.displayError( - _t('Server Error'), - _t('e.g. Your credit card details are wrong. Please verify.')); + if (data.error) { + self.displayError( + '', + data.error); + } else { // if the server doesn't provide an error message + self.displayError( + _t('Server Error'), + _t('e.g. Your credit card details are wrong. Please verify.')); + } } // here we remove the 'processing' icon from the 'add a new payment' button $(button).attr('disabled', false); @@ -265,10 +271,15 @@ odoo.define('payment.payment_form', function (require) { } // if the server has returned false, we display an error else { - self.displayError( - _t('Server Error'), - _t("

We are not able to add your payment method at the moment.

") - ); + if (data.error) { + self.displayError( + '', + data.error); + } else { // if the server doesn't provide an error message + self.displayError( + _t('Server Error'), + _t('e.g. Your credit card details are wrong. Please verify.')); + } } // here we remove the 'processing' icon from the 'add a new payment' button $(button).attr('disabled', false); diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py index 6b0aae9bf6a..451a5875b6d 100644 --- a/addons/payment_authorize/controllers/main.py +++ b/addons/payment_authorize/controllers/main.py @@ -3,8 +3,9 @@ import pprint import logging from werkzeug import urls, utils -from odoo import http +from odoo import http, _ from odoo.http import request +from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) @@ -39,7 +40,27 @@ class AuthorizeController(http.Controller): @http.route(['/payment/authorize/s2s/create_json_3ds'], type='json', auth='public', csrf=False) def authorize_s2s_create_json_3ds(self, verify_validity=False, **kwargs): - token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs) + token = False + acquirer = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))) + + try: + token = acquirer.s2s_process(kwargs) + except ValidationError as e: + message = e.args[0] + if isinstance(message, dict) and 'missing_fields' in message: + msg = _("The transaction cannot be processed because some contact details are missing or invalid: ") + message = msg + ', '.join(message['missing_fields']) + '. ' + if request.env.user._is_public(): + message += _("Please sign in to complete your profile.") + # update message if portal mode = b2b + if request.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited', 'False').lower() == 'false': + message += _("If you don't have any account, please ask your salesperson to update your profile. ") + else: + message += _("Please complete your profile. ") + + return { + 'error': message + } if not token: res = { diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index 939abcaf049..5a5f30072f1 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -139,6 +139,11 @@ class AuthorizeAPI(): etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual' billTo = etree.SubElement(payment_profile, "billTo") etree.SubElement(billTo, "address").text = (partner.street or '' + (partner.street2 if partner.street2 else '')) or None + + missing_fields = [partner._fields[field].string for field in ['city', 'country_id', 'zip'] if not partner[field]] + if missing_fields: + raise ValidationError({'missing_fields': missing_fields}) + etree.SubElement(billTo, "city").text = partner.city etree.SubElement(billTo, "state").text = partner.state_id.name or None etree.SubElement(billTo, "zip").text = partner.zip From 2ba6301d341400003298370b643f724eec822f06 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Wed, 13 Dec 2017 16:17:41 +0100 Subject: [PATCH 32/55] [FIX] website_sale: stop the generation of invoices if the option is unchecked in the e-commerce settings An invoice was always generated even if the option was unchecked when we the order is confirmed. --- addons/sale_payment/models/payment.py | 3 +-- .../views/sale_portal_templates.xml | 2 +- addons/website_sale/models/__init__.py | 1 + addons/website_sale/models/payment.py | 25 +++++++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 addons/website_sale/models/payment.py diff --git a/addons/sale_payment/models/payment.py b/addons/sale_payment/models/payment.py index 410d287b247..758bb94c112 100644 --- a/addons/sale_payment/models/payment.py +++ b/addons/sale_payment/models/payment.py @@ -66,10 +66,9 @@ class PaymentTransaction(models.Model): self.sale_order_id.with_context(send_email=True).action_confirm() if self.state == 'done': - _logger.info('<%s> transaction completed, auto-confirming order %s (ID %s) and generating invoice', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id) + _logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id) if self.sale_order_id.state in ('draft', 'sent'): self.sale_order_id.with_context(send_email=True).action_confirm() - self._generate_and_pay_invoice() elif self.state not in ['cancel', 'error'] and self.sale_order_id.state == 'draft': _logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id) self.sale_order_id.force_quotation_send() diff --git a/addons/sale_payment/views/sale_portal_templates.xml b/addons/sale_payment/views/sale_portal_templates.xml index df86ce75998..5ca7511fbec 100644 --- a/addons/sale_payment/views/sale_portal_templates.xml +++ b/addons/sale_payment/views/sale_portal_templates.xml @@ -13,7 +13,7 @@ -
+

Pay with

diff --git a/addons/website_sale/models/__init__.py b/addons/website_sale/models/__init__.py index 8cc73414b40..3a394e73151 100644 --- a/addons/website_sale/models/__init__.py +++ b/addons/website_sale/models/__init__.py @@ -7,6 +7,7 @@ from . import mail_compose_message from . import product from . import res_country from . import res_partner +from . import payment from . import sale_order from . import ir_model_fields from . import website diff --git a/addons/website_sale/models/payment.py b/addons/website_sale/models/payment.py new file mode 100644 index 00000000000..3bebb9dfff8 --- /dev/null +++ b/addons/website_sale/models/payment.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging + +from odoo import models + +_logger = logging.getLogger(__name__) + + +class PaymentTransaction(models.Model): + _inherit = 'payment.transaction' + + # -------------------------------------------------- + # Sale management + # -------------------------------------------------- + + def _confirm_so(self): + """Generate the invoice automatically when option is enabled from ecommerce setting""" + result = super(PaymentTransaction, self)._confirm_so() + IrConfigParameter = self.env['ir.config_parameter'].sudo() + if self.state == 'done' and IrConfigParameter.get_param('website_sale.automatic_invoice', default=False): + _logger.info('<%s> transaction completed, generating invoice for order %s (ID %s)', self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id) + self._generate_and_pay_invoice() + return result From f62b31caf3011e50e357db7fa6c93612fa412de5 Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Mon, 18 Dec 2017 13:47:39 +0100 Subject: [PATCH 33/55] [FIX/IMP] account,account_payment,sale,sale_payment: add a payment notification message for user who are coming back in Odoo after a payment (like in online quotations & e-commerce) WARNING: To avoid breaking any configuration in stable (11.0), the template payment_confirmation_status has been replicated in each module. They must be removed before a forward-port in master --- .../views/account_portal_templates.xml | 10 ++-- .../views/account_portal_templates.xml | 40 +++++++++++++++- .../views/payment_portal_templates.xml | 26 ++++++++++ addons/sale/views/sale_portal_templates.xml | 10 ++-- .../views/sale_portal_templates.xml | 39 ++++++++++++++- addons/website_quote/controllers/main.py | 1 + .../views/website_quote_templates.xml | 47 +++++++++++++------ 7 files changed, 146 insertions(+), 27 deletions(-) diff --git a/addons/account/views/account_portal_templates.xml b/addons/account/views/account_portal_templates.xml index 2ff82b4bc3a..5d62cc9481a 100644 --- a/addons/account/views/account_portal_templates.xml +++ b/addons/account/views/account_portal_templates.xml @@ -95,7 +95,7 @@
- +
@@ -253,8 +253,8 @@ + + + + + diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml index f599329e5e1..16776b880d1 100644 --- a/addons/payment/views/payment_portal_templates.xml +++ b/addons/payment/views/payment_portal_templates.xml @@ -122,5 +122,31 @@ + + diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml index d38b93cb821..1d724064c7b 100644 --- a/addons/sale/views/sale_portal_templates.xml +++ b/addons/sale/views/sale_portal_templates.xml @@ -172,7 +172,7 @@
- +
Date:
@@ -335,8 +335,8 @@ + + +