From df7953f3031a2d3b0f2abf30a41df4955fb6b553 Mon Sep 17 00:00:00 2001 From: Arnold Moyaux Date: Fri, 4 Jan 2019 13:35:28 +0000 Subject: [PATCH 01/95] [FIX] stock: unreserve drop all the processed work Usecase to reproduce(or any other way to lower initial demand on move): - Build a MO with 5 products to do - Produce 3 - Update the quantity on the mo to 3 -> All the consumed quantity on move lines is dropped. It happens because lowering the initial demand on a move will call unlink on its move lines. (increase it will update the move's state and reassign if it comes from a supplier) Instead of dropping and reassign the move in order to change the reservation, it is possible to directly write the new reserved quantity on the move lines and that will update the reservation on qaunt directly. Also we don't want move lines without reserved quantity and quantity done thus we drop them. closes odoo/odoo#29941 --- addons/mrp/models/mrp_production.py | 5 ++- addons/mrp/models/stock_move.py | 18 +++++++++ addons/mrp/tests/test_order.py | 46 ++++++++++++++++++++++ addons/mrp/wizard/change_production_qty.py | 4 -- 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 636d0abd8dc..608a0eace65 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -439,7 +439,10 @@ class MrpProduction(models.Model): move = self.move_raw_ids.filtered(lambda x: x.bom_line_id.id == bom_line.id and x.state not in ('done', 'cancel')) if move: if quantity > 0: - move[0].write({'product_uom_qty': quantity}) + move[0]._decrease_reserved_quanity(quantity) + move[0].with_context(do_not_unreserve=True).write({'product_uom_qty': quantity}) + move[0]._recompute_state() + move[0]._action_assign() move.unit_factor = quantity / move.raw_material_production_id.product_qty elif quantity < 0: # Do not remove 0 lines if move[0].quantity_done > 0: diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 9db5a2b9d49..0821c2f8449 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -182,6 +182,24 @@ class StockMove(models.Model): self.sudo().unlink() return processed_moves + def _decrease_reserved_quanity(self, quantity): + """ Decrease the reservation on move lines but keeps the + all other data. + """ + move_line_to_unlink = self.env['stock.move.line'] + for move in self: + reserved_quantity = quantity + for move_line in self.move_line_ids: + if move_line.product_uom_qty > reserved_quantity: + move_line.product_uom_qty = reserved_quantity + else: + move_line.product_uom_qty = 0 + reserved_quantity -= move_line.product_uom_qty + if not move_line.product_uom_qty and not move_line.qty_done: + move_line_to_unlink |= move_line + move_line_to_unlink.unlink() + return True + def _prepare_phantom_move_values(self, bom_line, quantity): return { 'picking_id': self.picking_id.id if self.picking_id else False, diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 656156a9cde..b49c39c226b 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -617,3 +617,49 @@ class TestMrpOrder(TestMrpCommon): mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") + + def test_product_produce_5(self): + """ Build 5 final products with different consumed lots, + then edit the finished quantity and update the Manufacturing + order quantity. Then check if the produced quantity do not + change and it is possible to close the MO. + """ + self.stock_location = self.env.ref('stock.stock_location_stock') + mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot') + self.assertEqual(len(mo), 1, 'MO should have been created') + + lot_1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': p1.id, + }) + lot_2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': p1.id, + }) + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 10, lot_id=lot_1) + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 10, lot_id=lot_2) + + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) + mo.action_assign() + + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 5.0, + }) + + for produce_line in produce_wizard.produce_line_ids: + produce_line.qty_done = produce_line.qty_to_consume + produce_wizard.do_produce() + + mo.move_finished_ids.move_line_ids.qty_done -= 1 + update_quantity_wizard = self.env['change.production.qty'].create({ + 'mo_id': mo.id, + 'product_qty': 4, + }) + update_quantity_wizard.change_prod_qty() + + self.assertEqual(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).quantity_done, 20, 'Update the produce quantity should not impact already produced quantity.') + mo.button_mark_done() diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index 38a125c3524..cddc9101043 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -50,7 +50,6 @@ class ChangeProductionQty(models.TransientModel): qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id) factor = production.product_uom_id._compute_quantity(production.product_qty - qty_produced, production.bom_id.product_uom_id) / production.bom_id.product_qty boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id) - done_quantities = {move: move.quantity_done for move in production.move_raw_ids} for line, line_data in lines: production._update_raw_move(line, line_data) operation_bom_qty = {} @@ -60,9 +59,6 @@ class ChangeProductionQty(models.TransientModel): self._update_product_to_produce(production, production.product_qty - qty_produced) moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) moves._action_assign() - for move in production.move_raw_ids: - if move.quantity_done != done_quantities[move]: - move._set_quantity_done(done_quantities[move]) for wo in production.workorder_ids: operation = wo.operation_id if operation_bom_qty.get(operation.id): From c5a3b607e6ee5ad4cafc6e427490ff361b79c707 Mon Sep 17 00:00:00 2001 From: Jorge Pinna Puissant Date: Mon, 7 Jan 2019 13:13:47 +0000 Subject: [PATCH 02/95] [FIX] website_sale: empty error message opw-1918450 Before this commit, the error message wasn't shown when a field was empty. Now, the error message shows correctly. closes odoo/odoo#29993 --- addons/website_sale/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 37ddd182ca5..401ad96f511 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -539,7 +539,7 @@ class WebsiteSale(http.Controller): except ValidationError: error["vat"] = 'error' - if [err for err in error.items() if err == 'missing']: + if [err for err in error.values() if err == 'missing']: error_message.append(_('Some required fields are empty.')) return error, error_message From eb11b465dc0cfed5159b9f794a2762693d87fe14 Mon Sep 17 00:00:00 2001 From: Andreas Perhab Date: Thu, 1 Nov 2018 10:15:55 +0000 Subject: [PATCH 03/95] [FIX] odoo: fix addons paths on windows with use of normcase This is necessary to get --dev=xml working on windows, because the xml import uses normcase in file_open and the case normalized filename is then used to find the addon path (which was not found before on windows) With this fix arch_fs is now correctly filled in windows and --dev=xml works. closes odoo/odoo#28331 --- odoo/modules/module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 15f4aae9a50..daf7fb7c440 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -128,17 +128,17 @@ def initialize_sys_path(): global ad_paths global hooked - dd = tools.config.addons_data_dir + dd = os.path.normcase(tools.config.addons_data_dir) if os.access(dd, os.R_OK) and dd not in ad_paths: ad_paths.append(dd) for ad in tools.config['addons_path'].split(','): - ad = os.path.abspath(tools.ustr(ad.strip())) + ad = os.path.normcase(os.path.abspath(tools.ustr(ad.strip()))) if ad not in ad_paths: ad_paths.append(ad) # add base module path - base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons')) + base_path = os.path.normcase(os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))) if base_path not in ad_paths and os.path.isdir(base_path): ad_paths.append(base_path) From 83f5dc8e3515a6c885b944472733ca2194d6c498 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Mon, 14 Jan 2019 11:31:16 +0100 Subject: [PATCH 04/95] [FIX] account: do not set unknow field when creating invoice The field `reference_type` has been removed with b5bb5bd421ad5c643f72602f6a0f97a4a0eb8711 Adapt newly forward-ported test to not use this field. --- addons/account/tests/test_account_customer_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/account/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py index 83822ef8ffb..0906eae595b 100644 --- a/addons/account/tests/test_account_customer_invoice.py +++ b/addons/account/tests/test_account_customer_invoice.py @@ -158,7 +158,6 @@ class TestAccountCustomerInvoice(AccountTestUsers): invoice = self.env['account.invoice'].create(dict( name="Test Customer Invoice", - reference_type="none", payment_term_id=payment_term.id, journal_id=journalrec.id, partner_id=partner3.id, From 4a3f04bcc580c514c61e3bd608fa2b85065434cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 11 Jan 2019 10:21:49 +0000 Subject: [PATCH 05/95] [FIX] web: add exponential backoff strategy for lost connection Before this commit, the web client had a naive strategy to handle lost connections: it tried to poll the server every 2 seconds until a rpc succeeds. This works quite well from the perspective of the user, but may be a problem from the perspective of the server. If a server is down for a longish period, then each users active tabs will then perform a request every 2 seconds. This means that the server will be progressively hammered by many requests, which will clutter the logs, and make it more difficult to gracefully recover. With this commit, we simply exponentially increase the delay each time, and add a little jitter to give a better distribution. closes odoo/odoo#30136 --- .../static/src/js/services/crash_manager.js | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js index e1da816c8e5..31aa178dc1b 100644 --- a/addons/web/static/src/js/services/crash_manager.js +++ b/addons/web/static/src/js/services/crash_manager.js @@ -22,6 +22,7 @@ var map_title ={ var CrashManager = core.Class.extend({ init: function() { this.active = true; + this.isConnected = true; }, enable: function () { this.active = true; @@ -29,8 +30,29 @@ var CrashManager = core.Class.extend({ disable: function () { this.active = false; }, - rpc_error: function(error) { + handleLostConnection: function () { var self = this; + if (!this.isConnected) { + // already handled, nothing to do. This can happen when several + // rpcs are done in parallel and fail because of a lost connection. + return; + } + this.isConnected = false; + var delay = 2000; + core.bus.trigger('connection_lost'); + + setTimeout(function checkConnection() { + ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () { + core.bus.trigger('connection_restored'); + self.isConnected = true; + }).fail(function () { + // exponential backoff, with some jitter + delay = (delay * 1.5) + 500*Math.random(); + setTimeout(checkConnection, delay); + }); + }, delay); + }, + rpc_error: function(error) { if (!this.active) { return; } @@ -38,16 +60,7 @@ var CrashManager = core.Class.extend({ return; } if (error.code === -32098) { - core.bus.trigger('connection_lost'); - this.connection_lost = true; - var timeinterval = setInterval(function() { - var options = {shadow: true}; - ajax.jsonRpc('/web/webclient/version_info', 'call', {}, options).then(function () { - clearInterval(timeinterval); - core.bus.trigger('connection_restored'); - self.connection_lost = false; - }); - }, 2000); + this.handleLostConnection(); return; } var handler = core.crash_registry.get(error.data.name, true); From 8da3f0c87385c9fcd9846d19568c87b130f8ae88 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Fri, 11 Jan 2019 13:36:02 +0000 Subject: [PATCH 06/95] [FIX] crm: create lead from email in right sale team and company crm lead is created with a default company_id which is the current company of the user When creating a lead from email, this logic doesn't hold, especially in v12.0 where the __system__ user is not someone real In that case, we retrieve the company from the sales team OPW 1918837 closes odoo/odoo#30146 --- addons/crm/models/crm_lead.py | 4 + .../crm/tests/test_new_lead_notification.py | 93 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 7fa44b31bfd..8f9b8f3638b 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1204,6 +1204,10 @@ class Lead(models.Model): if msg_dict.get('priority') in dict(crm_stage.AVAILABLE_PRIORITIES): defaults['priority'] = msg_dict.get('priority') defaults.update(custom_values) + + # assign right company + if 'company_id' not in defaults and 'team_id' in defaults: + defaults['company_id'] = self.env['crm.team'].browse(defaults['team_id']).company_id.id return super(Lead, self).message_new(msg_dict, custom_values=defaults) def _message_post_after_hook(self, message, *args, **kwargs): diff --git a/addons/crm/tests/test_new_lead_notification.py b/addons/crm/tests/test_new_lead_notification.py index dedcd02449a..7335dafa17e 100644 --- a/addons/crm/tests/test_new_lead_notification.py +++ b/addons/crm/tests/test_new_lead_notification.py @@ -41,3 +41,96 @@ class NewLeadNotification(TestCrmCases): # The user should have a new unread message lead_user = lead.sudo(self.crm_salesman) self.assertTrue(lead_user.message_needaction) + + def test_new_lead_from_email_multicompany(self): + companies = self.env['res.company'].search([]) + + company0 = companies[0] + company1 = companies[1] + self.env.user.write({ + 'company_ids': [(4, company0.id, False), (4, company1.id, False)], + }) + + crm_team_model = self.env['ir.model'].search([('model', '=', 'crm.team')]) + crm_lead_model = self.env['ir.model'].search([('model', '=', 'crm.lead')]) + self.env["ir.config_parameter"].sudo().set_param("mail.catchall.domain", 'aqualung.com') + + crm_team0 = self.env['crm.team'].create({ + 'name': 'crm team 0', + 'company_id': company0.id, + }) + crm_team1 = self.env['crm.team'].create({ + 'name': 'crm team 1', + 'company_id': company1.id, + }) + + mail_alias0 = self.env['mail.alias'].create({ + 'alias_name': 'sale_team_0', + 'alias_model_id': crm_lead_model.id, + 'alias_parent_model_id': crm_team_model.id, + 'alias_parent_thread_id': crm_team0.id, + 'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team0.id, + }) + mail_alias1 = self.env['mail.alias'].create({ + 'alias_name': 'sale_team_1', + 'alias_model_id': crm_lead_model.id, + 'alias_parent_model_id': crm_team_model.id, + 'alias_parent_thread_id': crm_team1.id, + 'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team1.id, + }) + + crm_team0.write({'alias_id': mail_alias0.id}) + crm_team1.write({'alias_id': mail_alias1.id}) + + new_message0 = """MIME-Version: 1.0 +Date: Thu, 27 Dec 2018 16:27:45 +0100 +Message-ID: blablabla0 +Subject: sale team 0 in company 0 +From: A client +To: sale_team_0@aqualung.com +Content-Type: multipart/alternative; boundary="000000000000a47519057e029630" + +--000000000000a47519057e029630 +Content-Type: text/plain; charset="UTF-8" + + +--000000000000a47519057e029630 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +
A good message
+ +--000000000000a47519057e029630-- +""" + + new_message1 = """MIME-Version: 1.0 +Date: Thu, 27 Dec 2018 16:27:45 +0100 +Message-ID: blablabla1 +Subject: sale team 1 in company 1 +From: B client +To: sale_team_1@aqualung.com +Content-Type: multipart/alternative; boundary="000000000000a47519057e029630" + +--000000000000a47519057e029630 +Content-Type: text/plain; charset="UTF-8" + + +--000000000000a47519057e029630 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +
A good message bis
+ +--000000000000a47519057e029630-- +""" + crm_lead0_id = self.env['mail.thread'].message_process('crm.lead', new_message0) + crm_lead1_id = self.env['mail.thread'].message_process('crm.lead', new_message1) + + crm_lead0 = self.env['crm.lead'].browse(crm_lead0_id) + crm_lead1 = self.env['crm.lead'].browse(crm_lead1_id) + + self.assertEqual(crm_lead0.team_id, crm_team0) + self.assertEqual(crm_lead1.team_id, crm_team1) + + self.assertEqual(crm_lead0.company_id, company0) + self.assertEqual(crm_lead1.company_id, company1) From 023869970c04a432f0c7d350f09587675afc9c82 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Fri, 11 Jan 2019 14:50:15 +0000 Subject: [PATCH 07/95] [FIX] hr_holidays: days allocated - Go to Leaves > Managers > All > Allocations - Create an allocation for: Leave Type: Legal Leaves Mode: By Department - Validate When going to the Legal Leaves form view, the days allocated are not shown. This is because the domain is too restrictive. The allocation has a `date_from` which is `False`, since it is only set for Accrual leaves. We adapt the domain accordingly. opw-1921338 closes odoo/odoo#30152 --- addons/hr_holidays/models/hr_leave_type.py | 25 +++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index 1911faf2d88..31ea964d22f 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -8,6 +8,7 @@ import logging from odoo import api, fields, models from odoo.exceptions import ValidationError +from odoo.osv import expression from odoo.tools.translate import _ from odoo.tools.float_utils import float_round @@ -204,9 +205,18 @@ class HolidaysType(models.Model): @api.multi def _compute_group_days_allocation(self): + domain = [ + ('holiday_status_id', 'in', self.ids), + ('holiday_type', '!=', 'employee'), + ('state', '=', 'validate'), + ] + domain2 = [ + '|', + ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))), + ('date_from', '=', False), + ] grouped_res = self.env['hr.leave.allocation'].read_group( - [('holiday_status_id', 'in', self.ids), ('holiday_type', '!=', 'employee'), ('state', '=', 'validate'), - ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)))], + expression.AND([domain, domain2]), ['holiday_status_id', 'number_of_days'], ['holiday_status_id'], ) @@ -267,11 +277,16 @@ class HolidaysType(models.Model): def action_see_days_allocated(self): self.ensure_one() action = self.env.ref('hr_holidays.hr_leave_allocation_action_all').read()[0] - action['domain'] = [ + domain = [ + ('holiday_status_id', 'in', self.ids), ('holiday_type', '!=', 'employee'), - ('holiday_status_id', '=', self.ids[0]), - ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))) ] + domain2 = [ + '|', + ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))), + ('date_from', '=', False), + ] + action['domain'] = expression.AND([domain, domain2]) action['context'] = { 'default_holiday_type': 'department', 'default_holiday_status_id': self.ids[0], From 8057b739af28484a7b30c2d985244347dca74006 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Fri, 11 Jan 2019 13:46:33 +0000 Subject: [PATCH 08/95] [FIX] product: weight unit Since commit 52a85e57b9a6353c14abdb, it is possible to configure the weight unit. Therefore, we should not hardcode `kg` anymore. opw-1919821 closes odoo/odoo#30147 --- addons/product/views/product_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index d4507b0bb46..f14f35c8cff 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -301,7 +301,7 @@