From ff613d8763655c57e0b179df4059bf5fe9490886 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 26 Jan 2017 09:44:57 +0100 Subject: [PATCH 01/20] [FIX] hr: correctly format mention partner suggestions Rev. 36f6f26 changed the format of the returned list of employees. Javascript is expecting an array containing the array of employees, each employee being a dict with keys id, name, email. But from rev. 36f6f26, it returned an array of employees, themselves represented by an array containing their id, name and email. As the format changed, the Javascript mention mechanism never used the prefetched employees, and always fallbacked on an RPC to fetch suggestions amongs all partners. --- addons/hr/res_partner.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/hr/res_partner.py b/addons/hr/res_partner.py index 5f85ce66186..aa9aa516b66 100644 --- a/addons/hr/res_partner.py +++ b/addons/hr/res_partner.py @@ -14,8 +14,9 @@ class Partner(models.Model): try: employee_group = self.env.ref('base.group_user') - for user in employee_group.users: - suggestions.append((user.partner_id.id, user.name, user.email)) + hr_suggestions = [{'id': user.partner_id.id, 'name': user.name, 'email': user.email} + for user in employee_group.users] + suggestions.append(hr_suggestions) return suggestions except AccessError: return suggestions From 4232a081640134eaced9c89834a5e15ee1c7362b Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Thu, 26 Jan 2017 10:29:04 +0100 Subject: [PATCH 02/20] [FIX] mass_mailing: ondelete cascade on transient The test wizard will be dropped eventually but it is not possible to delete the mass-mailing before the transient is cleaned too due to the required field. To make it faster, add a ondelete cascade on the field. Closes #15217 --- addons/mass_mailing/wizard/test_mailing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py index 20c57f80609..ea570ebb755 100644 --- a/addons/mass_mailing/wizard/test_mailing.py +++ b/addons/mass_mailing/wizard/test_mailing.py @@ -11,7 +11,8 @@ class TestMassMailing(osv.TransientModel): _columns = { 'email_to': fields.char('Recipients', required=True, help='Comma-separated list of email addresses.'), - 'mass_mailing_id': fields.many2one('mail.mass_mailing', 'Mailing', required=True), + 'mass_mailing_id': fields.many2one( + 'mail.mass_mailing', 'Mailing', required=True, ondelete='cascade'), } _defaults = { From 5ea8d4ea33258d3a1decf388a94f011f1e1e4579 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 26 Jan 2017 10:58:23 +0100 Subject: [PATCH 03/20] [FIX] base: fix local variable 'res' referenced before assignment In the loop `for action in self`, if all iterations passed in the first `if` block only, res was never assigned, but returned. This is an oversight during the conversion of the module to the new API in revision eb04aa1e395d347c474181e90f8c8eded1d17ca3 One could ask himself why `res = False` is before the loop and not in this `if` block, but we believe this is in the case multiple actions are passed to this method, you wouldn't like the result of the first actions in the loop to be overwritten by `False` when the condition of the last action of the loop is not met. For instance the first action returns as result an action but for the second, the condition to run it is not met, in such a case you would like what the first action returned as action. opw-706150 --- openerp/addons/base/ir/ir_actions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 2f58c46042a..d4a6fc7bf7c 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -948,6 +948,7 @@ class IrActionsServer(models.Model): :return: an action_id to be executed, or False is finished correctly without return action """ + res = False for action in self: eval_context = self._get_eval_context(action) condition = action.condition From cfcc37bca50baf43843ac938ea26cf23d7a30b25 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 26 Jan 2017 08:34:41 +0100 Subject: [PATCH 04/20] [FIX] expression: leaf in o2m with m2o inherits'd field The reverse field of a one2many could be originating from an inherits'd field, this was solved in some instance with f5e5bbda. The issue could still happen in some instances when doing a comparison of: - the one2many field to a False value, - the one2many with a negative operator and an empty set to negate, With this change, the ORM is used in such a situation. closes #15234 opw-704962 --- openerp/addons/test_inherit/tests/test_inherit.py | 6 ++++++ openerp/osv/expression.py | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openerp/addons/test_inherit/tests/test_inherit.py b/openerp/addons/test_inherit/tests/test_inherit.py index f6057817ee4..8e12b0b7e08 100644 --- a/openerp/addons/test_inherit/tests/test_inherit.py +++ b/openerp/addons/test_inherit/tests/test_inherit.py @@ -74,6 +74,12 @@ class test_inherits(common.TransactionCase): self.assertIn(daughter, partner_demo.daughter_ids) # search the partner from the daughter record + partners = self.env['res.partner'].search([('daughter_ids', 'like', 'not existing daugther')]) + self.assertFalse(partners) + partners = self.env['res.partner'].search([('daughter_ids', 'not like', 'not existing daugther')]) + self.assertIn(partner_demo, partners) + partners = self.env['res.partner'].search([('daughter_ids', '!=', False)]) + self.assertIn(partner_demo, partners) partners = self.env['res.partner'].search([('daughter_ids', 'in', daughter.ids)]) self.assertIn(partner_demo, partners) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index f29fc8f7084..2be571e538c 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -981,7 +981,14 @@ class expression(object): if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, column._fields_id, comodel._table)), model)) + # determine ids from column._fields_id + if comodel._fields[column._fields_id].store: + ids1 = select_distinct_from_where_not_null(cr, column._fields_id, comodel._table) + else: + ids2 = comodel.search(cr, uid, [(column._fields_id, '!=', False)], context=context) + recs = comodel.browse(cr, SUPERUSER_ID, ids2, {'prefetch_fields': False}) + ids1 = recs.mapped(column._fields_id).ids + push(create_substitution_leaf(leaf, ('id', o2m_op, ids1), model)) elif column._type == 'many2many': rel_table, rel_id1, rel_id2 = column._sql_names(model) From a8b1d30fca6bd0597746a619161af2f362ebac60 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Thu, 26 Jan 2017 15:20:43 +0100 Subject: [PATCH 05/20] [FIX] hr: Bank account from employee not loaded on payment on expense The bank account didn't appear when selecting SEPA credit transfer in ACCOUNTING>PURCHASE>PAYMENTS to refund an employee. The bank account is taken from partner set with the field home_address_id. So the account holder have to be set on the bank_account_id of the employee with the home_address_id. opw:702881 --- addons/hr/hr.py | 4 ++++ addons/hr/hr_view.xml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/hr/hr.py b/addons/hr/hr.py index e87ae89af24..f9b536c6915 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -198,6 +198,10 @@ class hr_employee(osv.osv): @api.multi def write(self, vals): + if 'address_home_id' in vals: + account_id = vals.get('bank_account_id') or self.bank_account_id.id + if account_id: + self.env['res.partner.bank'].browse(account_id).partner_id = vals['address_home_id'] tools.image_resize_images(vals) return super(hr_employee, self).write(vals) diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index 41ee36fb9cd..c6399119b1a 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -62,7 +62,7 @@ - + Date: Thu, 24 Nov 2016 17:26:11 +0100 Subject: [PATCH 06/20] [FIX] stock: picking's state according to its move lines There are some problems in the way the picking compute its state according to its move lives if the delivery strategy is partial. When a picking was in a partially available state and then you would force availability, the state would stay in partial availability. Therefore, we changed the way the state is calculated more like it was done before. This issue stems from the new API migration. --- addons/stock/models/stock_picking.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 8b4b73d56eb..5a26aebbced 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -314,22 +314,18 @@ class Picking(models.Model): self.state = 'cancel' elif all(move.state in ['cancel', 'done'] for move in self.move_lines): self.state = 'done' - elif self.move_type == 'one': - ordered_moves = self.move_lines.filtered( - lambda move: move.state not in ['cancel', 'done'] - ).sorted( - key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0, reverse=False - ) - self.state = ordered_moves[0].state else: - filtered_moves = self.move_lines.filtered(lambda move: move.state not in ['cancel', 'done']) - if not all(move.state == 'assigned' for move in filtered_moves) and any(move.state == 'assigned' for move in filtered_moves): - self.state = 'partially_available' - elif any(move.partially_available for move in filtered_moves): + # We sort our moves by importance of state: "confirmed" should be first, then we'll have + # "waiting" and finally "assigned" at the end. + moves_todo = self.move_lines\ + .filtered(lambda move: move.state not in ['cancel', 'done'])\ + .sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0) + if self.move_type == 'one': + self.state = moves_todo[0].state + elif moves_todo[0].state != 'assigned' and any(x.partially_available or x.state == 'assigned' for x in moves_todo): self.state = 'partially_available' else: - ordered_moves = filtered_moves.sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0, reverse=True) - self.state = ordered_moves[0].state + self.state = moves_todo[-1].state @api.one @api.depends('move_lines.priority') From c14ffc8339d66d29ed84907bb7ce5332f87ad870 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Thu, 26 Jan 2017 16:04:25 +0100 Subject: [PATCH 07/20] [ADD][TEST] stock: picking's state according to its move lines Tests 72 and 73 are proving the fix of rev XXX --- addons/stock/tests/test_stock_flow.py | 193 +++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 2 deletions(-) diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 87585773386..3f09260ed26 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1168,7 +1168,6 @@ class TestStockFlow(TestStockCommon): total_qty = sum([quant.qty for quant in quants]) self.assertEqual(total_qty, 0, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) - def test_30_check_with_no_incoming_lot(self): """ Picking in without lots and picking out with""" @@ -1236,7 +1235,6 @@ class TestStockFlow(TestStockCommon): self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot2.id]), 1.0, 'Wrong sum of quants with lot 2') self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot3.id]), 2.0, 'Wrong sum of quants with lot 3') - def test_40_pack_in_pack(self): """ Put a pack in pack""" picking_out = self.PickingObj.create({ @@ -1494,3 +1492,194 @@ class TestStockFlow(TestStockCommon): # We should also make sure that when matching stock moves with pack operations, it takes the correct self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should only have 2 links beween the move and the 2 operations') self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end') + + def test_70_picking_state_all_at_once_reserve(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "all at once" and "reserve" scenario + # ----------------------------------------------------------- + # get one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 1, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # create a "all at once" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'one' + + 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': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # receive one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 2, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # recheck availability of the delivery order, it should be assigned + picking_out.action_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_71_picking_state_all_at_once_force_assign(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "all at once" and "force assign" scenario + # ----------------------------------------------------------- + # create a "all at once" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + 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': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # force assign on the delivery order, it should be assigned + picking_out.force_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_72_picking_state_partial_reserve(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "partial" and "reserve" scenario + # ----------------------------------------------------------- + # get one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 1, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # create a "partial" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + 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': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in partially available + picking_out.action_assign() + self.assertEquals(picking_out.state, "partially_available") + + # receive one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 2, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # recheck availability of the delivery order, it should be assigned + picking_out.action_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_73_picking_state_partial_force_assign(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "partial" and "force assign" scenario + # ----------------------------------------------------------- + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + 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': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # force assign on the delivery order, it should be assigned + picking_out.force_assign() + self.assertEquals(picking_out.state, "assigned") \ No newline at end of file From 0c76ff484609a0f3b386b5d223f409ab55f0034d Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 26 Jan 2017 18:45:14 +0100 Subject: [PATCH 08/20] [FIX] account: modal account.invoice.line form traceback In some instances we could get a traceback in the form view of an account.invoice.line object. This is because the `parent` field can be undefined depending of the way the form view was opened. For example there can be a account.invoice.line list in a sale.order.line form and this lead to a product_id with a traceback capable context. --- addons/account/views/account_invoice_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 4d27ae6d465..27a854248df 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -61,7 +61,7 @@ + context="parent and {'partner_id': parent.partner_id}"/>