From 19b17365d78febe5fbf5ac8319d19e48703bc369 Mon Sep 17 00:00:00 2001 From: Nans Lefebvre Date: Wed, 4 Sep 2019 06:46:05 +0000 Subject: [PATCH 01/14] [FIX] stock_landed_costs: do not (incorrectly) use float_compare to mean float_is_zero Take a landed cost with an amount_total of 2464788.5 in a currency with no decimals, so precision_digits is 0. Now let the sum of the lines 2464788.499999999. Additionally, we have that 2464788.5 is rounded up to 2464789 and 2464788.499999999 is rounded down to 2464788. So float_compare returns a nonzero value. However, it turns out that the docstring of float_compare contains: Warning: ``float_is_zero(value1-value2)`` is not equivalent to ``float_compare(value1,value2) == 0`` So the intended use was to use float_is_zero. opw-2048388 closes odoo/odoo#36208 Signed-off-by: Nans Lefebvre (len) --- addons/stock_landed_costs/models/stock_landed_cost.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index ca1b544f868..b9fa7ace211 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -131,13 +131,13 @@ class LandedCost(models.Model): prec_digits = self.env.user.company_id.currency_id.decimal_places for landed_cost in self: total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost')) - if not tools.float_compare(total_amount, landed_cost.amount_total, precision_digits=prec_digits) == 0: + if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits): return False val_to_cost_lines = defaultdict(lambda: 0.0) for val_line in landed_cost.valuation_adjustment_lines: val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost - if any(tools.float_compare(cost_line.price_unit, val_amount, precision_digits=prec_digits) != 0 + if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits) for cost_line, val_amount in val_to_cost_lines.items()): return False return True From 3df5c5dd562ca5f0ffa925677653e62dabb05c0c Mon Sep 17 00:00:00 2001 From: Nans Lefebvre Date: Tue, 3 Sep 2019 13:18:15 +0000 Subject: [PATCH 02/14] [FIX] product: filter out archived pricelists from candidate pricelists Have a pricelist Pl set as property_product_pricelist on some partner Pa. Archive Pl, so that the public user Pu use another pricelist Pl'. Go to the shop, create some order. The prices are obtained from Pl'. Log in as Pa; prices are updated using Pl. However since Pl is archived, it is not visible on the pricelist views, so the end user cannot explain why the prices changed. When the pricelist is archived, nothing is done regarding the ir.property storing the partner/pricelist mapping, so the pricelist is retrieved as default nonetheless. We filter out the active pricelists when getting the property. opw 1946476 co-authored with JKE-be closes odoo/odoo#36389 Signed-off-by: Nans Lefebvre (len) --- addons/product/models/product_pricelist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index a8093611c29..dd023de06ce 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -316,10 +316,10 @@ class Pricelist(models.Model): return res[partner_id].id def _get_partner_pricelist_multi_search_domain_hook(self): - return [] + return [('active', '=', True)] def _get_partner_pricelist_multi_filter_hook(self): - return self + return self.filtered('active') def _get_partner_pricelist_multi(self, partner_ids, company_id=None): """ Retrieve the applicable pricelist for given partners in a given company. From 6d8e5efa84ddabd349068bbbaa7c25de14ce1dbc Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 28 Aug 2019 09:54:52 +0000 Subject: [PATCH 03/14] [FIX] purchase: default user_id on bill On top of leading to AccessError in multi-company (if the PO was created by OdooBot), it might also assign a user without Accounting rights to a vendor bill. This reverts commit 392f6f2b2bbb49a8a55b94510d51fa789b20a127. opw-2056061 closes odoo/odoo#36173 Signed-off-by: Nicolas Martinelli (nim) --- addons/purchase/models/purchase.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 021d4d4ca7f..d083ad98af7 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -404,8 +404,6 @@ class PurchaseOrder(models.Model): 'default_company_id': self.company_id.id, 'company_id': self.company_id.id } - if self.user_id: - result['context']['default_user_id'] = self.user_id.id # choose the view_mode accordingly if len(self.invoice_ids) > 1 and not create_bill: result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]" From 3a2eee5c8fc7fe58b5cdd21a43f16f0cd9269f38 Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Wed, 28 Aug 2019 01:30:46 +0000 Subject: [PATCH 04/14] [FIX] website_blog: speed up loading blog posts when there are many Doing filtered on all_post is problematic in case there are many blog.post records (>5000 were observed). It will trigger prefetching which will run the content fields through html_translate which can take many seconds to complete. Instead replace the filtered call with a more precise search domain. opw-2060704, opw-2060706 closes odoo/odoo#36207 Signed-off-by: Nicolas Martinelli (nim) --- addons/website_blog/controllers/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index 6f02268afc0..947ae83b06d 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -238,9 +238,11 @@ class WebsiteBlog(http.Controller): tags = request.env['blog.tag'].search([]) # Find next Post - all_post = BlogPost.search([('blog_id', '=', blog.id)]) + blog_post_domain = [('blog_id', '=', blog.id)] if not request.env.user.has_group('website.group_website_designer'): - all_post = all_post.filtered(lambda r: r.post_date <= fields.Datetime.now()) + blog_post_domain += [('post_date', '<=', fields.Datetime.now())] + + all_post = BlogPost.search(blog_post_domain) if blog_post not in all_post: return request.redirect("/blog/%s" % (slug(blog_post.blog_id))) From c7f11aae6ebe5c752f571ce54ef839c576c93256 Mon Sep 17 00:00:00 2001 From: RomainLibert Date: Wed, 4 Sep 2019 11:36:04 +0000 Subject: [PATCH 05/14] [FIX] hr_holidays: aggregate attendances using day_period and dayofweek Before this commit, if you had defined multiple attendances for the morning or for the afternoon, the system would only use the first attendance defined for either morning or afternoon. This lead to wrong computations of the real leave length. This commit fixes this issue by getting the min(hour_from) and max(hour_to) for the attendances using a read_group. closes odoo/odoo#36413 Signed-off-by: Yannick Tivisse (yti) --- addons/hr_holidays/models/hr_leave.py | 16 +- addons/hr_holidays/tests/__init__.py | 1 + .../tests/test_automatic_leave_dates.py | 212 ++++++++++++++++++ 3 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 addons/hr_holidays/tests/test_automatic_leave_dates.py diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 25ed2b52945..437fbbac2c0 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -6,6 +6,8 @@ import logging import math +from collections import namedtuple + from datetime import datetime, time from pytz import timezone, UTC @@ -18,6 +20,9 @@ from odoo.tools.translate import _ _logger = logging.getLogger(__name__) +# Used to agglomerate the attendances in order to find the hour_from and hour_to +# See _onchange_request_parameters +DummyAttendance = namedtuple('DummyAttendance', 'hour_from, hour_to, dayofweek, day_period') class HolidaysRequest(models.Model): """ Leave Requests Access specifications @@ -276,12 +281,17 @@ class HolidaysRequest(models.Model): return domain = [('calendar_id', '=', self.employee_id.resource_calendar_id.id or self.env.user.company_id.resource_calendar_id.id)] - attendances = self.env['resource.calendar.attendance'].search(domain, order='dayofweek, day_period DESC') + attendances = self.env['resource.calendar.attendance'].read_group(domain, ['ids:array_agg(id)', 'hour_from:min(hour_from)', 'hour_to:max(hour_to)', 'dayofweek', 'day_period'], ['dayofweek', 'day_period'], lazy=False) + + # Must be sorted by dayofweek ASC and day_period DESC + attendances = sorted([DummyAttendance(group['hour_from'], group['hour_to'], group['dayofweek'], group['day_period']) for group in attendances], key=lambda att: (att.dayofweek, att.day_period != 'morning')) + + default_value = DummyAttendance(0, 0, 0, 'morning') # find first attendance coming after first_day - attendance_from = next((att for att in attendances if int(att.dayofweek) >= self.request_date_from.weekday()), attendances[0]) + attendance_from = next((att for att in attendances if int(att.dayofweek) >= self.request_date_from.weekday()), attendances[0] if attendances else default_value) # find last attendance coming before last_day - attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1]) + attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1] if attendances else default_value) if self.request_unit_half: if self.request_date_from_period == 'am': diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py index ba8bdaabb15..e1d3523517c 100644 --- a/addons/hr_holidays/tests/__init__.py +++ b/addons/hr_holidays/tests/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_access_rights +from . import test_automatic_leave_dates from . import test_holidays_flow from . import test_hr_leave_type from . import test_accrual_allocations diff --git a/addons/hr_holidays/tests/test_automatic_leave_dates.py b/addons/hr_holidays/tests/test_automatic_leave_dates.py new file mode 100644 index 00000000000..5b39259d053 --- /dev/null +++ b/addons/hr_holidays/tests/test_automatic_leave_dates.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +from datetime import date, datetime + +from odoo.tests.common import Form, tagged + +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase + + +@tagged('prout') +class TestAutomaticLeaveDates(TestHrHolidaysBase): + def setUp(self): + super(TestAutomaticLeaveDates, self).setUp() + + self.leave_type = self.env['hr.leave.type'].create({ + 'name': 'Automatic Test', + 'time_type': 'leave', + 'allocation_type': 'no', + 'validity_start': False, + }) + + def test_no_attendances(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'No Attendances', + 'attendance_ids': [(5, 0, 0)], + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave']) as leave_form: + leave_form.employee_id = employee + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_display, 0) + + def test_single_attendance_on_morning_and_afternoon(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'simple morning + afternoon', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'monday morning', + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '0', + }), + (0, 0, { + 'name': 'monday afternoon', + 'hour_from': 13, + 'hour_to': 17, + 'day_period': 'afternoon', + 'dayofweek': '0', + })] + }) + + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave']) as leave_form: + leave_form.employee_id = employee + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, .5) + self.assertEqual(leave_form.number_of_hours_display, 4) + + leave_form.request_date_from_period = 'pm' + + self.assertEqual(leave_form.number_of_days_display, .5) + self.assertEqual(leave_form.number_of_hours_display, 4) + + def test_multiple_attendance_on_morning(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'multi morning', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'monday morning 1', + 'hour_from': 8, + 'hour_to': 10, + 'day_period': 'morning', + 'dayofweek': '0', + }), + (0, 0, { + 'name': 'monday morning 2', + 'hour_from': 10.25, + 'hour_to': 12.25, + 'day_period': 'morning', + 'dayofweek': '0', + }), + (0, 0, { + 'name': 'monday afternoon', + 'hour_from': 13, + 'hour_to': 17, + 'day_period': 'afternoon', + 'dayofweek': '0', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave']) as leave_form: + leave_form.employee_id = employee + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, .5) + self.assertEqual(leave_form.number_of_hours_display, 4) + + leave_form.request_date_from_period = 'pm' + + self.assertEqual(leave_form.number_of_days_display, .5) + self.assertEqual(leave_form.number_of_hours_display, 4) + + def test_attendance_on_morning(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'Morning only', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'Monday All day', + 'hour_from': 8, + 'hour_to': 16, + 'day_period': 'morning', + 'dayofweek': '0', + })], + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + with Form(self.env['hr.leave']) as leave_form: + leave_form.employee_id = employee + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + # Ask for morning + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, 1) + self.assertEqual(leave_form.number_of_hours_display, 8) + + # Ask for afternoon + leave_form.request_date_from_period = 'pm' + + self.assertEqual(leave_form.number_of_days_display, 1) + self.assertEqual(leave_form.number_of_hours_display, 8) + + def test_attendance_next_day(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'auto next day', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'tuesday morning', + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '1', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave']) as leave_form: + leave_form.employee_id = employee + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_display, 0) + self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0)) + + def test_attendance_previous_day(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'auto next day', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'monday morning', + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '0', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave']) as leave_form: + leave_form.employee_id = employee + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 3) + leave_form.request_date_to = date(2019, 9, 3) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_display, 0) + self.assertEqual(leave_form.date_from, datetime(2019, 9, 3, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 3, 10, 0, 0)) From 85b9f3abfd1d931193df455474f6686037f61578 Mon Sep 17 00:00:00 2001 From: Nans Lefebvre Date: Thu, 5 Sep 2019 08:54:23 +0000 Subject: [PATCH 06/14] [FIX] product: pricelists with temporary discounts Create a pricelist with a temporary discount on some product only for today. Go to the shop. The discount is applied on the product. Add the product to the cart. Go to the cart page (review order): the price does not use the discount. Because the query getting the pricelist item is some handwritten SQL, it only works for dates, and not e.g. in the case of datetimes. So the pricelist item encoding the discount is not retrieved, and thus not used. It turns out e.g. in _website_product_id_change it gives a datetime in context, explaining why sometimes the discount is applied, and sometimes not. We use fields.Date.to_date as it should work on all date formats we shall pass. Co-written with @sbbaro opw 2054881 closes odoo/odoo#36453 Signed-off-by: Nans Lefebvre (len) --- addons/product/models/product_pricelist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index dd023de06ce..91462e22e3f 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -109,6 +109,7 @@ class Pricelist(models.Model): self.ensure_one() if not date: date = self._context.get('date') or fields.Date.context_today(self) + date = fields.Date.to_date(date) # handwritten query below does not work with non-dates (e.g. datetimes) if not uom_id and self._context.get('uom'): uom_id = self._context['uom'] if uom_id: From 7c28c97724d34f961c2cf271783fbca2fe485fc4 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Thu, 5 Sep 2019 09:22:20 +0000 Subject: [PATCH 07/14] [FIX] hr_expense: sheet multi expense partial payment Create two expense and put them into one sheet Post the sheet Make One partial payment with the wizard (Register Payment) Do it again for the residual amount Before this commit, it crashed because the second payment tried to reconcile itself with an already reconciled line After this commit, all the lines that need to be reconciled actually are without crash OPW 2065501 closes odoo/odoo#36454 Signed-off-by: Lucas Perais (lpe) --- addons/hr_expense/tests/test_expenses.py | 50 +++++++++++++++++++ .../hr_expense_sheet_register_payment.py | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py index d789f6e92be..e9cc5bf366d 100644 --- a/addons/hr_expense/tests/test_expenses.py +++ b/addons/hr_expense/tests/test_expenses.py @@ -53,6 +53,19 @@ class TestCheckJournalEntry(TransactionCase): 'company_id': company.id }) + self.bank_journal = self.env['account.journal'].create({ + 'name': 'Payment Journal', + 'code': 'PAY', + 'type': 'bank', + 'company_id': company.id + }) + + self.outbound_pay_method = self.env['account.payment.method'].create({ + 'name': 'outbound', + 'code': 'out', + 'payment_type': 'outbound', + }) + self.expense = self.env['hr.expense.sheet'].create({ 'name': 'Expense for John Smith', 'employee_id': self.employee.id, @@ -107,3 +120,40 @@ class TestCheckJournalEntry(TransactionCase): self.assertEquals(expense.tax_ids.ids, [self.tax.id]) self.assertEquals(expense.total_amount, 10863.60) self.assertTrue(expense.employee_id in user_demo.employee_ids) + + def test_partial_payment_multiexpense(self): + self.expense_line.unit_amount = 200 + expense_line2 = self.expense_line.copy({ + 'sheet_id': self.expense.id + }) + self.expense.approve_expense_sheets() + self.expense.action_sheet_move_create() + exp_move_lines = self.expense.account_move_id.line_ids + payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') + self.assertEquals(len(payable_move_lines), 2) + + WizardRegister = self.env['hr.expense.sheet.register.payment.wizard'].with_context(active_ids=self.expense.ids) + + register_pay1 = WizardRegister.create({ + 'journal_id': self.bank_journal.id, + 'payment_method_id': self.outbound_pay_method.id, + 'amount': 300, + }) + register_pay1.expense_post_payment() + + exp_move_lines = self.expense.account_move_id.line_ids + payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') + self.assertEquals(len(payable_move_lines.filtered(lambda l: l.reconciled)), 1) + + register_pay2 = WizardRegister.create({ + 'journal_id': self.bank_journal.id, + 'payment_method_id': self.outbound_pay_method.id, + 'amount': 100, + }) + register_pay2.expense_post_payment() + exp_move_lines = self.expense.account_move_id.line_ids + payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') + self.assertEquals(len(payable_move_lines.filtered(lambda l: l.reconciled)), 2) + + full_reconcile = payable_move_lines.mapped('full_reconcile_id') + self.assertEquals(len(full_reconcile), 1) diff --git a/addons/hr_expense/wizard/hr_expense_sheet_register_payment.py b/addons/hr_expense/wizard/hr_expense_sheet_register_payment.py index f378f15a471..54b8d816d14 100644 --- a/addons/hr_expense/wizard/hr_expense_sheet_register_payment.py +++ b/addons/hr_expense/wizard/hr_expense_sheet_register_payment.py @@ -86,7 +86,7 @@ class HrExpenseSheetRegisterPaymentWizard(models.TransientModel): # Reconcile the payment and the expense, i.e. lookup on the payable account move lines account_move_lines_to_reconcile = self.env['account.move.line'] for line in payment.move_line_ids + expense_sheet.account_move_id.line_ids: - if line.account_id.internal_type == 'payable': + if line.account_id.internal_type == 'payable' and not line.reconciled: account_move_lines_to_reconcile |= line account_move_lines_to_reconcile.reconcile() From a3f2c82789520af5a58aa4193d462f4acf8ba529 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 5 Sep 2019 12:29:43 +0000 Subject: [PATCH 08/14] [FIX] stock, mrp: merge SFP for 2 different MO The condition needs to be fined-tuned, so revert for now. This reverts commit 40487aca5cc52d62ad3fd08b9494f190e4413d8c. closes odoo/odoo#36470 Signed-off-by: Nicolas Martinelli (nim) --- addons/mrp/models/stock_move.py | 6 -- .../test_warehouse_multistep_manufacturing.py | 94 ------------------- addons/stock/models/stock_move.py | 13 +-- 3 files changed, 4 insertions(+), 109 deletions(-) diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 13be696e191..f5e931a71d5 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -283,9 +283,3 @@ class StockMove(models.Model): def _should_be_assigned(self): res = super(StockMove, self)._should_be_assigned() return bool(res and not (self.production_id or self.raw_material_production_id)) - - def _search_picking_for_assignation_domain(self): - res = super(StockMove, self)._search_picking_for_assignation_domain() - if self.created_production_id: - res.append(('move_lines.created_production_id', '=', self.created_production_id.id)) - return res diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py index c65fe846885..bf1195fdaf0 100644 --- a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py +++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py @@ -121,100 +121,6 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon): self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id) self.assertFalse(sam_move.move_dest_ids) - def test_manufacturing_complex_product_3_steps(self): - """ Test MO/picking after manufacturing a complex product which uses - manufactured components. Ensure that everything is created and picked - correctly. - """ - # Create manifactured product which uses another manifactured - - product_form = Form(self.env['product.product']) - product_form.name = 'Arrow' - product_form.type = 'product' - product_form.route_ids.clear() - product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id) - product_form.route_ids.add(self.warehouse.mto_pull_id.route_id) - self.complex_product = product_form.save() - - ## Create raw product for manufactured product - product_form = Form(self.env['product.product']) - product_form.name = 'Raw Iron' - product_form.type = 'product' - product_form.uom_id = self.uom_unit - product_form.uom_po_id = self.uom_unit - self.raw_product_2 = product_form.save() - - ## Create bom for manufactured product - bom_product_form = Form(self.env['mrp.bom']) - bom_product_form.product_id = self.complex_product - bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id - with bom_product_form.bom_line_ids.new() as line: - line.product_id = self.finished_product - line.product_qty = 1.0 - with bom_product_form.bom_line_ids.new() as line: - line.product_id = self.raw_product_2 - line.product_qty = 1.0 - - self.complex_bom = bom_product_form.save() - - with Form(self.warehouse) as warehouse: - warehouse.manufacture_steps = 'pbm_sam' - - production_form = Form(self.env['mrp.production']) - production_form.product_id = self.complex_product - production_form.picking_type_id = self.warehouse.manu_type_id - production = production_form.save() - - move_raw_ids = production.move_raw_ids - self.assertEqual(len(move_raw_ids), 2) - sfp_move_raw_id, raw_move_raw_id = move_raw_ids - self.assertEqual(sfp_move_raw_id.product_id, self.finished_product) - self.assertEqual(raw_move_raw_id.product_id, self.raw_product_2) - - for move_raw_id in move_raw_ids: - self.assertEqual(move_raw_id.picking_type_id, self.warehouse.manu_type_id) - - pbm_move = move_raw_id.move_orig_ids - self.assertEqual(len(pbm_move), 1) - self.assertEqual(pbm_move.location_id, self.warehouse.lot_stock_id) - self.assertEqual(pbm_move.location_dest_id, self.warehouse.pbm_loc_id) - self.assertEqual(pbm_move.picking_type_id, self.warehouse.pbm_type_id) - - move_finished_ids = production.move_finished_ids - self.assertEqual(len(move_finished_ids), 1) - self.assertEqual(move_finished_ids.product_id, self.complex_product) - self.assertEqual(move_finished_ids.picking_type_id, self.warehouse.manu_type_id) - sam_move = move_finished_ids.move_dest_ids - self.assertEqual(len(sam_move), 1) - self.assertEqual(sam_move.location_id, self.warehouse.sam_loc_id) - self.assertEqual(sam_move.location_dest_id, self.warehouse.lot_stock_id) - self.assertEqual(sam_move.picking_type_id, self.warehouse.sam_type_id) - self.assertFalse(sam_move.move_dest_ids) - - pickings = production.picking_ids.sorted('id', reverse=True) - self.assertEqual(len(pickings), 3) - - # Picking: SFP PostProcessing -> Stock - ## Stick from Subprocess - picking = pickings[1] - self.assertEqual(len(picking.move_lines), 1) - picking.product_id = self.finished_product - - # Picking: PC Stock -> Preprocessing - ## Stick - ## Raw Iron - picking = pickings[0] - self.assertEqual(len(picking.move_lines), 2) - picking.move_lines[0].product_id = self.finished_product - picking.move_lines[1].product_id = self.raw_product_2 - - # Picking: SFP PostProcessing -> Stock - ## Arrow from this process - picking = pickings[2] - self.assertEqual(len(picking.move_lines), 1) - picking.product_id = self.complex_product - - def test_manufacturing_flow(self): """ Simulate a pick pack ship delivery combined with a picking before manufacturing and store after manufacturing. Also ensure that the MO and diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 7d3998ac442..8dee7a18126 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -672,20 +672,15 @@ class StockMove(models.Model): } } - def _search_picking_for_assignation_domain(self): - return [ + def _search_picking_for_assignation(self): + self.ensure_one() + picking = self.env['stock.picking'].search([ ('group_id', '=', self.group_id.id), ('location_id', '=', self.location_id.id), ('location_dest_id', '=', self.location_dest_id.id), ('picking_type_id', '=', self.picking_type_id.id), ('printed', '=', False), - ('state', 'in', ['draft', 'confirmed', 'waiting', - 'partially_available', 'assigned']) - ] - - def _search_picking_for_assignation(self): - self.ensure_one() - picking = self.env['stock.picking'].search(self._search_picking_for_assignation_domain(), limit=1) + ('state', 'in', ['draft', 'confirmed', 'waiting', 'partially_available', 'assigned'])], limit=1) return picking def _assign_picking(self): From b4a1b87fb669ac2fceeda909f36363c0c76308a3 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 4 Sep 2019 13:09:54 +0000 Subject: [PATCH 09/14] [FIX] mrp: domain on field final_lot_id in mrp.production.work.order.form Steps to reproduce the bug: Let's consider two storable products P1 and P2 where P1 is tracked by serial number - Create a BOM B for P1 where P2 is a component - Create a serial numberv SN for P1 with no quantity - Create a MO M1 for P1 and click on button "Produce" - In the view MRP Product Produce, SN is selectable in the field lot_id - Now change B and set a routing on it - Create a new MO M2 for P1 and click on produce - At least one WO has been created, click on the first one Bug: - SN is not selectable in the field final_lot_id The domain set on the field final_lot_id: "['|', '&', ('product_id', '=', product_id), ('product_id.tracking', '!=', 'serial'), ('use_next_on_work_order_id', '=', id)]" didn't allow to chose SN. With this fix, it's now possible to chose SN on the first WO created for M2 opw:2059745 closes odoo/odoo#36425 Signed-off-by: Simon Goffin (sig) --- addons/mrp/models/mrp_workorder.py | 8 ++++++++ addons/mrp/views/mrp_workorder_views.xml | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 92bfa990996..1c6d87a29c7 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -63,6 +63,9 @@ class MrpWorkorder(models.Model): is_produced = fields.Boolean(string="Has Been Produced", compute='_compute_is_produced') + is_first_wo = fields.Boolean(string="Is the first WO to produce", + compute='_compute_is_first_wo') + state = fields.Selection([ ('pending', 'Pending'), ('ready', 'Ready'), @@ -140,6 +143,11 @@ class MrpWorkorder(models.Model): rounding = self.production_id.product_uom_id.rounding self.is_produced = float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0 + @api.multi + def _compute_is_first_wo(self): + for wo in self: + wo.is_first_wo = (wo.production_id.workorder_ids[0] == wo) + @api.one @api.depends('time_ids.duration', 'qty_produced') def _compute_duration(self): diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index a01e3c700d3..f7499ab677d 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -141,7 +141,8 @@ - + + From 2581693b5da16c3c7362e428d3fe9b8f68032df4 Mon Sep 17 00:00:00 2001 From: Jorge Pinna Puissant Date: Thu, 5 Sep 2019 11:29:33 +0000 Subject: [PATCH 10/14] [FIX] mrp: warning message in production order report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit — create a bom which uses 5.6 units of a product; — make a production order; — reserve the materials (all materials should be available); — print the production order report. Before this commit, a warning message in the report that says : 'All components could not be reserved. Click on the "Check Availability button" to try to reserve components'. This warning message is shown despite the fact that all the components are reserved. This occurs because of a rounding error. Now, the warning message only when a component could not be reserved. opw-2058921 closes odoo/odoo#36462 Signed-off-by: Nicolas Martinelli (nim) --- addons/mrp/report/mrp_production_templates.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/mrp/report/mrp_production_templates.xml b/addons/mrp/report/mrp_production_templates.xml index 531b516f0da..a4df1ef7ec5 100644 --- a/addons/mrp/report/mrp_production_templates.xml +++ b/addons/mrp/report/mrp_production_templates.xml @@ -136,7 +136,8 @@ - + +

All components could not be reserved. Click on the "Check Availability button" to try to reserve components.

From 3cadfebf0fe7a8b9d410ecc80ac935d1f05283e5 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 5 Sep 2019 13:37:30 +0000 Subject: [PATCH 11/14] [FIX] account: avoid traceback in auto-reconciliation Commit 6b6d290a5b647c94760 made sure that the list of candidates was not empty to avoid a zero division. However, it might still happen that the `total_residual` is zero. We make sure to avoid the zero division in this case. opw-2058784 closes odoo/odoo#36482 Signed-off-by: Nicolas Martinelli (nim) --- addons/account/models/account_reconcile_model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 3afd9f34f8e..38832b68ae7 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -563,8 +563,10 @@ class AccountReconcileModel(models.Model): if line_residual > total_residual: amount_percentage = (total_residual / line_residual) * 100 - else: + elif total_residual: amount_percentage = (line_residual / total_residual) * 100 + else: + return False return amount_percentage >= self.match_total_amount_param @api.multi From c6bb103eac3cc645ce6053bf875b768a2b3bbd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Thu, 5 Sep 2019 14:53:16 +0000 Subject: [PATCH 12/14] [IMP] website: avoid computing multi_website_ qcontext if not useful MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Those values are only used on the website navbar when the user is in both groups so the vast majority of the time they are done for no reason. This reduces the number of queries by 2 on every page when the user is not admin and/or multi website is not enabled. Part of task-2061122 closes odoo/odoo#36483 Signed-off-by: Jérémy Kersten (jke) --- addons/website/models/ir_ui_view.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 2f1b4c8d17e..2dc67c1d8c6 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -331,18 +331,19 @@ class View(models.Model): qcontext['main_object'] = self cur = Website.get_current_website() - qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur.domain} - qcontext['multi_website_websites'] = [ - {'website_id': website.id, 'name': website.name, 'domain': website.domain} - for website in Website.search([]) if website != cur - ] + if self.env.user.has_group('website.group_website_publisher') and self.env.user.has_group('website.group_multi_website'): + qcontext['multi_website_websites_current'] = {'website_id': cur.id, 'name': cur.name, 'domain': cur.domain} + qcontext['multi_website_websites'] = [ + {'website_id': website.id, 'name': website.name, 'domain': website.domain} + for website in Website.search([]) if website != cur + ] - cur_company = self.env.user.company_id - qcontext['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name} - qcontext['multi_website_companies'] = [ - {'company_id': comp.id, 'name': comp.name} - for comp in self.env.user.company_ids if comp != cur_company - ] + cur_company = self.env.user.company_id + qcontext['multi_website_companies_current'] = {'company_id': cur_company.id, 'name': cur_company.name} + qcontext['multi_website_companies'] = [ + {'company_id': comp.id, 'name': comp.name} + for comp in self.env.user.company_ids if comp != cur_company + ] qcontext.update(dict( self._context.copy(), From 96266bfe547834e98f933e156c7e96a66b182095 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 5 Sep 2019 08:54:51 +0000 Subject: [PATCH 13/14] [FIX] models: callable in api.depends() sees valid _rec_name Determine `model._rec_name` before the dependencies of the fields. The compute method of the automatic field `display_name` uses a callable depends that retrieves `model._rec_name`. closes odoo/odoo#36480 Signed-off-by: Raphael Collet (rco) --- .../test_new_api/tests/test_new_fields.py | 7 +++++++ odoo/fields.py | 12 ++++++------ odoo/models.py | 18 +++++++++--------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 5c568f873c6..2306b834571 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -74,6 +74,13 @@ class TestFields(common.TransactionCase): field = self.env['test_new_api.message']._fields['x_bool_false_computed'] self.assertFalse(field.depends) + def test_10_display_name(self): + """ test definition of automatic field 'display_name' """ + field = type(self.env['test_new_api.discussion']).display_name + self.assertTrue(field.automatic) + self.assertTrue(field.compute) + self.assertEqual(field.depends, ('name',)) + def test_10_non_stored(self): """ test non-stored fields """ # a field declared with store=False should not have a column diff --git a/odoo/fields.py b/odoo/fields.py index 70466bf3c61..e5590df9ccb 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -485,6 +485,10 @@ class Field(MetaField('DummyField', (object,), {})): def _setup_regular_base(self, model): """ Setup the attributes of a non-related field. """ + pass + + def _setup_regular_full(self, model): + """ Determine the dependencies and inverse field(s) of ``self``. """ if self.depends is not None: return @@ -502,10 +506,6 @@ class Field(MetaField('DummyField', (object,), {})): else: self.depends = tuple(get_depends(self.compute)) - def _setup_regular_full(self, model): - """ Setup the inverse field(s) of ``self``. """ - pass - # # Setup of related fields # @@ -2213,8 +2213,8 @@ class _RelationalMulti(_Relational): for record in records: record[self.name] = record[self.name].filtered(accessible) - def _setup_regular_base(self, model): - super(_RelationalMulti, self)._setup_regular_base(model) + def _setup_regular_full(self, model): + super(_RelationalMulti, self)._setup_regular_full(model) if isinstance(self.domain, list): self.depends += tuple( self.name + '.' + arg[0] diff --git a/odoo/models.py b/odoo/models.py index 7a22538e338..5298c2ffbf8 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2436,6 +2436,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): cls._setup_done = True + # 5. determine and validate rec_name + if cls._rec_name: + assert cls._rec_name in cls._fields, \ + "Invalid rec_name %s for model %s" % (cls._rec_name, cls._name) + elif 'name' in cls._fields: + cls._rec_name = 'name' + elif 'x_name' in cls._fields: + cls._rec_name = 'x_name' + @api.model def _setup_fields(self): """ Setup the fields, except for recomputation triggers. """ @@ -2488,15 +2497,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # register constraints and onchange methods cls._init_constraints_onchanges() - # validate rec_name - if cls._rec_name: - assert cls._rec_name in cls._fields, \ - "Invalid rec_name %s for model %s" % (cls._rec_name, cls._name) - elif 'name' in cls._fields: - cls._rec_name = 'name' - elif 'x_name' in cls._fields: - cls._rec_name = 'x_name' - @api.model def fields_get(self, allfields=None, attributes=None): """ fields_get([fields][, attributes]) From f048652ca7ef761d69a4b1fce2c9db812380b0c0 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 5 Sep 2019 08:54:51 +0000 Subject: [PATCH 14/14] [FIX] models: callable in api.depends() sees valid _rec_name Determine `model._rec_name` before the dependencies of the fields. The compute method of the automatic field `display_name` uses a callable depends that retrieves `model._rec_name`. closes odoo/odoo#36479 Signed-off-by: Raphael Collet (rco) --- .../test_new_api/tests/test_new_fields.py | 7 +++++++ odoo/fields.py | 12 ++++++------ odoo/models.py | 18 +++++++++--------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 343e184e5ca..afdb8cf6d92 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -74,6 +74,13 @@ class TestFields(common.TransactionCase): field = self.env['test_new_api.message']._fields['x_bool_false_computed'] self.assertFalse(field.depends) + def test_10_display_name(self): + """ test definition of automatic field 'display_name' """ + field = type(self.env['test_new_api.discussion']).display_name + self.assertTrue(field.automatic) + self.assertTrue(field.compute) + self.assertEqual(field.depends, ('name',)) + def test_10_non_stored(self): """ test non-stored fields """ # a field declared with store=False should not have a column diff --git a/odoo/fields.py b/odoo/fields.py index f86bf938d33..a84dfa92e62 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -478,6 +478,10 @@ class Field(MetaField('DummyField', (object,), {})): def _setup_regular_base(self, model): """ Setup the attributes of a non-related field. """ + pass + + def _setup_regular_full(self, model): + """ Determine the dependencies and inverse field(s) of ``self``. """ def make_depends(deps): return tuple(deps(model) if callable(deps) else deps) @@ -489,10 +493,6 @@ class Field(MetaField('DummyField', (object,), {})): else: self.depends = make_depends(getattr(self.compute, '_depends', ())) - def _setup_regular_full(self, model): - """ Setup the inverse field(s) of ``self``. """ - pass - # # Setup of related fields # @@ -2182,8 +2182,8 @@ class _RelationalMulti(_Relational): for record in records: record[self.name] = record[self.name].filtered(accessible) - def _setup_regular_base(self, model): - super(_RelationalMulti, self)._setup_regular_base(model) + def _setup_regular_full(self, model): + super(_RelationalMulti, self)._setup_regular_full(model) if isinstance(self.domain, list): self.depends += tuple( self.name + '.' + arg[0] diff --git a/odoo/models.py b/odoo/models.py index 7707ffd2228..4eea8e8343d 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2414,6 +2414,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): cls._setup_done = True + # 5. determine and validate rec_name + if cls._rec_name: + assert cls._rec_name in cls._fields, \ + "Invalid rec_name %s for model %s" % (cls._rec_name, cls._name) + elif 'name' in cls._fields: + cls._rec_name = 'name' + elif 'x_name' in cls._fields: + cls._rec_name = 'x_name' + @api.model def _setup_fields(self): """ Setup the fields, except for recomputation triggers. """ @@ -2466,15 +2475,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # register constraints and onchange methods cls._init_constraints_onchanges() - # validate rec_name - if cls._rec_name: - assert cls._rec_name in cls._fields, \ - "Invalid rec_name %s for model %s" % (cls._rec_name, cls._name) - elif 'name' in cls._fields: - cls._rec_name = 'name' - elif 'x_name' in cls._fields: - cls._rec_name = 'x_name' - # make sure parent_order is set when necessary if cls._parent_store and not cls._parent_order: cls._parent_order = cls._order