From 9f31e50ee944934a2e2cae6a92491ecc400cdde1 Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Tue, 13 Jun 2017 21:19:46 -0700 Subject: [PATCH 01/13] [FIX] point_of_sale: don't launch Firefox before Odoo has started When using Wi-Fi the Odoo instance on the POSBox will start after a Wi-Fi connection is established. This takes a bit longer than when you're using a wired connection. If Firefox gets started before you end up with a 'Could not connect to localhost:8069' error. --- .../overwrite_after_init/etc/xdg/openbox/autostart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart index e2b61e2f591..15908b4c5aa 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart +++ b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/xdg/openbox/autostart @@ -3,4 +3,11 @@ xset s off xset -dpms export HOME=/tmp -/usr/bin/firefox http://localhost:8069/point_of_sale/display & + +DISPLAY_URL='http://localhost:8069/point_of_sale/display' + +# wait until odoo is ready, otherwise you will have to plug a +# keyboard into the posbox just to refresh firefox +until wget -q "${DISPLAY_URL}" -O /dev/null ; do sleep 1 ; done + +/usr/bin/firefox "${DISPLAY_URL}" & From 46263eb398443f2e19c737c4488f43990f244085 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Tue, 23 May 2017 14:09:16 +0200 Subject: [PATCH 02/13] [FIX] stock: backport fix negative quants Original commit (in 10.0) be9dce625c55e1b2d6039573c7035d61f762edc8 From original commit: It is still possible to have negative and positive quants in the same location because of returns: if you send something to the customer that is not there and you return it, you will still be able to reserve the returned goods to send to another client. Before, if you would do an inventory adjustment, it would not take into account these returned quants and their negative counterpart, which made them difficult to get out of the system. This fix takes them into account by creating two movements for one inventory line: move the positive counterpart to the inventory location before getting back from this location the same quantity. This way, even if you have 0 as quantity on hand but you have those 2 quants, it will eliminate them. (if you are increasing the stock, part of the process might have done it automatically already). Also, a key of context has been added which authorizes the process described above in the case of both a tracked product and no lot_id on the stock inventory OPW 743107 Closes #17167 --- addons/stock/stock.py | 65 +++++++++---- addons/stock/tests/test_stock_flow.py | 132 +++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 19 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index a5959536d90..59ece80fa24 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2987,35 +2987,64 @@ class stock_inventory_line(osv.osv): res['value']['product_qty'] = th_qty return res - def _resolve_inventory_line(self, cr, uid, inventory_line, context=None): - stock_move_obj = self.pool.get('stock.move') - quant_obj = self.pool.get('stock.quant') - diff = inventory_line.theoretical_qty - inventory_line.product_qty - if not diff: - return - #each theorical_lines where difference between theoretical and checked quantities is not 0 is a line for which we need to create a stock move - vals = { + # Do not forward port in 10.0 and beyond + def _get_move_values(self, cr, uid, inventory_line, qty, location_id, location_dest_id): + return { 'name': _('INV:') + (inventory_line.inventory_id.name or ''), 'product_id': inventory_line.product_id.id, 'product_uom': inventory_line.product_uom_id.id, + 'product_uom_qty': qty, 'date': inventory_line.inventory_id.date, 'company_id': inventory_line.inventory_id.company_id.id, 'inventory_id': inventory_line.inventory_id.id, 'state': 'confirmed', 'restrict_lot_id': inventory_line.prod_lot_id.id, 'restrict_partner_id': inventory_line.partner_id.id, - } + 'location_id': location_id, + 'location_dest_id': location_dest_id, + } + + def _fixup_negative_quants(self, cr, uid, inventory_line): + """ This will handle the irreconciable quants created by a force availability followed by a + return. When generating the moves of an inventory line, we look for quants of this line's + product created to compensate a force availability. If there are some and if the quant + which it is propagated from is still in the same location, we move it to the inventory + adjustment location before getting it back. Getting the quantity from the inventory + location will allow the negative quant to be compensated. + """ + quant_obj = self.pool.get('stock.quant') + stock_move_obj = self.pool.get('stock.move') + quant_ids = self._get_quants(cr, uid, inventory_line) + for quant in quant_obj.browse(cr, uid, quant_ids).filtered(lambda q: q.propagated_from_id.location_id.id == inventory_line.location_id.id): + # send the quantity to the inventory adjustment location + move_out_vals = self._get_move_values(cr, uid, inventory_line, quant.qty, inventory_line.location_id.id, inventory_line.product_id.property_stock_inventory.id) + move_out = stock_move_obj.create(cr, uid, move_out_vals) + move_out = stock_move_obj.browse(cr, uid, [move_out]) + quant_obj.quants_reserve(cr, uid, [(quant, quant.qty)], move_out) + move_out.action_done() + + # get back the quantity from the inventory adjustment location + move_in_vals = self._get_move_values(cr, uid, inventory_line, quant.qty, inventory_line.product_id.property_stock_inventory.id, inventory_line.location_id.id) + move_in = stock_move_obj.create(cr, uid, move_in_vals) + move_in = stock_move_obj.browse(cr, uid, [move_in]) + move_in.action_done() + + def _resolve_inventory_line(self, cr, uid, inventory_line, context=None): + stock_move_obj = self.pool.get('stock.move') + quant_obj = self.pool.get('stock.quant') + self._fixup_negative_quants(cr, uid, inventory_line) + + if float_compare(inventory_line.theoretical_qty, inventory_line.product_qty, precision_rounding=inventory_line.product_id.uom_id.rounding) == 0: + return False + diff = inventory_line.theoretical_qty - inventory_line.product_qty + + #each theorical_lines where difference between theoretical and checked quantities is not 0 is a line for which we need to create a stock move inventory_location_id = inventory_line.product_id.property_stock_inventory.id - if diff < 0: - #found more than expected - vals['location_id'] = inventory_location_id - vals['location_dest_id'] = inventory_line.location_id.id - vals['product_uom_qty'] = -diff + if diff < 0: # found more than expected + vals = self._get_move_values(cr, uid, inventory_line, abs(diff), inventory_location_id, inventory_line.location_id.id) else: - #found less than expected - vals['location_id'] = inventory_line.location_id.id - vals['location_dest_id'] = inventory_location_id - vals['product_uom_qty'] = diff + vals = self._get_move_values(cr, uid, inventory_line, abs(diff), inventory_line.location_id.id, inventory_location_id) + move_id = stock_move_obj.create(cr, uid, vals, context=context) move = stock_move_obj.browse(cr, uid, move_id, context=context) if diff > 0: diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 09012465fe9..4bfa38d835c 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1269,4 +1269,134 @@ class TestStockFlow(TestStockCommon): self.assertEqual(len(neg_quants), 0, 'There are negative quants!') # 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') \ No newline at end of file + self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end') + + # Do not forward port in 10.0 and beyond + def test_inventory_adjustment_and_negative_quants_1(self): + """Make sure negative quants from returns get wiped out with an inventory adjustment""" + productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'}) + stock_location = self.env.ref('stock.stock_location_stock') + customer_location = self.env.ref('stock.stock_location_customers') + location_loss = self.env.ref('stock.location_inventory') + + # Create a picking out and force availability + picking_out = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + self.env['stock.move'].create({ + 'name': productA.name, + 'product_id': productA.id, + 'product_uom_qty': 1, + 'product_uom': productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + picking_out.action_confirm() + picking_out.force_assign() + picking_out.do_transfer() + + # Create return picking for all goods + default_data = self.env['stock.return.picking']\ + .with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\ + .default_get([ + 'move_dest_exists', + 'product_return_moves' + ]) + + list_return_moves = default_data['product_return_moves'] + default_data['product_return_moves'] = [(0, 0, return_move) for return_move in list_return_moves] + + return_wiz = self.env['stock.return.picking']\ + .with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\ + .create(default_data) + res = return_wiz._create_returns()[0] + return_pick = self.env['stock.picking'].browse(res) + return_pick.action_assign() + return_pick.do_transfer() + + # Make an inventory adjustment to set the quantity to 0 + inventory = self.env['stock.inventory'].create({ + 'name': 'Starting for product_1', + 'filter': 'product', + 'location_id': stock_location.id, + 'product_id': productA.id, + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.") + self.assertEqual(inventory.line_ids.theoretical_qty, 0, "Theoretical quantity should be zero.") + inventory.action_done() + + # The inventory adjustment should have created two moves + self.assertEqual(len(inventory.move_ids), 2) + quantity = inventory.move_ids.mapped('product_qty') + self.assertEqual(quantity, [1, 1], "Moves created with wrong quantity.") + location_ids = inventory.move_ids.mapped('location_id').ids + self.assertEqual(set(location_ids), {stock_location.id, location_loss.id}) + + # There should be no quant in the stock location + quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)]) + self.assertEqual(len(quants), 0) + + # There should be one quant in the inventory loss location + quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)]) + self.assertEqual(len(quant), 1) + self.assertEqual(quant.qty, 1) + + def test_inventory_adjustment_and_negative_quants_2(self): + """Make sure negative quants get wiped out with an inventory adjustment""" + productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'}) + stock_location = self.env.ref('stock.stock_location_stock') + customer_location = self.env.ref('stock.stock_location_customers') + location_loss = self.env.ref('stock.location_inventory') + + # Create a picking out and force availability + picking_out = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + self.env['stock.move'].create({ + 'name': productA.name, + 'product_id': productA.id, + 'product_uom_qty': 1, + 'product_uom': productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + picking_out.action_confirm() + picking_out.force_assign() + picking_out.do_transfer() + + # Make an inventory adjustment to set the quantity to 0 + inventory = self.env['stock.inventory'].create({ + 'name': 'Starting for product_1', + 'filter': 'product', + 'location_id': stock_location.id, + 'product_id': productA.id, + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.") + self.assertEqual(inventory.line_ids.theoretical_qty, -1, "Theoretical quantity should be -1.") + inventory.line_ids.product_qty = 0 # Put the quantity back to 0 + inventory.action_done() + + # The inventory adjustment should have created one + self.assertEqual(len(inventory.move_ids), 1) + quantity = inventory.move_ids.mapped('product_qty') + self.assertEqual(quantity, [1], "Moves created with wrong quantity.") + location_ids = inventory.move_ids.mapped('location_id').ids + self.assertEqual(set(location_ids), {location_loss.id}) + + # There should be no quant in the stock location + quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)]) + self.assertEqual(len(quants), 0) + + # There should be no quant in the inventory loss location + quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)]) + self.assertEqual(len(quant), 0) From b5eec3414b8a2fdad9f1a4018a6a0a578b6d5817 Mon Sep 17 00:00:00 2001 From: qdp-odoo Date: Thu, 15 Jun 2017 12:11:43 +0200 Subject: [PATCH 03/13] [FIX] l10n_fr_certification: don't copy hash strings and sequence numbers. copy() was created corrupted data since copied moves already had the parent's info --- addons/l10n_fr_certification/models/account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/l10n_fr_certification/models/account.py b/addons/l10n_fr_certification/models/account.py index 8d1a26d0ad6..ecb0f5aff8b 100644 --- a/addons/l10n_fr_certification/models/account.py +++ b/addons/l10n_fr_certification/models/account.py @@ -16,8 +16,8 @@ LINE_FIELDS = ['debit', 'credit', 'account_id', 'move_id', 'partner_id'] class AccountMove(models.Model): _inherit = "account.move" - l10n_fr_secure_sequence_number = fields.Integer(readonly=True) - l10n_fr_hash = fields.Char(readonly=True) + l10n_fr_secure_sequence_number = fields.Integer(readonly=True, copy=False) + l10n_fr_hash = fields.Char(readonly=True, copy=False) l10n_fr_string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True, store=False) def _get_new_hash(self, secure_seq_number): From 2bf6005e6a2ecf9c44d17f8b886e02c2670d84bf Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 15 Jun 2017 13:55:41 +0200 Subject: [PATCH 04/13] [FIX] point_of_sale: tax on line Complement of commit f0f45c8fa2e9069f25b202e57774d3f4d44b1d2a The result of the onchange is incorrectly interpreted by the web client, leading to taxes not saved. opw-746827 --- addons/point_of_sale/point_of_sale_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 83b4fa23bbd..c3e20ee40b1 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -40,7 +40,7 @@ - + @@ -53,7 +53,7 @@ - + From eb8d9190154a72d557674fcffb2a397bce5daed9 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 13 Jun 2017 17:17:45 +0200 Subject: [PATCH 05/13] [FIX] sql_db: port fix from psycopg/psycopg2#459 NUL characters must not be used in query parameters, as they will be ignored by libpq, being end-of-string characters. Preventing NULs avoids unexpected results from queries. It is only necessary with psycopg2 versions before 2.7, which includes the upstream fix. --- openerp/sql_db.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openerp/sql_db.py b/openerp/sql_db.py index a3af4be677a..22bed4eabbc 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -63,6 +63,19 @@ psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,) import tools + +from tools import parse_version as pv +if pv(psycopg2.__version__) < pv('2.7'): + from psycopg2._psycopg import QuotedString + def adapt_string(adapted): + """Python implementation of psycopg/psycopg2#459 from v2.7""" + if '\x00' in adapted: + raise ValueError("A string literal cannot contain NUL (0x00) characters.") + return QuotedString(adapted) + + psycopg2.extensions.register_adapter(str, adapt_string) + psycopg2.extensions.register_adapter(unicode, adapt_string) + from tools.func import frame_codeinfo from datetime import datetime as mdt from datetime import timedelta From d655824028dd144e449a9ab835f827f6fdfd3260 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 14 Jun 2017 16:29:55 +0200 Subject: [PATCH 06/13] [FIX] http: force protocol when missing in URL --- openerp/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openerp/http.py b/openerp/http.py index e3de9c95665..18b91b94e20 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -160,6 +160,8 @@ def redirect_with_hash(url, code=303): # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/ if request.httprequest.user_agent.browser in ('firefox',): return werkzeug.utils.redirect(url, code) + if urlparse.urlparse(url, scheme='http').scheme not in ('http', 'https'): + url = 'http://' + url url = url.replace("'", "%27").replace("<", "%3C") return "" % url From 090d7a51beb51b7b27a9e7e4f2f7846fca2d3c89 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 15 Jun 2017 16:17:35 +0200 Subject: [PATCH 07/13] [FIX] auth_oauth: correctly register token field --- addons/auth_oauth/res_users.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/auth_oauth/res_users.py b/addons/auth_oauth/res_users.py index 1aa86ba2d50..58198a24338 100644 --- a/addons/auth_oauth/res_users.py +++ b/addons/auth_oauth/res_users.py @@ -10,6 +10,9 @@ from openerp.addons.auth_signup.res_users import SignupError from openerp.osv import osv, fields from openerp import SUPERUSER_ID +from openerp.addons.base.res import res_users +res_users.USER_PRIVATE_FIELDS.append('oauth_access_token') + _logger = logging.getLogger(__name__) class res_users(osv.Model): From e3a52a9966d4d99cebf7dc95437ecadf87897f69 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 13 Jun 2017 17:19:59 +0200 Subject: [PATCH 08/13] [FIX] sql_db: port fix from psycopg/psycopg2#459 NUL characters must not be used in query parameters, as they will be ignored by libpq, being end-of-string characters. Preventing NULs avoids unexpected results from queries. It is only necessary with psycopg2 versions before 2.7, which includes the upstream fix. --- openerp/sql_db.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openerp/sql_db.py b/openerp/sql_db.py index fe0502a4fa9..be7cf59d744 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -46,6 +46,19 @@ psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,) import tools + +from tools import parse_version as pv +if pv(psycopg2.__version__) < pv('2.7'): + from psycopg2._psycopg import QuotedString + def adapt_string(adapted): + """Python implementation of psycopg/psycopg2#459 from v2.7""" + if '\x00' in adapted: + raise ValueError("A string literal cannot contain NUL (0x00) characters.") + return QuotedString(adapted) + + psycopg2.extensions.register_adapter(str, adapt_string) + psycopg2.extensions.register_adapter(unicode, adapt_string) + from tools.func import frame_codeinfo from datetime import datetime as mdt from datetime import timedelta From 692f47da6ecb0511fdb914a7b1eefff10ebe1edf Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 14 Jun 2017 16:36:22 +0200 Subject: [PATCH 09/13] [FIX] http: force protocol when missing in URL --- openerp/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openerp/http.py b/openerp/http.py index 84847f13590..c4c1f95fabf 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -160,6 +160,8 @@ def redirect_with_hash(url, code=303): # See extensive test page at http://greenbytes.de/tech/tc/httpredirects/ if request.httprequest.user_agent.browser in ('firefox',): return werkzeug.utils.redirect(url, code) + if urlparse.urlparse(url, scheme='http').scheme not in ('http', 'https'): + url = 'http://' + url url = url.replace("'", "%27").replace("<", "%3C") return "" % url From 0527564463fa9144bca906fcacf7bb4b4e68ba0a Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 15 Jun 2017 16:21:32 +0200 Subject: [PATCH 10/13] [FIX] auth_oauth: correctly register token field --- addons/auth_oauth/res_users.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/auth_oauth/res_users.py b/addons/auth_oauth/res_users.py index 1c9eb492b06..60fdd7319a1 100644 --- a/addons/auth_oauth/res_users.py +++ b/addons/auth_oauth/res_users.py @@ -10,6 +10,9 @@ from openerp.addons.auth_signup.res_users import SignupError from openerp.osv import osv, fields from openerp import SUPERUSER_ID +from openerp.addons.base.res import res_users +res_users.USER_PRIVATE_FIELDS.append('oauth_access_token') + _logger = logging.getLogger(__name__) class res_users(osv.Model): From e40a4baf41b3b833af37d0086a3251ae52d4ad1b Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 15 Jun 2017 16:34:47 +0200 Subject: [PATCH 11/13] [FIX] anonymization: correct pickle protocol and import --- addons/anonymization/anonymization.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/addons/anonymization/anonymization.py b/addons/anonymization/anonymization.py index c4ef22f8a80..e38a2c4f8c4 100644 --- a/addons/anonymization/anonymization.py +++ b/addons/anonymization/anonymization.py @@ -4,14 +4,11 @@ from lxml import etree import os import base64 -try: - import cPickle as pickle -except ImportError: - import pickle import random import datetime from openerp.release import version_info from openerp.osv import fields, osv +from openerp.tools import pickle from openerp.tools.translate import _ from openerp.tools.safe_eval import safe_eval as eval @@ -447,7 +444,7 @@ class ir_model_fields_anonymize_wizard(osv.osv_memory): # save pickle: fn = open(abs_filepath, 'w') - pickle.dump(data, fn, pickle.HIGHEST_PROTOCOL) + pickle.dump(data, fn, -1) # update the anonymization fields: values = { From 89eca9647636b1d801cf52aa98d83131b99d48fd Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 15 Jun 2017 16:36:05 +0200 Subject: [PATCH 12/13] [FIX] anonymization: correct pickle protocol and import --- addons/anonymization/anonymization.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/addons/anonymization/anonymization.py b/addons/anonymization/anonymization.py index ac7aa6e438e..b0889066fa4 100644 --- a/addons/anonymization/anonymization.py +++ b/addons/anonymization/anonymization.py @@ -23,13 +23,10 @@ from lxml import etree import os import base64 -try: - import cPickle as pickle -except ImportError: - import pickle import random import datetime from openerp.osv import fields, osv +from openerp.tools import pickle from openerp.tools.translate import _ from openerp.tools.safe_eval import safe_eval as eval @@ -461,7 +458,7 @@ class ir_model_fields_anonymize_wizard(osv.osv_memory): # save pickle: fn = open(abs_filepath, 'w') - pickle.dump(data, fn, pickle.HIGHEST_PROTOCOL) + pickle.dump(data, fn, -1) # update the anonymization fields: values = { From cb73aaf71be4ca5f6e61546fb944ac46b88e3302 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Mon, 13 Mar 2017 14:06:32 +0100 Subject: [PATCH 13/13] [FIX] stock: inventory adjustment and negative quants It is still possible to have negative and positive quants in the same location because of returns: if you send something to the customer that is not there and you return it, you will still be able to reserve the returned goods to send to another client. Before, if you would do an inventory adjustment, it would not take into account these returned quants and their negative counterpart, which made them difficult to get out of the system. This fix takes them into account by creating two movements for one inventory line: move the positive counterpart to the inventory location before getting back from this location the same quantity. This way, even if you have 0 as quantity on hand but you have those 2 quants, it will eliminate them. (if you are increasing the stock, part of the process might have done it automatically already). Test cases contributed by @jir-odoo Cherry-pick of be9dce625c55e1b2d6039573c7035d61f762edc8 --- addons/stock/models/stock_inventory.py | 58 +++++++---- addons/stock/tests/test_warehouse.py | 128 +++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 16 deletions(-) diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 5322e13e05a..62f5861afba 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -325,31 +325,57 @@ class InventoryLine(models.Model): ('owner_id', '=', self.partner_id.id), ('package_id', '=', self.package_id.id)]) + def _get_move_values(self, qty, location_id, location_dest_id): + self.ensure_one() + return { + 'name': _('INV:') + (self.inventory_id.name or ''), + 'product_id': self.product_id.id, + 'product_uom': self.product_uom_id.id, + 'product_uom_qty': qty, + 'date': self.inventory_id.date, + 'company_id': self.inventory_id.company_id.id, + 'inventory_id': self.inventory_id.id, + 'state': 'confirmed', + 'restrict_lot_id': self.prod_lot_id.id, + 'restrict_partner_id': self.partner_id.id, + 'location_id': location_id, + 'location_dest_id': location_dest_id, + } + + def _fixup_negative_quants(self): + """ This will handle the irreconciable quants created by a force availability followed by a + return. When generating the moves of an inventory line, we look for quants of this line's + product created to compensate a force availability. If there are some and if the quant + which it is propagated from is still in the same location, we move it to the inventory + adjustment location before getting it back. Getting the quantity from the inventory + location will allow the negative quant to be compensated. + """ + self.ensure_one() + for quant in self._get_quants().filtered(lambda q: q.propagated_from_id.location_id.id == self.location_id.id): + # send the quantity to the inventory adjustment location + move_out_vals = self._get_move_values(quant.qty, self.location_id.id, self.product_id.property_stock_inventory.id) + move_out = self.env['stock.move'].create(move_out_vals) + self.env['stock.quant'].quants_reserve([(quant, quant.qty)], move_out) + move_out.action_done() + + # get back the quantity from the inventory adjustment location + move_in_vals = self._get_move_values(quant.qty, self.product_id.property_stock_inventory.id, self.location_id.id) + move_in = self.env['stock.move'].create(move_in_vals) + move_in.action_done() + def _generate_moves(self): moves = self.env['stock.move'] Quant = self.env['stock.quant'] for line in self: + line._fixup_negative_quants() + if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0: continue diff = line.theoretical_qty - line.product_qty - vals = { - 'name': _('INV:') + (line.inventory_id.name or ''), - 'product_id': line.product_id.id, - 'product_uom': line.product_uom_id.id, - 'date': line.inventory_id.date, - 'company_id': line.inventory_id.company_id.id, - 'inventory_id': line.inventory_id.id, - 'state': 'confirmed', - 'restrict_lot_id': line.prod_lot_id.id, - 'restrict_partner_id': line.partner_id.id} if diff < 0: # found more than expected - vals['location_id'] = line.product_id.property_stock_inventory.id - vals['location_dest_id'] = line.location_id.id - vals['product_uom_qty'] = abs(diff) + vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id) else: - vals['location_id'] = line.location_id.id - vals['location_dest_id'] = line.product_id.property_stock_inventory.id - vals['product_uom_qty'] = diff + vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id) move = moves.create(vals) if diff > 0: diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index 048392f5085..94cca6b034e 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -103,6 +103,134 @@ class TestInventory(TestStockCommon): self.assertEqual(product.qty_available, 8.0) self.assertEqual(product.with_context(location=self.env.ref('stock.stock_location_customers').id).qty_available, 7.0) + def test_inventory_adjustment_and_negative_quants_1(self): + """Make sure negative quants from returns get wiped out with an inventory adjustment""" + productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'}) + stock_location = self.env.ref('stock.stock_location_stock') + customer_location = self.env.ref('stock.stock_location_customers') + location_loss = self.env.ref('stock.location_inventory') + + # Create a picking out and force availability + picking_out = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + self.env['stock.move'].create({ + 'name': productA.name, + 'product_id': productA.id, + 'product_uom_qty': 1, + 'product_uom': productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + picking_out.action_confirm() + picking_out.force_assign() + picking_out.do_transfer() + + # Create return picking for all goods + default_data = self.env['stock.return.picking']\ + .with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\ + .default_get([ + 'move_dest_exists', + 'original_location_id', + 'product_return_moves', + 'parent_location_id', + 'location_id', + ]) + return_wiz = self.env['stock.return.picking']\ + .with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\ + .create(default_data) + res = return_wiz.create_returns() + return_pick = self.env['stock.picking'].browse(res['res_id']) + return_pick.action_assign() + return_pick.do_transfer() + + # Make an inventory adjustment to set the quantity to 0 + inventory = self.env['stock.inventory'].create({ + 'name': 'Starting for product_1', + 'filter': 'product', + 'location_id': stock_location.id, + 'product_id': productA.id, + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.") + self.assertEqual(inventory.line_ids.theoretical_qty, 0, "Theoretical quantity should be zero.") + inventory.action_done() + + # The inventory adjustment should have created two moves + self.assertEqual(len(inventory.move_ids), 2) + quantity = inventory.move_ids.mapped('product_qty') + self.assertEqual(quantity, [1, 1], "Moves created with wrong quantity.") + location_ids = inventory.move_ids.mapped('location_id').ids + self.assertEqual(set(location_ids), {stock_location.id, location_loss.id}) + + # There should be no quant in the stock location + quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)]) + self.assertEqual(len(quants), 0) + + # There should be one quant in the inventory loss location + quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)]) + self.assertEqual(len(quant), 1) + self.assertEqual(quant.qty, 1) + + def test_inventory_adjustment_and_negative_quants_2(self): + """Make sure negative quants get wiped out with an inventory adjustment""" + productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'}) + stock_location = self.env.ref('stock.stock_location_stock') + customer_location = self.env.ref('stock.stock_location_customers') + location_loss = self.env.ref('stock.location_inventory') + + # Create a picking out and force availability + picking_out = self.env['stock.picking'].create({ + 'partner_id': self.env.ref('base.res_partner_2').id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + self.env['stock.move'].create({ + 'name': productA.name, + 'product_id': productA.id, + 'product_uom_qty': 1, + 'product_uom': productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + }) + picking_out.action_confirm() + picking_out.force_assign() + picking_out.do_transfer() + + # Make an inventory adjustment to set the quantity to 0 + inventory = self.env['stock.inventory'].create({ + 'name': 'Starting for product_1', + 'filter': 'product', + 'location_id': stock_location.id, + 'product_id': productA.id, + }) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.") + self.assertEqual(inventory.line_ids.theoretical_qty, -1, "Theoretical quantity should be -1.") + inventory.line_ids.product_qty = 0 # Put the quantity back to 0 + inventory.action_done() + + # The inventory adjustment should have created one + self.assertEqual(len(inventory.move_ids), 1) + quantity = inventory.move_ids.mapped('product_qty') + self.assertEqual(quantity, [1], "Moves created with wrong quantity.") + location_ids = inventory.move_ids.mapped('location_id').ids + self.assertEqual(set(location_ids), {location_loss.id}) + + # There should be no quant in the stock location + quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)]) + self.assertEqual(len(quants), 0) + + # There should be no quant in the inventory loss location + quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)]) + self.assertEqual(len(quant), 0) + class TestResupply(TestStockCommon): def setUp(self):