diff --git a/addons/stock/i18n/stock.pot b/addons/stock/i18n/stock.pot index 154b7d5b3ea..79f7f1baf83 100644 --- a/addons/stock/i18n/stock.pot +++ b/addons/stock/i18n/stock.pot @@ -8899,15 +8899,6 @@ msgstr "" msgid "You need to supply a Lot/Serial number for products %s." msgstr "" -#. module: stock -#: code:addons/stock/models/stock_picking.py:0 -#, python-format -msgid "" -"You should not use a planned internal transfer to move some products between" -" two warehouses. Instead, use an immediate internal transfer or the resupply" -" route." -msgstr "" - #. module: stock #: code:addons/stock/models/stock_warehouse.py:0 #, python-format diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 6c4524e59a0..5d0b2f29d65 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -726,15 +726,6 @@ class Picking(models.Model): @api.onchange('location_id', 'location_dest_id') def _onchange_locations(self): - from_wh = self.location_id.warehouse_id - to_wh = self.location_dest_id.warehouse_id - is_immediate = self.immediate_transfer if self._origin else self._context.get('default_immediate_transfer') - if self.picking_type_id.code == 'internal' and not is_immediate and from_wh and to_wh and from_wh != to_wh: - return {'warning': { - 'title': _("Warning"), - 'message': _("You should not use a planned internal transfer to move some products between two warehouses. " - "Instead, use an immediate internal transfer or the resupply route.") - }} (self.move_ids | self.move_ids_without_package).update({ "location_id": self.location_id, "location_dest_id": self.location_dest_id diff --git a/addons/stock/report/report_stock_quantity.py b/addons/stock/report/report_stock_quantity.py index bd7758c4ef6..6592303f5cf 100644 --- a/addons/stock/report/report_stock_quantity.py +++ b/addons/stock/report/report_stock_quantity.py @@ -23,9 +23,55 @@ class ReportStockQuantity(models.Model): warehouse_id = fields.Many2one('stock.warehouse', readonly=True) def init(self): + """ + Because we can transfer a product from a warehouse to another one thanks to a stock move, we need to + generate some fake stock moves before processing all of them. That way, in case of an interwarehouse + transfer, we will have an outgoing stock move for the source warehouse and an incoming stock move + for the destination one. To do so, we select all relevant SM (incoming, outgoing and interwarehouse), + then we duplicate all these SM and edit the values: + - product_qty is kept if the SM is not the duplicated one or if the SM is an interwarehouse one + otherwise, we set the value to 0 (this allows us to filter it out during the SM processing) + - the source warehouse is kept if the SM is not the duplicated one + - the dest warehouse is kept if the SM is not the duplicated one and is not an interwarehouse + OR the SM is the duplicated one and is an interwarehouse + """ tools.drop_view_if_exists(self._cr, 'report_stock_quantity') query = """ CREATE or REPLACE VIEW report_stock_quantity AS ( +WITH + existing_sm (id, product_id, tmpl_id, product_qty, date, state, company_id, whs_id, whd_id) AS ( + SELECT m.id, m.product_id, pt.id, m.product_qty, m.date, m.state, m.company_id, whs.id, whd.id + FROM stock_move m + LEFT JOIN stock_location ls on (ls.id=m.location_id) + LEFT JOIN stock_location ld on (ld.id=m.location_dest_id) + LEFT JOIN stock_warehouse whs ON ls.parent_path like concat('%/', whs.view_location_id, '/%') + LEFT JOIN stock_warehouse whd ON ld.parent_path like concat('%/', whd.view_location_id, '/%') + LEFT JOIN product_product pp on pp.id=m.product_id + LEFT JOIN product_template pt on pt.id=pp.product_tmpl_id + WHERE pt.type = 'product' AND + (whs.id IS NOT NULL OR whd.id IS NOT NULL) AND + (whs.id IS NULL OR whd.id IS NULL OR whs.id != whd.id) AND + m.product_qty != 0 AND + m.state NOT IN ('draft', 'cancel') AND + (m.state != 'done' or m.date >= ((now() at time zone 'utc')::date - interval '3month')) + ), + all_sm (id, product_id, tmpl_id, product_qty, date, state, company_id, whs_id, whd_id) AS ( + SELECT sm.id, sm.product_id, sm.tmpl_id, + CASE + WHEN is_duplicated = 0 THEN sm.product_qty + WHEN sm.whs_id IS NOT NULL AND sm.whd_id IS NOT NULL AND sm.whs_id != sm.whd_id THEN sm.product_qty + ELSE 0 + END, + sm.date, sm.state, sm.company_id, + CASE WHEN is_duplicated = 0 THEN sm.whs_id END, + CASE + WHEN is_duplicated = 0 AND NOT (sm.whs_id IS NOT NULL AND sm.whd_id IS NOT NULL AND sm.whs_id != sm.whd_id) THEN sm.whd_id + WHEN is_duplicated = 1 AND (sm.whs_id IS NOT NULL AND sm.whd_id IS NOT NULL AND sm.whs_id != sm.whd_id) THEN sm.whd_id + END + FROM + GENERATE_SERIES(0, 1, 1) is_duplicated, + existing_sm sm + ) SELECT MIN(id) as id, product_id, @@ -38,35 +84,26 @@ SELECT FROM (SELECT m.id, m.product_id, - pt.id as product_tmpl_id, + m.tmpl_id as product_tmpl_id, CASE - WHEN whs.id IS NOT NULL AND whd.id IS NULL THEN 'out' - WHEN whd.id IS NOT NULL AND whs.id IS NULL THEN 'in' + WHEN m.whs_id IS NOT NULL AND m.whd_id IS NULL THEN 'out' + WHEN m.whd_id IS NOT NULL AND m.whs_id IS NULL THEN 'in' END AS state, m.date::date AS date, CASE - WHEN whs.id IS NOT NULL AND whd.id IS NULL THEN -m.product_qty - WHEN whd.id IS NOT NULL AND whs.id IS NULL THEN m.product_qty + WHEN m.whs_id IS NOT NULL AND m.whd_id IS NULL THEN -m.product_qty + WHEN m.whd_id IS NOT NULL AND m.whs_id IS NULL THEN m.product_qty END AS product_qty, m.company_id, CASE - WHEN whs.id IS NOT NULL AND whd.id IS NULL THEN whs.id - WHEN whd.id IS NOT NULL AND whs.id IS NULL THEN whd.id + WHEN m.whs_id IS NOT NULL AND m.whd_id IS NULL THEN m.whs_id + WHEN m.whd_id IS NOT NULL AND m.whs_id IS NULL THEN m.whd_id END AS warehouse_id FROM - stock_move m - LEFT JOIN stock_location ls on (ls.id=m.location_id) - LEFT JOIN stock_location ld on (ld.id=m.location_dest_id) - LEFT JOIN stock_warehouse whs ON ls.parent_path like concat('%/', whs.view_location_id, '/%') - LEFT JOIN stock_warehouse whd ON ld.parent_path like concat('%/', whd.view_location_id, '/%') - LEFT JOIN product_product pp on pp.id=m.product_id - LEFT JOIN product_template pt on pt.id=pp.product_tmpl_id + all_sm m WHERE - pt.type = 'product' AND m.product_qty != 0 AND - (whs.id IS NOT NULL OR whd.id IS NOT NULL) AND - (whs.id IS NULL OR whd.id IS NULL OR whs.id != whd.id) AND - m.state NOT IN ('cancel', 'draft', 'done') + m.state != 'done' UNION ALL SELECT -q.id as id, @@ -91,7 +128,7 @@ FROM (SELECT SELECT m.id, m.product_id, - pt.id as product_tmpl_id, + m.tmpl_id as product_tmpl_id, 'forecast' as state, GENERATE_SERIES( CASE @@ -103,30 +140,20 @@ FROM (SELECT ELSE m.date::date - interval '1 day' END, '1 day'::interval)::date date, CASE - WHEN whs.id IS NOT NULL AND whd.id IS NULL AND m.state = 'done' THEN m.product_qty - WHEN whd.id IS NOT NULL AND whs.id IS NULL AND m.state = 'done' THEN -m.product_qty - WHEN whs.id IS NOT NULL AND whd.id IS NULL THEN -m.product_qty - WHEN whd.id IS NOT NULL AND whs.id IS NULL THEN m.product_qty + WHEN m.whs_id IS NOT NULL AND m.whd_id IS NULL AND m.state = 'done' THEN m.product_qty + WHEN m.whd_id IS NOT NULL AND m.whs_id IS NULL AND m.state = 'done' THEN -m.product_qty + WHEN m.whs_id IS NOT NULL AND m.whd_id IS NULL THEN -m.product_qty + WHEN m.whd_id IS NOT NULL AND m.whs_id IS NULL THEN m.product_qty END AS product_qty, m.company_id, CASE - WHEN whs.id IS NOT NULL AND whd.id IS NULL THEN whs.id - WHEN whd.id IS NOT NULL AND whs.id IS NULL THEN whd.id + WHEN m.whs_id IS NOT NULL AND m.whd_id IS NULL THEN m.whs_id + WHEN m.whd_id IS NOT NULL AND m.whs_id IS NULL THEN m.whd_id END AS warehouse_id FROM - stock_move m - LEFT JOIN stock_location ls on (ls.id=m.location_id) - LEFT JOIN stock_location ld on (ld.id=m.location_dest_id) - LEFT JOIN stock_warehouse whs ON ls.parent_path like concat('%/', whs.view_location_id, '/%') - LEFT JOIN stock_warehouse whd ON ld.parent_path like concat('%/', whd.view_location_id, '/%') - LEFT JOIN product_product pp on pp.id=m.product_id - LEFT JOIN product_template pt on pt.id=pp.product_tmpl_id + all_sm m WHERE - pt.type = 'product' AND - m.product_qty != 0 AND - (whs.id IS NOT NULL OR whd.id IS NOT NULL) AND - (whs.id IS NULL or whd.id IS NULL OR whs.id != whd.id) AND - m.state NOT IN ('cancel', 'draft')) as forecast_qty + m.product_qty != 0) AS forecast_qty GROUP BY product_id, product_tmpl_id, state, date, company_id, warehouse_id ); """ diff --git a/addons/stock/tests/test_report_stock_quantity.py b/addons/stock/tests/test_report_stock_quantity.py index b3d78e734bf..d37b241487c 100644 --- a/addons/stock/tests/test_report_stock_quantity.py +++ b/addons/stock/tests/test_report_stock_quantity.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import timedelta +from datetime import datetime, timedelta from odoo import fields, tests from odoo.tests.common import Form @@ -184,3 +184,61 @@ class TestReportStockQuantity(tests.TransactionCase): self.assertEqual(orderpoint.qty_to_order, 0.0) self.env['stock.warehouse.orderpoint'].action_open_orderpoints() self.assertEqual(orderpoint.qty_to_order, 0.0) + + def test_inter_warehouse_transfer(self): + """ + Ensure that the report correctly processes the inter-warehouses SM + """ + product = self.env['product.product'].create({ + 'name': 'SuperProduct', + 'type': 'product', + }) + + today = datetime.now() + two_days_ago = today - timedelta(days=2) + in_two_days = today + timedelta(days=2) + + wh01, wh02 = self.env['stock.warehouse'].create([{ + 'name': 'Warehouse 01', + 'code': 'WH01', + }, { + 'name': 'Warehouse 02', + 'code': 'WH02', + }]) + + self.env['stock.quant']._update_available_quantity(product, wh01.lot_stock_id, 3, in_date=two_days_ago) + + # Let's have 2 inter-warehouses stock moves (one for today and one for two days from now) + move01, move02 = self.env['stock.move'].create([{ + 'name': 'Inter WH Move', + 'location_id': wh01.lot_stock_id.id, + 'location_dest_id': wh02.lot_stock_id.id, + 'product_id': product.id, + 'product_uom': product.uom_id.id, + 'product_uom_qty': 1, + 'date': date, + } for date in (today, in_two_days)]) + + (move01 + move02)._action_confirm() + move01.quantity_done = 1 + move01._action_done() + + self.env['stock.move'].flush() + + data = self.env['report.stock.quantity'].read_group( + [('state', '=', 'forecast'), ('product_id', '=', product.id), ('date', '>=', two_days_ago), ('date', '<=', in_two_days)], + ['product_qty', 'date', 'warehouse_id'], + ['date:day', 'warehouse_id'], + orderby='date, warehouse_id', + lazy=False, + ) + + for row, qty in zip(data, [ + # wh01_qty, wh02_qty + 3.0, 0.0, # two days ago + 3.0, 0.0, + 2.0, 1.0, # today + 2.0, 1.0, + 1.0, 2.0, # in two days + ]): + self.assertEqual(row['product_qty'], qty, "Incorrect qty for Date '%s' Warehouse '%s'" % (row['date:day'], row['warehouse_id'][1]))