odoo18/addons/stock/tests/test_report_stock_quantity.py

327 lines
15 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from odoo import fields, tests
from odoo.fields import Command
from odoo.tests import Form
from freezegun import freeze_time
class TestReportStockQuantity(tests.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# freeze time to avoid test errors due to the class being initialized before 00:00:00 and the test run after
cls.fake_today = fields.Date.today()
cls.startClassPatcher(freeze_time(cls.fake_today))
cls.product1 = cls.env['product.product'].create({
'name': 'Mellohi',
'default_code': 'C418',
'is_storable': True,
'categ_id': cls.env.ref('product.product_category_all').id,
'tracking': 'lot',
'barcode': 'scan_me'
})
cls.wh = cls.env['stock.warehouse'].create({
'name': 'Base Warehouse',
'code': 'TESTWH'
})
cls.categ_unit = cls.env.ref('uom.product_uom_categ_unit')
cls.uom_unit = cls.env['uom.uom'].search([('category_id', '=', cls.categ_unit.id), ('uom_type', '=', 'reference')], limit=1)
cls.customer_location = cls.env.ref('stock.stock_location_customers')
cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
# replenish
cls.move1 = cls.env['stock.move'].create({
'name': 'test_in_1',
'location_id': cls.supplier_location.id,
'location_dest_id': cls.wh.lot_stock_id.id,
'product_id': cls.product1.id,
'product_uom': cls.uom_unit.id,
'product_uom_qty': 100.0,
'quantity': 100.0,
'state': 'done',
'date': fields.Datetime.now(),
})
# ship
cls.move2 = cls.env['stock.move'].create({
'name': 'test_out_1',
'location_id': cls.wh.lot_stock_id.id,
'location_dest_id': cls.customer_location.id,
'product_id': cls.product1.id,
'product_uom': cls.uom_unit.id,
'product_uom_qty': 120.0,
'state': 'partially_available',
'date': fields.Datetime.add(fields.Datetime.now(), days=3),
'date_deadline': fields.Datetime.add(fields.Datetime.now(), days=3),
})
def test_report_stock_quantity(self):
from_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=-1))
to_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=4))
report = self.env['report.stock.quantity']._read_group(
[('date', '>=', from_date), ('date', '<=', to_date), ('product_id', '=', self.product1.id)],
['date:day', 'product_id', 'state'],
['product_qty:sum'])
forecast_report = [qty for __, __, state, qty in report if state == 'forecast']
self.assertEqual(forecast_report, [0, 100, 100, 100, -20, -20])
def test_report_stock_quantity_stansit(self):
wh2 = self.env['stock.warehouse'].create({'name': 'WH2', 'code': 'WH2'})
transit_loc = self.wh.company_id.internal_transit_location_id
self.move_transit_out = self.env['stock.move'].create({
'name': 'test_transit_out_1',
'location_id': self.wh.lot_stock_id.id,
'location_dest_id': transit_loc.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 25.0,
'state': 'assigned',
'date': fields.Datetime.now(),
'date_deadline': fields.Datetime.now(),
})
self.move_transit_in = self.env['stock.move'].create({
'name': 'test_transit_in_1',
'location_id': transit_loc.id,
'location_dest_id': wh2.lot_stock_id.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 25.0,
'state': 'waiting',
'date': fields.Datetime.now(),
'date_deadline': fields.Datetime.now(),
})
self.env.flush_all()
report = self.env['report.stock.quantity']._read_group(
[('date', '>=', fields.Date.today()), ('date', '<=', fields.Date.today()), ('product_id', '=', self.product1.id)],
['date:day', 'product_id', 'state'],
['product_qty:sum'])
forecast_in_report = [qty for __, __, state, qty in report if state == 'in']
self.assertEqual(forecast_in_report, [25])
forecast_out_report = [qty for __, __, state, qty in report if state == 'out']
self.assertEqual(forecast_out_report, [-25])
def test_report_stock_quantity_with_product_qty_filter(self):
from_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=-1))
to_date = fields.Date.to_string(fields.Date.add(fields.Date.today(), days=4))
report = self.env['report.stock.quantity']._read_group(
[('product_qty', '<', 0), ('date', '>=', from_date), ('date', '<=', to_date), ('product_id', '=', self.product1.id)],
['date:day', 'product_id', 'state'],
['product_qty:sum'])
forecast_report = [qty for __, __, state, qty in report if state == 'forecast']
self.assertEqual(forecast_report, [-20, -20])
def test_replenishment_report_1(self):
self.product_replenished = self.env['product.product'].create({
'name': 'Security razor',
'is_storable': True,
'categ_id': self.env.ref('product.product_category_all').id,
})
# get auto-created pull rule from when warehouse is created
self.wh.reception_route_id.rule_ids.unlink()
self.env['stock.rule'].create({
'name': 'Rule Supplier',
'route_id': self.wh.reception_route_id.id,
'location_dest_id': self.wh.lot_stock_id.id,
'location_src_id': self.env.ref('stock.stock_location_suppliers').id,
'action': 'pull',
'delay': 1.0,
'procure_method': 'make_to_stock',
'picking_type_id': self.wh.in_type_id.id,
})
delivery_picking = self.env['stock.picking'].create({
'location_id': self.wh.lot_stock_id.id,
'location_dest_id': self.ref('stock.stock_location_customers'),
'picking_type_id': self.ref('stock.picking_type_out'),
})
self.env['stock.move'].create({
'name': 'Delivery',
'product_id': self.product_replenished.id,
'product_uom_qty': 500.0,
'product_uom': self.uom_unit.id,
'location_id': self.wh.lot_stock_id.id,
'location_dest_id': self.ref('stock.stock_location_customers'),
'picking_id': delivery_picking.id,
})
delivery_picking.action_confirm()
# Trigger the manual orderpoint creation for missing product
self.env.flush_all()
self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
orderpoint = self.env['stock.warehouse.orderpoint'].search([
('product_id', '=', self.product_replenished.id)
])
self.assertTrue(orderpoint)
self.assertEqual(orderpoint.location_id, self.wh.lot_stock_id)
self.assertEqual(orderpoint.qty_to_order, 500.0)
orderpoint.action_replenish()
self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
move = self.env['stock.move'].search([
('product_id', '=', self.product_replenished.id),
('location_dest_id', '=', self.wh.lot_stock_id.id)
])
# Simulate a supplier delay
move.date = fields.datetime.now() + timedelta(days=1)
orderpoint = self.env['stock.warehouse.orderpoint'].search([
('product_id', '=', self.product_replenished.id)
])
self.assertFalse(orderpoint)
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = self.product_replenished
orderpoint_form.location_id = self.wh.lot_stock_id
orderpoint = orderpoint_form.save()
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',
'is_storable': True,
})
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 = 1
move01.picked = True
move01._action_done()
self.env.flush_all()
data = self.env['report.stock.quantity']._read_group(
[('state', '=', 'forecast'), ('product_id', '=', product.id), ('date', '>=', two_days_ago), ('date', '<=', in_two_days)],
['date:day', 'warehouse_id'],
['product_qty:sum'],
)
for (date_day, warehouse, qty_rd), 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(qty_rd, qty, f"Incorrect qty for Date '{date_day}' Warehouse '{warehouse.display_name}'")
def test_past_date_quantity_with_multistep_delivery(self):
"""
Verify that available quantities are correctly computed at different past dates
when using multi-step reciept/delivery.
"""
def get_inv_qty_at_date(product_id, inv_datetime):
inventory_at_date_wizard = self.env['stock.quantity.history'].create({'inventory_datetime': inv_datetime})
r = inventory_at_date_wizard.open_at_date()
return next((product['qty_available'], product['virtual_available']) for product in self.env[r['res_model']].with_context(r['context']).search_read(
domain=(r['domain'] + [('id', '=', product_id)]),
fields=['qty_available', 'virtual_available']
))
# We add a second warehouse and put the resuplying flow in push mechanic to test receipt in 2 steps with an external transfer
warehouse, warehouse_2 = self.wh, self.env['stock.warehouse'].create({
'name': 'Resupplier warehouse',
'code': 'WH02',
})
transit_loc = self.wh.company_id.internal_transit_location_id
warehouse.write({
'resupply_wh_ids': [Command.set(warehouse_2.ids)],
'delivery_steps': 'pick_ship',
})
warehouse.resupply_route_ids.rule_ids.filtered(lambda r: r.location_src_id == transit_loc).action = 'push'
product = self.env['product.product'].create({'name': 'Test', 'is_storable': True})
today = fields.Date.today()
with freeze_time(today - timedelta(days=8)):
move_transit = self.env['stock.move'].create({
'name': 'test transit',
'warehouse_id': warehouse.id,
'picking_type_id': warehouse.in_type_id.id,
'location_id': self.supplier_location.id,
'location_dest_id': transit_loc.id,
'location_final_id': warehouse.lot_stock_id.id,
'route_ids': [Command.set(warehouse.resupply_route_ids.ids)],
'product_id': product.id,
'product_uom_qty': 150.0,
})
move_transit._action_confirm()
move_transit.write({'quantity': 150.0, 'picked': True})
move_transit._action_done()
self.assertRecordValues(product.with_context(warehouse_id=warehouse.id), [{'qty_available': 0.0, 'virtual_available': 150.0}])
move_transit._action_done()
self.assertRecordValues(product.with_context(warehouse_id=warehouse.id), [{'qty_available': 0.0, 'virtual_available': 150.0}])
with freeze_time(today - timedelta(days=6)):
move_in = move_transit.move_dest_ids
move_in._action_confirm()
move_in.write({'quantity': 100.0, 'picked': True})
self.assertRecordValues(product.with_context(warehouse_id=warehouse.id), [{'qty_available': 0.0, 'virtual_available': 150.0}])
move_in._action_done()
self.assertRecordValues(product.with_context(warehouse_id=warehouse.id), [{'qty_available': 100.0, 'virtual_available': 150.0}])
with freeze_time(today - timedelta(days=4)):
move_pick = self.env['stock.move'].create({
'name': 'pick',
'picking_type_id': warehouse.pick_type_id.id,
'location_id': warehouse.lot_stock_id.id,
'location_dest_id': warehouse.wh_output_stock_loc_id.id,
'location_final_id': self.customer_location.id,
'product_id': product.id,
'product_uom_qty': 60.0,
})
move_pick._action_confirm()
self.assertRecordValues(product.with_context(warehouse_id=warehouse.id), [{'qty_available': 100.0, 'virtual_available': 90.0}])
move_pick.write({'quantity': 60.0, 'picked': True})
move_pick._action_done()
self.assertRecordValues(product.with_context(warehouse_id=warehouse.id), [{'qty_available': 100.0, 'virtual_available': 90.0}])
with freeze_time(today - timedelta(days=2)):
move_out = move_pick.move_dest_ids
move_out.write({'quantity': 25.0, 'picked': True})
self.assertRecordValues(product.with_context(warehouse_id=warehouse.id), [{'qty_available': 100.0, 'virtual_available': 90.0}])
move_out._action_done()
self.assertRecordValues(product.with_context(warehouse_id=warehouse.id), [{'qty_available': 75.0, 'virtual_available': 90.0}])
for date, expected_qties in (
(move_transit.date - timedelta(days=1), (0.0, 0.0)),
(move_in.date - timedelta(days=1), (0.0, 50.0)), # The backorder of move_in contributes in the incoming qty
(move_pick.date - timedelta(days=1), (100.0, 150.0)),
(move_out.date - timedelta(days=1), (100.0, 115.0)), # The backorder of move_out contributes in the outgoing qty
(today - timedelta(days=1), (75.0, 90.0)),
):
qty = get_inv_qty_at_date(product.id, date)
self.assertEqual(qty, expected_qties)