1305 lines
60 KiB
Python
1305 lines
60 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from datetime import datetime as dt, time
|
||
|
from datetime import timedelta as td
|
||
|
from json import loads
|
||
|
|
||
|
from odoo import SUPERUSER_ID, Command
|
||
|
from odoo.fields import Date
|
||
|
from odoo.tests import Form, tagged, freeze_time
|
||
|
from odoo.tests.common import TransactionCase
|
||
|
from odoo.tools import format_date
|
||
|
from odoo.tools.date_utils import add
|
||
|
from odoo.exceptions import UserError, ValidationError
|
||
|
|
||
|
|
||
|
@tagged('post_install', '-at_install')
|
||
|
@freeze_time("2021-01-14 09:12:15")
|
||
|
class TestReorderingRule(TransactionCase):
|
||
|
@classmethod
|
||
|
def setUpClass(cls):
|
||
|
super(TestReorderingRule, cls).setUpClass()
|
||
|
cls.partner = cls.env['res.partner'].create({
|
||
|
'name': 'Smith'
|
||
|
})
|
||
|
|
||
|
# create product and set the vendor
|
||
|
product_form = Form(cls.env['product.product'])
|
||
|
product_form.name = 'Product A'
|
||
|
product_form.is_storable = True
|
||
|
product_form.description = 'Internal Notes'
|
||
|
with product_form.seller_ids.new() as seller:
|
||
|
seller.partner_id = cls.partner
|
||
|
product_form.route_ids.add(cls.env.ref('purchase_stock.route_warehouse0_buy'))
|
||
|
cls.product_01 = product_form.save()
|
||
|
|
||
|
def test_reordering_rule_1(self):
|
||
|
"""
|
||
|
- Receive products in 2 steps
|
||
|
- The product has a reordering rule
|
||
|
- Manually create and confirm a PO => the forecast should be updated
|
||
|
- Cancel the PO => the forecast should be updated
|
||
|
- Create a picking that automatically generates another PO
|
||
|
- On the po generated, the source document should be the name of the reordering rule
|
||
|
- Increase the quantity on the RFQ, the extra quantity should follow the push rules
|
||
|
- Increase the quantity on the PO, the extra quantity should follow the push rules
|
||
|
- There should be one move supplier -> input and two moves input -> stock
|
||
|
"""
|
||
|
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
warehouse_1.write({'reception_steps': 'two_steps'})
|
||
|
warehouse_2 = self.env['stock.warehouse'].create({'name': 'WH 2', 'code': 'WH2', 'company_id': self.env.company.id, 'partner_id': self.env.company.partner_id.id, 'reception_steps': 'one_step'})
|
||
|
|
||
|
# Create and set specific buyer for partner
|
||
|
buyer_id = self.env['res.users'].create({
|
||
|
'login': 'buyer1',
|
||
|
'name': 'Buyer1',
|
||
|
'email': 'buyer1@example.com',
|
||
|
})
|
||
|
self.partner.buyer_id = buyer_id.id
|
||
|
|
||
|
# create reordering rule
|
||
|
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
|
||
|
orderpoint_form.warehouse_id = warehouse_1
|
||
|
orderpoint_form.location_id = warehouse_1.lot_stock_id
|
||
|
orderpoint_form.product_id = self.product_01
|
||
|
orderpoint_form.product_min_qty = 0.000
|
||
|
orderpoint_form.product_max_qty = 0.000
|
||
|
order_point = orderpoint_form.save()
|
||
|
|
||
|
# Manually create a PO, and check orderpoint forecast
|
||
|
manual_po = self.env['purchase.order'].create({
|
||
|
'name': 'Manual PO',
|
||
|
'partner_id': self.partner.id,
|
||
|
'order_line': [Command.create({
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_qty': 10,
|
||
|
})],
|
||
|
})
|
||
|
|
||
|
manual_po.button_confirm()
|
||
|
self.assertEqual(order_point.qty_forecast, 10)
|
||
|
|
||
|
manual_po.button_cancel()
|
||
|
self.assertEqual(order_point.qty_forecast, 0)
|
||
|
|
||
|
# Create Delivery Order of 10 product
|
||
|
picking_form = Form(self.env['stock.picking'])
|
||
|
picking_form.partner_id = self.partner
|
||
|
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = self.product_01
|
||
|
move.product_uom_qty = 10.0
|
||
|
customer_picking = picking_form.save()
|
||
|
customer_picking.action_confirm()
|
||
|
# Run scheduler
|
||
|
self.env['procurement.group'].run_scheduler()
|
||
|
|
||
|
# Check purchase order created or not
|
||
|
purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.partner.id), ('state', '!=', 'cancel')])
|
||
|
self.assertTrue(purchase_order, 'No purchase order created.')
|
||
|
# Check the picking type on the purchase order
|
||
|
purchase_order.picking_type_id = warehouse_2.in_type_id
|
||
|
with self.assertRaises(UserError):
|
||
|
purchase_order.button_confirm()
|
||
|
purchase_order.picking_type_id = warehouse_1.in_type_id
|
||
|
|
||
|
# On the po generated, the source document should be the name of the reordering rule
|
||
|
self.assertEqual(order_point.name, purchase_order.origin, 'Source document on purchase order should be the name of the reordering rule.')
|
||
|
self.assertEqual(purchase_order.order_line.product_qty, 10)
|
||
|
self.assertEqual(purchase_order.order_line.name, 'Product A')
|
||
|
self.assertEqual(purchase_order.user_id, buyer_id)
|
||
|
|
||
|
# Increase the quantity on the RFQ before confirming it
|
||
|
purchase_order.order_line.product_qty = 12
|
||
|
purchase_order.button_confirm()
|
||
|
|
||
|
self.assertEqual(purchase_order.picking_ids.move_ids.filtered(lambda m: m.product_id == self.product_01).product_qty, 12)
|
||
|
purchase_order.picking_ids.button_validate()
|
||
|
next_picking = purchase_order.picking_ids.move_ids.move_dest_ids.picking_id
|
||
|
self.assertEqual(len(next_picking), 1)
|
||
|
self.assertEqual(next_picking.move_ids.filtered(lambda m: m.product_id == self.product_01).product_qty, 12)
|
||
|
|
||
|
# Increase the quantity on the PO
|
||
|
purchase_order.order_line.product_qty = 15
|
||
|
receipt1, receipt2 = purchase_order.picking_ids
|
||
|
self.assertEqual(receipt1.move_ids.product_qty, 12)
|
||
|
self.assertEqual(receipt2.move_ids.product_qty, 3)
|
||
|
purchase_order.picking_ids[1].button_validate()
|
||
|
self.assertEqual(next_picking.move_ids.product_qty, 15)
|
||
|
|
||
|
def test_reordering_rule_2(self):
|
||
|
""" - Receive products in 1 steps
|
||
|
- The product has two reordering rules, each one applying in a sublocation
|
||
|
- Processing the purchase order should fulfill the two sublocations
|
||
|
- Increase the quantity on the RFQ for one of the POL, the extra quantity will go to
|
||
|
the original subloc since we don't know where to push it (no move dest)
|
||
|
- Increase the quantity on the PO, the extra quantity should follow the push rules and
|
||
|
thus go to stock
|
||
|
"""
|
||
|
# Required for `warehouse_id` to be visible in the view
|
||
|
self.env.user.groups_id += self.env.ref('stock.group_stock_multi_locations')
|
||
|
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
subloc_1 = self.env['stock.location'].create({'name': 'subloc_1', 'location_id': warehouse_1.lot_stock_id.id})
|
||
|
subloc_2 = self.env['stock.location'].create({'name': 'subloc_2', 'location_id': warehouse_1.lot_stock_id.id})
|
||
|
|
||
|
# create reordering rules
|
||
|
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
|
||
|
orderpoint_form.warehouse_id = warehouse_1
|
||
|
orderpoint_form.location_id = subloc_1
|
||
|
orderpoint_form.product_id = self.product_01
|
||
|
orderpoint_form.product_min_qty = 0.000
|
||
|
orderpoint_form.product_max_qty = 0.000
|
||
|
order_point_1 = orderpoint_form.save()
|
||
|
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
|
||
|
orderpoint_form.warehouse_id = warehouse_1
|
||
|
orderpoint_form.location_id = subloc_2
|
||
|
orderpoint_form.product_id = self.product_01
|
||
|
orderpoint_form.product_min_qty = 0.000
|
||
|
orderpoint_form.product_max_qty = 0.000
|
||
|
order_point_2 = orderpoint_form.save()
|
||
|
|
||
|
# Create Delivery Order of 10 product
|
||
|
picking_form = Form(self.env['stock.picking'])
|
||
|
picking_form.partner_id = self.partner
|
||
|
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = self.product_01
|
||
|
move.product_uom_qty = 10.0
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = self.product_01
|
||
|
move.product_uom_qty = 10.0
|
||
|
customer_picking = picking_form.save()
|
||
|
customer_picking.move_ids[0].location_id = subloc_1.id
|
||
|
customer_picking.move_ids[1].location_id = subloc_2.id
|
||
|
|
||
|
# picking confirm
|
||
|
customer_picking.action_confirm()
|
||
|
self.assertEqual(self.product_01.with_context(location=subloc_1.id).virtual_available, -10)
|
||
|
self.assertEqual(self.product_01.with_context(location=subloc_2.id).virtual_available, -10)
|
||
|
|
||
|
# Run scheduler
|
||
|
self.env['procurement.group'].run_scheduler()
|
||
|
|
||
|
# Check purchase order created or not
|
||
|
purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.partner.id)])
|
||
|
self.assertTrue(purchase_order, 'No purchase order created.')
|
||
|
self.assertEqual(len(purchase_order.order_line), 2, 'Not enough purchase order lines created.')
|
||
|
|
||
|
# increment the qty of the first po line
|
||
|
purchase_order.order_line.filtered(lambda pol: pol.orderpoint_id == order_point_1).product_qty = 15
|
||
|
purchase_order.button_confirm()
|
||
|
self.assertEqual(self.product_01.with_context(location=subloc_1.id).virtual_available, 5)
|
||
|
self.assertEqual(self.product_01.with_context(location=subloc_2.id).virtual_available, 0)
|
||
|
|
||
|
# increment the qty of the second po line
|
||
|
purchase_order.order_line.filtered(lambda pol: pol.orderpoint_id == order_point_2).product_qty = 15
|
||
|
self.assertEqual(self.product_01.with_context(location=subloc_1.id).virtual_available, 5)
|
||
|
self.assertEqual(self.product_01.with_context(location=subloc_2.id).virtual_available, 5)
|
||
|
self.assertEqual(self.product_01.with_context(location=warehouse_1.lot_stock_id.id).virtual_available, 10) # 5 on the subloc_2, 5 on subloc_1
|
||
|
|
||
|
def test_reordering_rule_3(self):
|
||
|
"""
|
||
|
trigger a reordering rule with a route to a location without warehouse
|
||
|
"""
|
||
|
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
|
||
|
outside_loc = self.env['stock.location'].create({
|
||
|
'name': 'outside',
|
||
|
'usage': 'internal',
|
||
|
'location_id': self.env.ref('stock.stock_location_locations').id,
|
||
|
})
|
||
|
route = self.env['stock.route'].create({
|
||
|
'name': 'resupply outside',
|
||
|
'rule_ids': [
|
||
|
(0, False, {
|
||
|
'name': 'Buy',
|
||
|
'location_dest_id': warehouse_1.lot_stock_id.id,
|
||
|
'company_id': self.env.company.id,
|
||
|
'action': 'buy',
|
||
|
'sequence': 2,
|
||
|
'procure_method': 'make_to_stock',
|
||
|
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||
|
}),
|
||
|
(0, False, {
|
||
|
'name': 'ressuply from stock',
|
||
|
'location_src_id': warehouse_1.lot_stock_id.id,
|
||
|
'location_dest_id': outside_loc.id,
|
||
|
'company_id': self.env.company.id,
|
||
|
'action': 'pull',
|
||
|
'procure_method': 'mts_else_mto',
|
||
|
'sequence': 1,
|
||
|
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||
|
}),
|
||
|
],
|
||
|
})
|
||
|
vendor1 = self.env['res.partner'].create({'name': 'AAA', 'email': 'from.test@example.com'})
|
||
|
supplier_info1 = self.env['product.supplierinfo'].create({
|
||
|
'partner_id': vendor1.id,
|
||
|
'price': 50,
|
||
|
})
|
||
|
product = self.env['product.product'].create({
|
||
|
'name': 'product_rr_3',
|
||
|
'is_storable': True,
|
||
|
'route_ids': [(4, route.id)],
|
||
|
'seller_ids': [(6, 0, [supplier_info1.id])],
|
||
|
})
|
||
|
|
||
|
# create reordering rules
|
||
|
# Required for `warehouse_id` to be visible in the view
|
||
|
self.env['res.users'].browse(2).groups_id += self.env.ref('stock.group_stock_multi_locations')
|
||
|
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'].with_user(2))
|
||
|
orderpoint_form.warehouse_id = warehouse_1
|
||
|
orderpoint_form.location_id = outside_loc
|
||
|
orderpoint_form.product_id = product
|
||
|
orderpoint_form.product_min_qty = 0.000
|
||
|
orderpoint_form.product_max_qty = 0.000
|
||
|
order_point_1 = orderpoint_form.save()
|
||
|
order_point_1.route_id = route
|
||
|
order_point_1.trigger = 'manual'
|
||
|
|
||
|
# Create move out of 10 product
|
||
|
move = self.env['stock.move'].create({
|
||
|
'name': 'move out',
|
||
|
'product_id': product.id,
|
||
|
'product_uom': product.uom_id.id,
|
||
|
'product_uom_qty': 10,
|
||
|
'location_id': outside_loc.id,
|
||
|
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||
|
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||
|
})
|
||
|
move._action_confirm()
|
||
|
|
||
|
# Forecast on the order point should be -10
|
||
|
self.assertEqual(order_point_1.qty_forecast, -10)
|
||
|
|
||
|
order_point_1.action_replenish()
|
||
|
|
||
|
# Check purchase order created or not
|
||
|
purchase_order = self.env['purchase.order.line'].search([('product_id', '=', product.id)]).order_id
|
||
|
self.assertTrue(purchase_order, 'No purchase order created.')
|
||
|
self.assertEqual(len(purchase_order.order_line), 1, 'Not enough purchase order lines created.')
|
||
|
purchase_order.button_confirm()
|
||
|
|
||
|
def test_reordering_rule_4(self):
|
||
|
""" Test that a reordering rule where the min qty is larger than
|
||
|
the max qty cannot be created """
|
||
|
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
|
||
|
with self.assertRaises(ValidationError, msg="The minimum quantity must be less than or equal to the maximum quantity."):
|
||
|
self.env['stock.warehouse.orderpoint'].create({
|
||
|
'warehouse_id': warehouse_1.id,
|
||
|
'location_id': warehouse_1.lot_stock_id.id,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_min_qty': 2,
|
||
|
'product_max_qty': 1,
|
||
|
})
|
||
|
|
||
|
def test_reordering_rule_triggered_two_times(self):
|
||
|
"""
|
||
|
A product P wth RR 0-0-1.
|
||
|
Confirm a delivery with 1 x P -> PO created for it.
|
||
|
Confirm a second delivery, with 1 x P again:
|
||
|
- The PO should be updated
|
||
|
- The qty to order of the RR should be zero
|
||
|
"""
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
stock_location = warehouse.lot_stock_id
|
||
|
out_type = warehouse.out_type_id
|
||
|
customer_location = self.env.ref('stock.stock_location_customers')
|
||
|
|
||
|
rr = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'location_id': stock_location.id,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_min_qty': 0,
|
||
|
'product_max_qty': 0,
|
||
|
'qty_multiple': 1,
|
||
|
})
|
||
|
|
||
|
delivery = self.env['stock.picking'].create({
|
||
|
'picking_type_id': out_type.id,
|
||
|
'location_id': stock_location.id,
|
||
|
'location_dest_id': customer_location.id,
|
||
|
'move_ids': [(0, 0, {
|
||
|
'name': self.product_01.name,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_uom_qty': 1,
|
||
|
'product_uom': self.product_01.uom_id.id,
|
||
|
'location_id': stock_location.id,
|
||
|
'location_dest_id': customer_location.id,
|
||
|
})]
|
||
|
})
|
||
|
delivery.action_confirm()
|
||
|
|
||
|
pol = self.env['purchase.order.line'].search([('product_id', '=', self.product_01.id)])
|
||
|
self.assertEqual(pol.product_qty, 1.0)
|
||
|
self.assertEqual(rr.qty_to_order, 0.0)
|
||
|
|
||
|
delivery = self.env['stock.picking'].create({
|
||
|
'picking_type_id': out_type.id,
|
||
|
'location_id': stock_location.id,
|
||
|
'location_dest_id': customer_location.id,
|
||
|
'move_ids': [(0, 0, {
|
||
|
'name': self.product_01.name,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_uom_qty': 1,
|
||
|
'product_uom': self.product_01.uom_id.id,
|
||
|
'location_id': stock_location.id,
|
||
|
'location_dest_id': customer_location.id,
|
||
|
})]
|
||
|
})
|
||
|
delivery.action_confirm()
|
||
|
|
||
|
self.assertEqual(pol.product_qty, 2.0)
|
||
|
self.assertEqual(rr.qty_to_order, 0.0)
|
||
|
|
||
|
def test_replenish_report_1(self):
|
||
|
"""Tests the auto generation of manual orderpoints.
|
||
|
|
||
|
Opening multiple times the report should not duplicate the generated orderpoints.
|
||
|
MTO products should not trigger the creation of generated orderpoints
|
||
|
"""
|
||
|
partner = self.env['res.partner'].create({
|
||
|
'name': 'Tintin'
|
||
|
})
|
||
|
route_buy = self.env.ref('purchase_stock.route_warehouse0_buy')
|
||
|
route_mto = self.env.ref('stock.route_warehouse0_mto')
|
||
|
|
||
|
product_form = Form(self.env['product.product'])
|
||
|
product_form.name = 'Simple Product'
|
||
|
product_form.is_storable = True
|
||
|
with product_form.seller_ids.new() as s:
|
||
|
s.partner_id = partner
|
||
|
product = product_form.save()
|
||
|
|
||
|
product_form = Form(self.env['product.product'])
|
||
|
product_form.name = 'Product BUY + MTO'
|
||
|
product_form.is_storable = True
|
||
|
product_form.route_ids.add(route_buy)
|
||
|
product_form.route_ids.add(route_mto)
|
||
|
with product_form.seller_ids.new() as s:
|
||
|
s.partner_id = partner
|
||
|
product_buy_mto = product_form.save()
|
||
|
|
||
|
# Create Delivery Order of 20 product and 10 buy + MTO
|
||
|
picking_form = Form(self.env['stock.picking'])
|
||
|
picking_form.partner_id = partner
|
||
|
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product
|
||
|
move.product_uom_qty = 10.0
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product
|
||
|
move.product_uom_qty = 10.0
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product_buy_mto
|
||
|
move.product_uom_qty = 10.0
|
||
|
customer_picking = picking_form.save()
|
||
|
customer_picking.move_ids.filtered(lambda m: m.product_id == product_buy_mto).procure_method = 'make_to_order'
|
||
|
customer_picking.action_confirm()
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
|
||
|
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product.id)])
|
||
|
orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product_buy_mto.id)])
|
||
|
self.assertFalse(orderpoint_product_mto_buy)
|
||
|
self.assertEqual(len(orderpoint_product), 1.0)
|
||
|
self.assertEqual(orderpoint_product.qty_to_order, 20.0)
|
||
|
self.assertEqual(orderpoint_product.trigger, 'manual')
|
||
|
self.assertEqual(orderpoint_product.create_uid.id, SUPERUSER_ID)
|
||
|
|
||
|
orderpoint_product.action_replenish()
|
||
|
po = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
|
||
|
self.assertTrue(po)
|
||
|
self.assertEqual(len(po.order_line), 2.0)
|
||
|
po_line_product_mto = po.order_line.filtered(lambda l: l.product_id == product_buy_mto)
|
||
|
po_line_product = po.order_line.filtered(lambda l: l.product_id == product)
|
||
|
self.assertEqual(po_line_product_mto.product_uom_qty, 10.0)
|
||
|
self.assertEqual(po_line_product.product_uom_qty, 20.0)
|
||
|
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product.id)])
|
||
|
orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product_buy_mto.id)])
|
||
|
self.assertFalse(orderpoint_product)
|
||
|
self.assertFalse(orderpoint_product_mto_buy)
|
||
|
|
||
|
# Create Delivery Order of 10 product and 10 buy + MTO
|
||
|
picking_form = Form(self.env['stock.picking'])
|
||
|
picking_form.partner_id = partner
|
||
|
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product
|
||
|
move.product_uom_qty = 10.0
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product_buy_mto
|
||
|
move.product_uom_qty = 10.0
|
||
|
customer_picking = picking_form.save()
|
||
|
customer_picking.move_ids.filtered(lambda m: m.product_id == product_buy_mto).procure_method = 'make_to_order'
|
||
|
customer_picking.action_confirm()
|
||
|
self.env['stock.warehouse.orderpoint'].flush_model()
|
||
|
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product.id)])
|
||
|
orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product_buy_mto.id)])
|
||
|
self.assertFalse(orderpoint_product_mto_buy)
|
||
|
self.assertEqual(len(orderpoint_product), 1.0)
|
||
|
self.assertEqual(orderpoint_product.qty_to_order, 10.0)
|
||
|
self.assertEqual(orderpoint_product.trigger, 'manual')
|
||
|
self.assertEqual(orderpoint_product.create_uid.id, SUPERUSER_ID)
|
||
|
|
||
|
def test_replenish_report_2(self):
|
||
|
"""Same then `test_replenish_report_1` but with two steps receipt enabled"""
|
||
|
partner = self.env['res.partner'].create({
|
||
|
'name': 'Tintin'
|
||
|
})
|
||
|
for wh in self.env['stock.warehouse'].search([]):
|
||
|
wh.write({'reception_steps': 'two_steps'})
|
||
|
route_buy = self.env.ref('purchase_stock.route_warehouse0_buy')
|
||
|
route_mto = self.env.ref('stock.route_warehouse0_mto')
|
||
|
|
||
|
product_form = Form(self.env['product.product'])
|
||
|
product_form.name = 'Simple Product'
|
||
|
product_form.is_storable = True
|
||
|
with product_form.seller_ids.new() as s:
|
||
|
s.partner_id = partner
|
||
|
product = product_form.save()
|
||
|
|
||
|
product_form = Form(self.env['product.product'])
|
||
|
product_form.name = 'Product BUY + MTO'
|
||
|
product_form.is_storable = True
|
||
|
product_form.route_ids.add(route_buy)
|
||
|
product_form.route_ids.add(route_mto)
|
||
|
with product_form.seller_ids.new() as s:
|
||
|
s.partner_id = partner
|
||
|
product_buy_mto = product_form.save()
|
||
|
|
||
|
# Create Delivery Order of 20 product and 10 buy + MTO
|
||
|
picking_form = Form(self.env['stock.picking'])
|
||
|
picking_form.partner_id = partner
|
||
|
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product
|
||
|
move.product_uom_qty = 10.0
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product
|
||
|
move.product_uom_qty = 10.0
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product_buy_mto
|
||
|
move.product_uom_qty = 10.0
|
||
|
customer_picking = picking_form.save()
|
||
|
customer_picking.move_ids.filtered(lambda m: m.product_id == product_buy_mto).procure_method = 'make_to_order'
|
||
|
customer_picking.action_confirm()
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product.id)])
|
||
|
orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product_buy_mto.id)])
|
||
|
self.assertFalse(orderpoint_product_mto_buy)
|
||
|
self.assertEqual(len(orderpoint_product), 1.0)
|
||
|
self.assertEqual(orderpoint_product.qty_to_order, 20.0)
|
||
|
self.assertEqual(orderpoint_product.trigger, 'manual')
|
||
|
self.assertEqual(orderpoint_product.create_uid.id, SUPERUSER_ID)
|
||
|
|
||
|
orderpoint_product.action_replenish()
|
||
|
po = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
|
||
|
self.assertTrue(po)
|
||
|
self.assertEqual(len(po.order_line), 2.0)
|
||
|
po_line_product_mto = po.order_line.filtered(lambda l: l.product_id == product_buy_mto)
|
||
|
po_line_product = po.order_line.filtered(lambda l: l.product_id == product)
|
||
|
self.assertEqual(po_line_product_mto.product_uom_qty, 10.0)
|
||
|
self.assertEqual(po_line_product.product_uom_qty, 20.0)
|
||
|
|
||
|
self.env['stock.warehouse.orderpoint'].flush_model()
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product.id)])
|
||
|
orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product_buy_mto.id)])
|
||
|
self.assertFalse(orderpoint_product)
|
||
|
self.assertFalse(orderpoint_product_mto_buy)
|
||
|
|
||
|
# Create Delivery Order of 10 product and 10 buy + MTO
|
||
|
picking_form = Form(self.env['stock.picking'])
|
||
|
picking_form.partner_id = partner
|
||
|
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product
|
||
|
move.product_uom_qty = 10.0
|
||
|
with picking_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = product_buy_mto
|
||
|
move.product_uom_qty = 10.0
|
||
|
customer_picking = picking_form.save()
|
||
|
customer_picking.move_ids.filtered(lambda m: m.product_id == product_buy_mto).procure_method = 'make_to_order'
|
||
|
customer_picking.action_confirm()
|
||
|
self.env['stock.warehouse.orderpoint'].flush_model()
|
||
|
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product.id)])
|
||
|
orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
|
||
|
[('product_id', '=', product_buy_mto.id)])
|
||
|
self.assertFalse(orderpoint_product_mto_buy)
|
||
|
self.assertEqual(len(orderpoint_product), 1.0)
|
||
|
self.assertEqual(orderpoint_product.qty_to_order, 10.0)
|
||
|
self.assertEqual(orderpoint_product.trigger, 'manual')
|
||
|
self.assertEqual(orderpoint_product.create_uid.id, SUPERUSER_ID)
|
||
|
|
||
|
def test_procure_not_default_partner(self):
|
||
|
"""Define a product with 2 vendors. First run a "standard" procurement,
|
||
|
default vendor should be used. Then, call a procurement with
|
||
|
`partner_id` specified in values, the specified vendor should be
|
||
|
used."""
|
||
|
purchase_route = self.env.ref("purchase_stock.route_warehouse0_buy")
|
||
|
uom_unit = self.env.ref("uom.product_uom_unit")
|
||
|
warehouse = self.env['stock.warehouse'].search(
|
||
|
[('company_id', '=', self.env.company.id)], limit=1)
|
||
|
product = self.env["product.product"].create({
|
||
|
"name": "product TEST",
|
||
|
"standard_price": 100.0,
|
||
|
"is_storable": True,
|
||
|
"uom_id": uom_unit.id,
|
||
|
"default_code": "A",
|
||
|
"route_ids": [(6, 0, purchase_route.ids)],
|
||
|
})
|
||
|
default_vendor = self.env["res.partner"].create({
|
||
|
"name": "Supplier A",
|
||
|
})
|
||
|
secondary_vendor = self.env["res.partner"].create({
|
||
|
"name": "Supplier B",
|
||
|
})
|
||
|
self.env["product.supplierinfo"].create({
|
||
|
"partner_id": default_vendor.id,
|
||
|
"product_tmpl_id": product.product_tmpl_id.id,
|
||
|
"delay": 7,
|
||
|
})
|
||
|
self.env["product.supplierinfo"].create({
|
||
|
"partner_id": secondary_vendor.id,
|
||
|
"product_tmpl_id": product.product_tmpl_id.id,
|
||
|
"delay": 10,
|
||
|
})
|
||
|
|
||
|
# Test standard procurement.
|
||
|
po_line = self.env["purchase.order.line"].search(
|
||
|
[("product_id", "=", product.id)])
|
||
|
self.assertFalse(po_line)
|
||
|
self.env["procurement.group"].run(
|
||
|
[self.env["procurement.group"].Procurement(
|
||
|
product, 100, uom_unit,
|
||
|
warehouse.lot_stock_id, "Test default vendor", "/",
|
||
|
self.env.company,
|
||
|
{
|
||
|
"warehouse_id": warehouse,
|
||
|
"date_planned": dt.today() + td(days=15),
|
||
|
"rule_id": warehouse.buy_pull_id,
|
||
|
"group_id": False,
|
||
|
"route_ids": [],
|
||
|
}
|
||
|
)])
|
||
|
po_line = self.env["purchase.order.line"].search(
|
||
|
[("product_id", "=", product.id)])
|
||
|
self.assertTrue(po_line)
|
||
|
self.assertEqual(po_line.partner_id, default_vendor)
|
||
|
po_line.order_id.button_cancel()
|
||
|
po_line.order_id.unlink()
|
||
|
|
||
|
# now force the vendor:
|
||
|
po_line = self.env["purchase.order.line"].search(
|
||
|
[("product_id", "=", product.id)])
|
||
|
self.assertFalse(po_line)
|
||
|
self.env["procurement.group"].run(
|
||
|
[self.env["procurement.group"].Procurement(
|
||
|
product, 100, uom_unit,
|
||
|
warehouse.lot_stock_id, "Test default vendor", "/",
|
||
|
self.env.company,
|
||
|
{
|
||
|
"warehouse_id": warehouse,
|
||
|
"date_planned": dt.today() + td(days=15),
|
||
|
"rule_id": warehouse.buy_pull_id,
|
||
|
"group_id": False,
|
||
|
"route_ids": [],
|
||
|
"supplierinfo_name": secondary_vendor,
|
||
|
}
|
||
|
)])
|
||
|
po_line = self.env["purchase.order.line"].search(
|
||
|
[("product_id", "=", product.id)])
|
||
|
self.assertTrue(po_line)
|
||
|
self.assertEqual(po_line.partner_id, secondary_vendor)
|
||
|
|
||
|
def test_procure_multi_lingual(self):
|
||
|
"""
|
||
|
Define a product with description in English and French.
|
||
|
Run a procurement specifying a group_id with a partner (customer)
|
||
|
set up with French as language. Verify that the PO is generated
|
||
|
using the default (English) language.
|
||
|
"""
|
||
|
purchase_route = self.env.ref("purchase_stock.route_warehouse0_buy")
|
||
|
# create a new warehouse to make sure it gets the mts/mto rule
|
||
|
warehouse = self.env['stock.warehouse'].create({
|
||
|
"name": "test warehouse",
|
||
|
"active": True,
|
||
|
'reception_steps': 'one_step',
|
||
|
'delivery_steps': 'ship_only',
|
||
|
'code': 'TEST'
|
||
|
})
|
||
|
customer_loc, _ = warehouse._get_partner_locations()
|
||
|
mto_rule = self.env['stock.rule'].search(
|
||
|
[('warehouse_id', '=', warehouse.id),
|
||
|
('procure_method', '=', 'make_to_order'),
|
||
|
('location_dest_id', '=', customer_loc.id)
|
||
|
]
|
||
|
)
|
||
|
route_mto = self.env["stock.route"].create({
|
||
|
"name": "MTO",
|
||
|
"active": True,
|
||
|
"sequence": 3,
|
||
|
"product_selectable": True,
|
||
|
"rule_ids": [(6, 0, [
|
||
|
mto_rule.id
|
||
|
])]
|
||
|
})
|
||
|
uom_unit = self.env.ref("uom.product_uom_unit")
|
||
|
product = self.env["product.product"].create({
|
||
|
"name": "product TEST",
|
||
|
"standard_price": 100.0,
|
||
|
"is_storable": True,
|
||
|
"uom_id": uom_unit.id,
|
||
|
"default_code": "A",
|
||
|
"route_ids": [(6, 0, [
|
||
|
route_mto.id,
|
||
|
purchase_route.id,
|
||
|
])],
|
||
|
})
|
||
|
self.env['res.lang']._activate_lang('fr_FR')
|
||
|
product.product_tmpl_id.with_context(lang='fr_FR').name = 'produit en français'
|
||
|
product.with_context(lang='fr_FR').name = 'produit en français'
|
||
|
default_vendor = self.env["res.partner"].create({
|
||
|
"name": "Supplier A",
|
||
|
})
|
||
|
self.env["product.supplierinfo"].create({
|
||
|
"partner_id": default_vendor.id,
|
||
|
"product_tmpl_id": product.product_tmpl_id.id,
|
||
|
"delay": 7,
|
||
|
})
|
||
|
customer = self.env["res.partner"].create({
|
||
|
"name": "Customer",
|
||
|
"lang": "fr_FR"
|
||
|
})
|
||
|
proc_group = self.env["procurement.group"].create({
|
||
|
"partner_id": customer.id
|
||
|
})
|
||
|
procurement = self.env["procurement.group"].Procurement(
|
||
|
product, 100, uom_unit,
|
||
|
customer.property_stock_customer,
|
||
|
"Test default vendor",
|
||
|
"/",
|
||
|
self.env.company,
|
||
|
{
|
||
|
"warehouse_id": warehouse,
|
||
|
"date_planned": dt.today() + td(days=15),
|
||
|
"group_id": proc_group,
|
||
|
"route_ids": [],
|
||
|
}
|
||
|
)
|
||
|
self.env.invalidate_all()
|
||
|
|
||
|
self.env["procurement.group"].run([procurement])
|
||
|
|
||
|
po_line = self.env["purchase.order.line"].search(
|
||
|
[("product_id", "=", product.id)])
|
||
|
self.assertTrue(po_line)
|
||
|
self.assertEqual("[A] product TEST", po_line.name)
|
||
|
|
||
|
def test_multi_locations_and_reordering_rule(self):
|
||
|
""" Suppose two orderpoints for the same product, each one to a different location
|
||
|
If the user triggers each orderpoint separately, it should still produce two
|
||
|
different purchase order lines (one for each orderpoint)
|
||
|
"""
|
||
|
# Required for `warehouse_id` to be visible in the view
|
||
|
self.env.user.groups_id += self.env.ref('stock.group_stock_multi_locations')
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
stock_location = warehouse.lot_stock_id
|
||
|
sub_location = self.env['stock.location'].create({'name': 'subloc_1', 'location_id': stock_location.id})
|
||
|
|
||
|
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
|
||
|
orderpoint_form.warehouse_id = warehouse
|
||
|
orderpoint_form.location_id = stock_location
|
||
|
orderpoint_form.product_id = self.product_01
|
||
|
orderpoint_form.product_min_qty = 1
|
||
|
orderpoint_form.product_max_qty = 1
|
||
|
stock_op = orderpoint_form.save()
|
||
|
|
||
|
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
|
||
|
orderpoint_form.warehouse_id = warehouse
|
||
|
orderpoint_form.location_id = sub_location
|
||
|
orderpoint_form.product_id = self.product_01
|
||
|
orderpoint_form.product_min_qty = 2
|
||
|
orderpoint_form.product_max_qty = 2
|
||
|
sub_op = orderpoint_form.save()
|
||
|
|
||
|
stock_op.action_replenish()
|
||
|
sub_op.action_replenish()
|
||
|
|
||
|
po = self.env['purchase.order'].search([('partner_id', '=', self.partner.id)])
|
||
|
self.assertRecordValues(po.order_line, [
|
||
|
{'product_id': self.product_01.id, 'product_qty': 1.0, 'orderpoint_id': stock_op.id},
|
||
|
{'product_id': self.product_01.id, 'product_qty': 2.0, 'orderpoint_id': sub_op.id},
|
||
|
])
|
||
|
|
||
|
po.button_confirm()
|
||
|
picking = po.picking_ids
|
||
|
picking.button_validate()
|
||
|
|
||
|
self.assertRecordValues(picking.move_line_ids, [
|
||
|
{'product_id': self.product_01.id, 'quantity': 1.0, 'state': 'done', 'location_dest_id': stock_location.id},
|
||
|
{'product_id': self.product_01.id, 'quantity': 2.0, 'state': 'done', 'location_dest_id': sub_location.id},
|
||
|
])
|
||
|
|
||
|
def test_2steps_and_partner_on_orderpoint(self):
|
||
|
"""
|
||
|
Suppose a 2-steps receipt
|
||
|
This test ensures that an orderpoint with its route and supplied defined correctly works
|
||
|
"""
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)])
|
||
|
route_buy_id = self.ref('purchase_stock.route_warehouse0_buy')
|
||
|
|
||
|
warehouse.reception_steps = 'two_steps'
|
||
|
|
||
|
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'name': 'RR for %s' % self.product_01.name,
|
||
|
'warehouse_id': warehouse.id,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'trigger': 'manual',
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_min_qty': 1,
|
||
|
'product_max_qty': 5,
|
||
|
'route_id': route_buy_id,
|
||
|
'supplier_id': self.product_01.seller_ids.id,
|
||
|
})
|
||
|
orderpoint.action_replenish()
|
||
|
|
||
|
po_line = self.env['purchase.order.line'].search([('partner_id', '=', self.partner.id), ('product_id', '=', self.product_01.id)])
|
||
|
self.assertEqual(po_line.product_qty, 5)
|
||
|
|
||
|
def test_change_of_scheduled_date(self):
|
||
|
"""
|
||
|
A user creates a delivery, an orderpoint is created. Its forecast
|
||
|
quantity becomes -1 and the quantity to order is 1. Then the user
|
||
|
postpones the scheduled date of the delivery. The quantities of the
|
||
|
orderpoint should be reset to zero.
|
||
|
"""
|
||
|
delivery_form = Form(self.env['stock.picking'])
|
||
|
delivery_form.partner_id = self.partner
|
||
|
delivery_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||
|
with delivery_form.move_ids_without_package.new() as move:
|
||
|
move.product_id = self.product_01
|
||
|
move.product_uom_qty = 1
|
||
|
delivery = delivery_form.save()
|
||
|
delivery.action_confirm()
|
||
|
|
||
|
delivery.move_ids.flush_recordset()
|
||
|
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
|
||
|
|
||
|
orderpoint = self.env['stock.warehouse.orderpoint'].search([('product_id', '=', self.product_01.id)])
|
||
|
self.assertRecordValues(orderpoint, [
|
||
|
{'qty_forecast': -1, 'qty_to_order': 1},
|
||
|
])
|
||
|
|
||
|
# invalidate the fields that will eventually be inconsistent
|
||
|
orderpoint.invalidate_model(fnames=['qty_forecast', 'qty_to_order'])
|
||
|
orderpoint.product_id.invalidate_model(fnames=['virtual_available'])
|
||
|
|
||
|
delivery.scheduled_date += td(days=7)
|
||
|
self.assertRecordValues(orderpoint, [
|
||
|
{'qty_forecast': 0, 'qty_to_order': 0},
|
||
|
])
|
||
|
|
||
|
def test_decrease_qty_multi_step_receipt(self):
|
||
|
""" Two-steps receipt. An orderpoint generates a move from Input to Stock
|
||
|
with 5 x Product01 and a purchase order to fulfill the need of that SM.
|
||
|
Then, the user decreases the qty on the PO and confirms it. The existing
|
||
|
SM should be updated and another one should be created (from Vendors to
|
||
|
Input, for the PO)
|
||
|
"""
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||
|
warehouse.reception_steps = 'two_steps'
|
||
|
input_location_id = warehouse.wh_input_stock_loc_id.id
|
||
|
stock_location_id = warehouse.lot_stock_id.id
|
||
|
customer_location_id = self.ref('stock.stock_location_customers')
|
||
|
supplier_location_id = self.ref('stock.stock_location_suppliers')
|
||
|
|
||
|
self.product_01.description = 'Super Note'
|
||
|
|
||
|
op = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'name': self.product_01.name,
|
||
|
'location_id': stock_location_id,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_min_qty': 0,
|
||
|
'product_max_qty': 0,
|
||
|
'trigger': 'manual',
|
||
|
})
|
||
|
|
||
|
out_move = self.env['stock.move'].create({
|
||
|
'name': self.product_01.name,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_uom': self.product_01.uom_id.id,
|
||
|
'product_uom_qty': 5,
|
||
|
'location_id': stock_location_id,
|
||
|
'location_dest_id': customer_location_id,
|
||
|
})
|
||
|
out_move._action_confirm()
|
||
|
|
||
|
op.action_replenish()
|
||
|
|
||
|
purchase = self.env['purchase.order'].search([('partner_id', '=', self.partner.id)], order="id desc", limit=1)
|
||
|
with Form(purchase) as form:
|
||
|
with form.order_line.edit(0) as line:
|
||
|
line.product_qty = 4
|
||
|
purchase.button_confirm()
|
||
|
moves = self.env['stock.move'].search([('id', '!=', out_move.id), ('product_id', '=', self.product_01.id)], order='id desc')
|
||
|
self.assertRecordValues(moves, [
|
||
|
{'location_id': supplier_location_id, 'location_dest_id': input_location_id, 'product_qty': 4},
|
||
|
])
|
||
|
moves.picking_id.button_validate()
|
||
|
moves = self.env['stock.move'].search([('id', '!=', out_move.id), ('product_id', '=', self.product_01.id)], order='id desc')
|
||
|
self.assertRecordValues(moves, [
|
||
|
{'location_id': input_location_id, 'location_dest_id': stock_location_id, 'product_qty': 4},
|
||
|
{'location_id': supplier_location_id, 'location_dest_id': input_location_id, 'product_qty': 4},
|
||
|
])
|
||
|
|
||
|
def test_decrease_qty_multi_step_receipt02(self):
|
||
|
"""
|
||
|
Two-steps receipt. An orderpoint generates a move from Input to Stock
|
||
|
with 4 x Product01 and a purchase order to fulfill the need of that SM.
|
||
|
Then, the user increases and decreases the qty on the PO. The existing
|
||
|
SMs should be updated.
|
||
|
"""
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||
|
warehouse.reception_steps = 'two_steps'
|
||
|
input_location_id = warehouse.wh_input_stock_loc_id.id
|
||
|
stock_location_id = warehouse.lot_stock_id.id
|
||
|
supplier_location_id = self.ref('stock.stock_location_suppliers')
|
||
|
|
||
|
self.product_01.description = False
|
||
|
|
||
|
op = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'name': self.product_01.name,
|
||
|
'location_id': stock_location_id,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_min_qty': 4,
|
||
|
'product_max_qty': 4,
|
||
|
'trigger': 'manual',
|
||
|
})
|
||
|
op.action_replenish()
|
||
|
|
||
|
purchase = self.env['purchase.order'].search([('partner_id', '=', self.partner.id)], order="id desc", limit=1)
|
||
|
with Form(purchase) as form:
|
||
|
with form.order_line.edit(0) as line:
|
||
|
line.product_qty = 10
|
||
|
purchase.button_confirm()
|
||
|
|
||
|
moves = self.env['stock.move'].search([('product_id', '=', self.product_01.id)], order='id desc')
|
||
|
self.assertRecordValues(moves, [
|
||
|
{'location_id': supplier_location_id, 'location_dest_id': input_location_id, 'product_qty': 10},
|
||
|
])
|
||
|
|
||
|
with Form(purchase) as form:
|
||
|
with form.order_line.edit(0) as line:
|
||
|
line.product_qty = 1
|
||
|
|
||
|
moves = self.env['stock.move'].search([('product_id', '=', self.product_01.id)], order='id desc')
|
||
|
self.assertRecordValues(moves, [
|
||
|
{'location_id': supplier_location_id, 'location_dest_id': input_location_id, 'product_qty': 1},
|
||
|
])
|
||
|
|
||
|
def test_add_line_to_existing_draft_po(self):
|
||
|
"""
|
||
|
Days to purchase = 10
|
||
|
Two products P1, P2 from the same supplier
|
||
|
Several use cases, each time we run the RR one by one. Then, according
|
||
|
to the dates and the configuration, it should use the existing PO or not
|
||
|
"""
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||
|
|
||
|
self.env.company.days_to_purchase = 10
|
||
|
expected_order_date = dt.combine(dt.today() + td(days=10), time(12))
|
||
|
expected_delivery_date = expected_order_date + td(days=1.0)
|
||
|
|
||
|
product_02 = self.env['product.product'].create({
|
||
|
'name': 'Super Product',
|
||
|
'is_storable': True,
|
||
|
'seller_ids': [(0, 0, {'partner_id': self.partner.id})],
|
||
|
})
|
||
|
|
||
|
op_01, op_02 = self.env['stock.warehouse.orderpoint'].create([{
|
||
|
'warehouse_id': warehouse.id,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'product_id': p.id,
|
||
|
'product_min_qty': 1,
|
||
|
'product_max_qty': 1,
|
||
|
} for p in [self.product_01, product_02]])
|
||
|
|
||
|
op_01.action_replenish()
|
||
|
po01 = self.env['purchase.order'].search([], order='id desc', limit=1)
|
||
|
self.assertEqual(po01.date_order, expected_order_date)
|
||
|
|
||
|
op_02.action_replenish()
|
||
|
self.assertEqual(po01.date_order, expected_order_date)
|
||
|
self.assertRecordValues(po01.order_line, [
|
||
|
{'product_id': self.product_01.id, 'date_planned': expected_delivery_date},
|
||
|
{'product_id': product_02.id, 'date_planned': expected_delivery_date},
|
||
|
])
|
||
|
|
||
|
# Reset and try another flow
|
||
|
po01.button_cancel()
|
||
|
op_01.action_replenish()
|
||
|
po02 = self.env['purchase.order'].search([], order='id desc', limit=1)
|
||
|
self.assertNotEqual(po02, po01)
|
||
|
|
||
|
with freeze_time(dt.today() + td(days=1)):
|
||
|
op_02.invalidate_recordset(fnames=['lead_days_date'])
|
||
|
op_02.action_replenish()
|
||
|
self.assertEqual(po02.date_order, expected_order_date)
|
||
|
self.assertRecordValues(po02.order_line, [
|
||
|
{'product_id': self.product_01.id, 'date_planned': expected_delivery_date},
|
||
|
{'product_id': product_02.id, 'date_planned': expected_delivery_date + td(days=1)},
|
||
|
])
|
||
|
|
||
|
# Restrict the merge with POs that have their order deadline in [today - 2 days, today + 2 days]
|
||
|
self.env['ir.config_parameter'].set_param('purchase_stock.delta_days_merge', '2')
|
||
|
|
||
|
# Reset and try with a second RR executed in the dates range (-> should still use the existing PO)
|
||
|
po02.button_cancel()
|
||
|
op_01.action_replenish()
|
||
|
po03 = self.env['purchase.order'].search([], order='id desc', limit=1)
|
||
|
self.assertNotEqual(po03, po02)
|
||
|
|
||
|
with freeze_time(dt.today() + td(days=2)):
|
||
|
op_02.invalidate_recordset(fnames=['lead_days_date'])
|
||
|
op_02.action_replenish()
|
||
|
self.assertEqual(po03.date_order, expected_order_date)
|
||
|
self.assertRecordValues(po03.order_line, [
|
||
|
{'product_id': self.product_01.id, 'date_planned': expected_delivery_date},
|
||
|
{'product_id': product_02.id, 'date_planned': expected_delivery_date + td(days=2)},
|
||
|
])
|
||
|
|
||
|
# Reset and try with a second RR executed after the dates range (-> should not use the existing PO)
|
||
|
po03.button_cancel()
|
||
|
op_01.action_replenish()
|
||
|
po04 = self.env['purchase.order'].search([], order='id desc', limit=1)
|
||
|
self.assertNotEqual(po04, po03)
|
||
|
|
||
|
with freeze_time(dt.today() + td(days=3)):
|
||
|
op_02.invalidate_recordset(fnames=['lead_days_date'])
|
||
|
op_02.action_replenish()
|
||
|
self.assertEqual(po04.order_line.product_id, self.product_01, 'There should be only a line for product 01')
|
||
|
po05 = self.env['purchase.order'].search([], order='id desc', limit=1)
|
||
|
self.assertNotEqual(po05, po04, 'A new PO should be generated')
|
||
|
self.assertEqual(po05.order_line.product_id, product_02)
|
||
|
|
||
|
def test_reordering_rule_visibility_days(self):
|
||
|
"""
|
||
|
Test the visibility days on the reordering rule update the qty_to_order but do not
|
||
|
update the forecasted quantity of the current day.
|
||
|
|
||
|
ex:
|
||
|
- We are January 14th
|
||
|
- visibility days = 10
|
||
|
- A sale order is scheduled on January 20th
|
||
|
-> 2 scenarios
|
||
|
1. Today's forecasted quantity is < orderpoint's min qty
|
||
|
the sale order will be taken into account in the forecasted quantity
|
||
|
2. Todays's forecasted quantity is >= orderpoint's min qty
|
||
|
the sale order will not be taken into account in the forecasted quantity
|
||
|
"""
|
||
|
# create reordering rule
|
||
|
wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
op = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'warehouse_id': wh.id,
|
||
|
'location_id': wh.lot_stock_id.id,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_min_qty': 0,
|
||
|
'product_max_qty': 0,
|
||
|
'visibility_days': 10,
|
||
|
})
|
||
|
|
||
|
# out move on January 20th
|
||
|
move = self.env['stock.move'].create({
|
||
|
'name': 'Test move',
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_uom': self.product_01.uom_id.id,
|
||
|
'product_uom_qty': 1,
|
||
|
'location_id': wh.lot_stock_id.id,
|
||
|
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||
|
'date': dt.today() + td(days=6),
|
||
|
})
|
||
|
move._action_confirm()
|
||
|
self.assertEqual(op.qty_to_order, 0, 'sale order is ignored')
|
||
|
# out move today to force the forecast to be negative
|
||
|
move = self.env['stock.move'].create({
|
||
|
'name': 'Test move',
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_uom': self.product_01.uom_id.id,
|
||
|
'product_uom_qty': 1,
|
||
|
'location_id': wh.lot_stock_id.id,
|
||
|
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||
|
})
|
||
|
move._action_confirm()
|
||
|
|
||
|
# virtual available is -1 but we need to replenish 2
|
||
|
self.product_01.virtual_available = -1
|
||
|
self.assertEqual(op.qty_to_order, 2, 'sale order is ignored')
|
||
|
|
||
|
def test_reordering_rule_visibility_days_display(self):
|
||
|
""" Checks that the visibility days are properly shown on the info wizard & the orderpoint forecast.
|
||
|
"""
|
||
|
today = dt.today()
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||
|
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'warehouse_id': warehouse.id,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_min_qty': 0,
|
||
|
'product_max_qty': 0,
|
||
|
'visibility_days': 5,
|
||
|
})
|
||
|
|
||
|
# Out move in 5 days
|
||
|
out_5_days = self.env['stock.move'].create({
|
||
|
'name': '5 days',
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_uom_qty': 5,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'picking_type_id': warehouse.out_type_id.id,
|
||
|
'date': today + td(days=5),
|
||
|
})
|
||
|
out_5_days._action_confirm()
|
||
|
|
||
|
# Visibility days should be ignored if nothing is found within lead times (today + 1 day)
|
||
|
replenishment_info = loads(self.env['stock.replenishment.info'].create({'orderpoint_id': orderpoint.id}).json_lead_days)
|
||
|
self.assertEqual(replenishment_info['lead_days_date'], format_date(orderpoint.env, today + td(days=1)))
|
||
|
self.assertEqual(float(replenishment_info['qty_to_order']), 0)
|
||
|
self.assertEqual(replenishment_info['visibility_days'], 0)
|
||
|
# Extra lines for forecast are given through its context
|
||
|
context = orderpoint.action_product_forecast_report()['context']
|
||
|
self.assertEqual(context['qty_to_order'], 0)
|
||
|
self.assertEqual(context['lead_days_date'], format_date(orderpoint.env, today + td(days=1)))
|
||
|
self.assertEqual(context['qty_to_order_with_visibility_days'], 0)
|
||
|
|
||
|
# Out move today
|
||
|
out_today = self.env['stock.move'].create({
|
||
|
'name': 'today',
|
||
|
'product_id': self.product_01.id,
|
||
|
'product_uom_qty': 3,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'picking_type_id': warehouse.out_type_id.id,
|
||
|
'partner_id': self.partner.id, # Avoids the two moves being merged
|
||
|
'date': today,
|
||
|
})
|
||
|
out_today._action_confirm()
|
||
|
|
||
|
# Visibility days should be used something is found within lead times
|
||
|
replenishment_info = loads(self.env['stock.replenishment.info'].create({'orderpoint_id': orderpoint.id}).json_lead_days)
|
||
|
self.assertEqual(replenishment_info['lead_days_date'], format_date(orderpoint.env, today + td(days=1)))
|
||
|
self.assertEqual(float(replenishment_info['qty_to_order']), 8)
|
||
|
self.assertEqual(replenishment_info['visibility_days'], 5)
|
||
|
self.assertEqual(replenishment_info['visibility_days_date'], format_date(orderpoint.env, today + td(days=1) + td(days=5)))
|
||
|
# Extra lines for forecast are given through its context
|
||
|
context = orderpoint.action_product_forecast_report()['context']
|
||
|
self.assertEqual(context['qty_to_order'], 3)
|
||
|
self.assertEqual(context['lead_days_date'], format_date(orderpoint.env, today + td(days=1)))
|
||
|
self.assertEqual(context['qty_to_order_with_visibility_days'], 8)
|
||
|
self.assertEqual(context['visibility_days_date'], format_date(orderpoint.env, today + td(days=1) + td(days=5)))
|
||
|
|
||
|
def test_update_po_line_without_purchase_access_right(self):
|
||
|
""" Test that a user without purchase access right can update a PO line from picking."""
|
||
|
# create a user with only inventory access right
|
||
|
user = self.env['res.users'].create({
|
||
|
'name': 'Inventory Manager',
|
||
|
'login': 'inv_manager',
|
||
|
'groups_id': [(6, 0, [self.env.ref('stock.group_stock_user').id])]
|
||
|
})
|
||
|
product = self.env['product.product'].create({
|
||
|
'name': 'Storable Product',
|
||
|
'is_storable': True,
|
||
|
'seller_ids': [(0, 0, {'partner_id': self.partner.id})],
|
||
|
})
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||
|
self.env['stock.warehouse.orderpoint'].create({
|
||
|
'warehouse_id': warehouse.id,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'product_id': product.id,
|
||
|
'product_min_qty': 5,
|
||
|
'product_max_qty': 5,
|
||
|
})
|
||
|
# run the scheduler
|
||
|
self.env['procurement.group'].run_scheduler()
|
||
|
# check that the PO line is created
|
||
|
po_line = self.env['purchase.order.line'].search([('product_id', '=', product.id)])
|
||
|
self.assertEqual(len(po_line), 1, 'There should be only one PO line')
|
||
|
self.assertEqual(po_line.product_qty, 5, 'The PO line quantity should be 5')
|
||
|
# Update the po line from the picking
|
||
|
picking = self.env['stock.picking'].with_user(user).create({
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||
|
'picking_type_id': warehouse.out_type_id.id,
|
||
|
'move_ids': [(0, 0, {
|
||
|
'name': product.name,
|
||
|
'product_id': product.id,
|
||
|
'product_uom': product.uom_id.id,
|
||
|
'product_uom_qty': 1,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||
|
})],
|
||
|
'state': 'draft',
|
||
|
})
|
||
|
picking.with_user(user).action_assign()
|
||
|
# check that the PO line quantity has been updated
|
||
|
self.assertEqual(po_line.product_qty, 6, 'The PO line quantity should be 6')
|
||
|
|
||
|
def test_set_supplier_in_orderpoint(self):
|
||
|
"""
|
||
|
Test that qty_to_order is correctly computed when setting the supplier in an orderpoint
|
||
|
Have a product with a uom in Kg and a purchase uom in Tonne (the purchase UOM should be bigger that the UOM)
|
||
|
and a supplier with a min_qty of 6T
|
||
|
Create an orderpoint with a min_qty of 500Kg and a max_qty of 0Kg
|
||
|
Set the supplier in the orderpoint and check that the qty_to_order is correctly updated to 6000Kg
|
||
|
"""
|
||
|
product = self.env['product.product'].create({
|
||
|
'name': 'Storable Product',
|
||
|
'is_storable': True,
|
||
|
'uom_id': self.env.ref('uom.product_uom_categ_kgm').uom_ids[3].id,
|
||
|
'uom_po_id': self.env.ref('uom.product_uom_categ_kgm').uom_ids[4].id,
|
||
|
'seller_ids': [(0, 0, {'partner_id': self.partner.id, 'min_qty': 6})],
|
||
|
})
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||
|
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'warehouse_id': warehouse.id,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'product_id': product.id,
|
||
|
'product_min_qty': 500,
|
||
|
'product_max_qty': 500,
|
||
|
})
|
||
|
product.seller_ids.with_context(orderpoint_id=orderpoint.id).action_set_supplier()
|
||
|
self.assertEqual(orderpoint.supplier_id, product.seller_ids, 'The supplier should be set in the orderpoint')
|
||
|
self.assertEqual(orderpoint.product_uom, product.uom_id, 'The orderpoint uom should be the same as the product uom')
|
||
|
self.assertEqual(orderpoint.qty_to_order, 6000)
|
||
|
|
||
|
def test_tax_po_line_reordering_rule_with_branch_company(self):
|
||
|
"""
|
||
|
Test that the parent company tax is correctly set in the purchase order line
|
||
|
when the scheduler is triggered and the branch company is used."
|
||
|
"""
|
||
|
self.env.company.write({
|
||
|
'child_ids': [Command.create({
|
||
|
'name': 'Branch A',
|
||
|
'zip': '85120',
|
||
|
})],
|
||
|
})
|
||
|
self.cr.precommit.run() # load the CoA
|
||
|
branch = self.env.company.child_ids
|
||
|
product = self.env['product.product'].with_company(branch).create({
|
||
|
'name': 'Storable Product',
|
||
|
'is_storable': True,
|
||
|
'seller_ids': [Command.create({'partner_id': self.partner.id, 'min_qty': 1})],
|
||
|
})
|
||
|
warehouse = self.env['stock.warehouse'].search([('company_id', '=', branch.id)], limit=1)
|
||
|
product.env['stock.warehouse.orderpoint'].create({
|
||
|
'warehouse_id': warehouse.id,
|
||
|
'location_id': warehouse.lot_stock_id.id,
|
||
|
'product_id': product.id,
|
||
|
'product_min_qty': 10,
|
||
|
'product_max_qty': 10,
|
||
|
})
|
||
|
# run the scheduler
|
||
|
self.env['procurement.group'].run_scheduler()
|
||
|
# check that the PO line is created
|
||
|
po_line = self.env['purchase.order.line'].search([('product_id', '=', product.id)])
|
||
|
self.assertEqual(len(po_line), 1, 'There should be only one PO line')
|
||
|
self.assertEqual(po_line.product_qty, 10, 'The PO line quantity should be 10')
|
||
|
self.assertTrue(po_line.taxes_id)
|
||
|
|
||
|
def test_forbid_snoozing_auto_trigger_orderpoint(self):
|
||
|
"""
|
||
|
Check that you can not snooze an auto-trigger reoredering rule
|
||
|
"""
|
||
|
buy_route = self.env.ref('purchase_stock.route_warehouse0_buy')
|
||
|
product = self.env['product.product'].create({
|
||
|
'name': 'Super product',
|
||
|
'is_storable': True,
|
||
|
'route_ids': [Command.set(buy_route.ids)],
|
||
|
})
|
||
|
|
||
|
# check that you can not create a snoozed auto-trigger reoredering rule
|
||
|
with self.assertRaises(UserError):
|
||
|
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'name': 'Super product RR',
|
||
|
'route_id': buy_route.id,
|
||
|
'product_id': product.id,
|
||
|
'product_min_qty': 0,
|
||
|
'product_max_qty': 5,
|
||
|
'snoozed_until': add(Date.today(), days=1),
|
||
|
})
|
||
|
|
||
|
# check that you can not snooze an existing one
|
||
|
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'name': 'Super product RR',
|
||
|
'route_id': buy_route.id,
|
||
|
'product_id': product.id,
|
||
|
'product_min_qty': 0,
|
||
|
'product_max_qty': 5,
|
||
|
})
|
||
|
with self.assertRaises(UserError):
|
||
|
orderpoint.snoozed_until = add(Date.today(), days=1)
|
||
|
|
||
|
def test_supplierinfo_last_purchase_date(self):
|
||
|
"""
|
||
|
Test that the last_purchase_date on the replenishment information is correctly computed
|
||
|
A user creates two purchase orders
|
||
|
The last_purchase_date on the supplier info should be computed as the most recent date_order from the purchase orders
|
||
|
"""
|
||
|
res_partner = self.env['res.partner'].create({
|
||
|
'name': 'Test Partner',
|
||
|
})
|
||
|
product = self.env['product.product'].create({
|
||
|
'name': 'Storable Product',
|
||
|
'is_storable': True,
|
||
|
})
|
||
|
orderpoint = self.env['stock.warehouse.orderpoint'].create({
|
||
|
'product_id': product.id,
|
||
|
'product_min_qty': 0,
|
||
|
'product_max_qty': 0,
|
||
|
})
|
||
|
po1_vals = {
|
||
|
'partner_id': res_partner.id,
|
||
|
'date_order': dt.today() - td(days=15),
|
||
|
'order_line': [
|
||
|
(0, 0, {
|
||
|
'name': product.name,
|
||
|
'product_id': product.id,
|
||
|
'product_qty': 1.0,
|
||
|
})],
|
||
|
}
|
||
|
po2_vals = {
|
||
|
'partner_id': res_partner.id,
|
||
|
'date_order': dt.today(),
|
||
|
'order_line': [
|
||
|
(0, 0, {
|
||
|
'name': product.name,
|
||
|
'product_id': product.id,
|
||
|
'product_qty': 1.0,
|
||
|
})],
|
||
|
}
|
||
|
po1 = self.env['purchase.order'].create(po1_vals)
|
||
|
po1.button_confirm()
|
||
|
po2 = self.env['purchase.order'].create(po2_vals)
|
||
|
po2.button_confirm()
|
||
|
replenishment_info = self.env['stock.replenishment.info'].create({'orderpoint_id': orderpoint.id})
|
||
|
supplier_info = replenishment_info.supplierinfo_ids
|
||
|
self.assertEqual(supplier_info.last_purchase_date, dt.today().date(), "The last_purhchase_date should be set to the most recent date_order from the purchase orders")
|