237 lines
9.9 KiB
Python
237 lines
9.9 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.tests.common import Form
|
||
|
|
||
|
|
||
|
class TestReportStockQuantity(tests.TransactionCase):
|
||
|
|
||
|
@classmethod
|
||
|
def setUpClass(cls):
|
||
|
super().setUpClass()
|
||
|
cls.product1 = cls.env['product.product'].create({
|
||
|
'name': 'Mellohi',
|
||
|
'default_code': 'C418',
|
||
|
'type': 'product',
|
||
|
'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',
|
||
|
'type': 'product',
|
||
|
'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',
|
||
|
'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 = 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}'")
|