From 626d4c5dc254d087e679565403d9a07c9d211852 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Thu, 19 Sep 2024 10:58:14 -0400 Subject: [PATCH] add more tests for bemade_vendor_pricelist to get coverage up --- .../models/sale_order_line.py | 52 +-- .../tests/__init__.py | 2 +- .../tests/test_margins.py | 372 ++++++++++++++++++ .../tests/test_sale_order_line.py | 164 -------- 4 files changed, 377 insertions(+), 213 deletions(-) create mode 100644 bemade_margin_vendor_pricelist/tests/test_margins.py delete mode 100644 bemade_margin_vendor_pricelist/tests/test_sale_order_line.py diff --git a/bemade_margin_vendor_pricelist/models/sale_order_line.py b/bemade_margin_vendor_pricelist/models/sale_order_line.py index ebe6986..e111f60 100644 --- a/bemade_margin_vendor_pricelist/models/sale_order_line.py +++ b/bemade_margin_vendor_pricelist/models/sale_order_line.py @@ -12,20 +12,6 @@ class SaleOrderLine(models.Model): digits='Product Price' ) - gross_profit_percent_vendor = fields.Float( - string='GP (%) on Vendor Price', - groups='base.group_user', - group_operator='avg', - compute='_compute_gp_vendor' - ) - - gross_profit_vendor = fields.Float( - string='GP on Vendor Price', - groups='base.group_user', - digits='Product Price', - compute='_compute_gp_vendor' - ) - purchase_price_actual = fields.Float( compute="_compute_actual_gp", digits='Product Price', @@ -102,31 +88,13 @@ class SaleOrderLine(models.Model): if is_order and self.qty_to_deliver <= 0: return 0 elif is_order and self.qty_to_deliver > 0: - reserved = sum([q.reserved_quantity for q in self.move_ids.mapped('move_line_ids').mapped('quant_id')]) - missing = self.qty_to_deliver - reserved - if float_compare(missing, 0.0, - precision_rounding=self.product_uom.rounding) == 1: - # Not enough reserved, check stock - missing = missing - qty_available - if float_compare(missing, 0.0, - precision_rounding=self.product_uom.rounding) == 1: - # Missing some stock to meet demand, return the quantity - return missing - else: - # Enough stock available to meet this line's demand - return 0 - else: - # Already have stock reserved - return 0 + reserved = sum([q.reserved_quantity for q in + self.move_ids.mapped('move_line_ids').mapped('quant_id')]) + return max(0, self.qty_to_deliver - reserved - qty_available) else: # This is a quotation, don't bother with stock reservations # Also, if available is negative for some reason, - missing = self.product_uom_qty - qty_available - if float_compare(missing, 0.0, - precision_rounding=self.product_uom.rounding) == 1: - return missing - else: - return 0 + return max(0, self.product_uom_qty - qty_available) @api.depends('product_id', 'product_id.seller_ids', 'product_id.seller_ids.price') @@ -147,15 +115,3 @@ class SaleOrderLine(models.Model): date=line.order_id.date_order or fields.Date.today(), round=False, ) if to_cur and suppinfo.price else suppinfo.price - - @api.depends('purchase_price_vendor') - def _compute_gp_vendor(self): - for line in self: - if not line.price_unit or float_is_zero(line.price_unit): - line.gross_profit_vendor = 0 - line.gross_profit_percent_vendor = 0 - continue - unit_gp = line.price_unit - line.purchase_price_vendor - line.gross_profit_percent_vendor = unit_gp / line.price_unit - - line.gross_profit_vendor = unit_gp * line.product_uom_qty diff --git a/bemade_margin_vendor_pricelist/tests/__init__.py b/bemade_margin_vendor_pricelist/tests/__init__.py index 3142f20..014c7c5 100644 --- a/bemade_margin_vendor_pricelist/tests/__init__.py +++ b/bemade_margin_vendor_pricelist/tests/__init__.py @@ -1 +1 @@ -from . import test_sale_order_line \ No newline at end of file +from . import test_margins \ No newline at end of file diff --git a/bemade_margin_vendor_pricelist/tests/test_margins.py b/bemade_margin_vendor_pricelist/tests/test_margins.py new file mode 100644 index 0000000..fedd0d3 --- /dev/null +++ b/bemade_margin_vendor_pricelist/tests/test_margins.py @@ -0,0 +1,372 @@ +from odoo.tests import TransactionCase, tagged +from unittest.mock import patch, PropertyMock + + +@tagged("-at_install", "post_install") +class TestSaleOrderLine(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create a test vendor + cls.vendor = cls.env['res.partner'].create({ + 'name': 'Test Vendor', + 'supplier_rank': 1, + }) + + # Create a test product + cls.product = cls.env['product.product'].create({ + 'name': 'Test Product', + 'type': 'product', + 'list_price': 100.0, + 'standard_price': 80.0, + }) + + # Link supplier info to the product + cls.supplierinfo = cls.env['product.supplierinfo'].create({ + 'partner_id': cls.vendor.id, + 'product_tmpl_id': cls.product.product_tmpl_id.id, + 'min_qty': 1, + 'price': 75.0, # Vendor price + 'currency_id': cls.env.ref('base.USD').id, # Assuming USD for simplicity + }) + + # Create a test sale order + cls.sale_order = cls.env['sale.order'].create({ + 'partner_id': cls.env.ref('base.res_partner_1').id, + # Assuming a default partner + }) + + # Create a test sale order line + cls.sale_order_line = cls.env['sale.order.line'].create({ + 'order_id': cls.sale_order.id, + 'product_id': cls.product.id, + 'product_uom_qty': 10, + 'price_unit': 120.0, + }) + + def test_purchase_price_all_from_stock(self): + """Test when all quantity is fulfilled from stock.""" + with patch.object( + self.sale_order_line.product_id.__class__, + "qty_available", + new_callable=PropertyMock + ) as mock_qty_available: + mock_qty_available.return_value = 10 # All available + + # Recompute the fields + self.sale_order_line._compute_actual_gp() + + # Assert purchase_price_actual equals purchase_price (from stock) + self.assertEqual( + self.sale_order_line.purchase_price_actual, + self.sale_order_line.purchase_price, + "Purchase price should be from stock when all quantity is available." + ) + + # Assert gross_profit is calculated correctly + expected_gross_profit = self.sale_order_line.price_subtotal - ( + self.sale_order_line.purchase_price_actual * self.sale_order_line.product_uom_qty + ) + self.assertAlmostEqual( + self.sale_order_line.gross_profit, + expected_gross_profit, + msg="Gross profit should be correctly calculated from stock purchase price." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() + + def assertSaleOrderComputesLikeSaleOrderLine(self): + self.assertAlmostEqual( + self.sale_order_line.gross_profit, + self.sale_order.gross_profit, + ) + self.assertAlmostEqual( + self.sale_order_line.gross_profit_percent, + self.sale_order.gross_profit_percent, + ) + + def test_purchase_price_all_from_vendor(self): + """Test when all quantity is fulfilled from vendor.""" + with patch.object( + self.sale_order_line.product_id.__class__, + "qty_available", + new_callable=PropertyMock + ) as mock_qty_available: + mock_qty_available.return_value = 0 + + # Assert purchase_price_actual equals purchase_price_vendor (from vendor) + self.assertEqual( + self.sale_order_line.purchase_price_actual, + self.sale_order_line.purchase_price_vendor, + "Purchase price should be from vendor when all quantity is missing from stock." + ) + + # Assert gross_profit is calculated correctly + expected_gross_profit = self.sale_order_line.price_subtotal - ( + self.sale_order_line.purchase_price_actual * self.sale_order_line.product_uom_qty + ) + self.assertAlmostEqual( + self.sale_order_line.gross_profit, + expected_gross_profit, + msg="Gross profit should be correctly calculated from vendor purchase price." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() + + def test_purchase_price_mixed(self): + """Test when quantity is partially fulfilled from stock and partially from vendor.""" + with patch.object( + self.sale_order_line.product_id.__class__, + "qty_available", + new_callable=PropertyMock + ) as mock_qty_available: + mock_qty_available.return_value = 5 + # Calculate expected blended purchase price + qty_from_vendor = 5 + qty_from_stock = 5 + expected_purchase_price = ( + ( + qty_from_vendor * self.sale_order_line.purchase_price_vendor) + + ( + qty_from_stock * self.sale_order_line.purchase_price) + ) / self.sale_order_line.product_uom_qty + + self.assertAlmostEqual( + self.sale_order_line.purchase_price_actual, + expected_purchase_price, + msg="Purchase price should be a blend of stock and vendor prices." + ) + + # Assert gross_profit is calculated correctly + expected_gross_profit = self.sale_order_line.price_subtotal - ( + expected_purchase_price * self.sale_order_line.product_uom_qty + ) + self.assertAlmostEqual( + self.sale_order_line.gross_profit, + expected_gross_profit, + msg="Gross profit should be correctly calculated from blended purchase price." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() + + def test_negative_available_stock_zero_qty_ordered(self): + """We had a division by zero error with a zero product_uom_qty field + and a negative available stock. This test aims to root out this and + other possible causes of a div by zero error.""" + with patch.object( + self.sale_order_line.product_id.__class__, + "qty_available", + new_callable=PropertyMock + ) as mock_qty_available: + mock_qty_available.return_value = -1 # Negative available stock + + # Set product_uom_qty to zero + self.sale_order_line.product_uom_qty = 0 + # Recompute the fields + self.sale_order_line._compute_actual_gp() + + # Assert gross_profit is 0 + self.assertEqual( + self.sale_order_line.gross_profit, + 0.0, + "Gross profit should be 0 when product_uom_qty is zero." + ) + + # Assert gross_profit_percent is 0 + self.assertEqual( + self.sale_order_line.gross_profit_percent, + 0.0, + "Gross profit percent should be 0 when product_uom_qty is zero." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() + + def test_purchase_price_all_from_stock_confirmed(self): + """Test when all quantity is fulfilled from stock.""" + self.sale_order.action_confirm() + with patch.object( + self.sale_order_line.product_id.__class__, + "qty_available", + new_callable=PropertyMock + ) as mock_qty_available: + mock_qty_available.return_value = 10 # All available + + # Recompute the fields + self.sale_order_line._compute_actual_gp() + + # Assert purchase_price_actual equals purchase_price (from stock) + self.assertEqual( + self.sale_order_line.purchase_price_actual, + self.sale_order_line.purchase_price, + "Purchase price should be from stock when all quantity is available." + ) + + # Assert gross_profit is calculated correctly + expected_gross_profit = self.sale_order_line.price_subtotal - ( + self.sale_order_line.purchase_price_actual * self.sale_order_line.product_uom_qty + ) + self.assertAlmostEqual( + self.sale_order_line.gross_profit, + expected_gross_profit, + msg="Gross profit should be correctly calculated from stock purchase price." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() + + def test_purchase_price_all_from_vendor_confirmed(self): + """Test when all quantity is fulfilled from vendor.""" + self.sale_order.action_confirm() + with patch.object( + self.sale_order_line.product_id.__class__, + "qty_available", + new_callable=PropertyMock + ) as mock_qty_available: + mock_qty_available.return_value = 0 + + # Assert purchase_price_actual equals purchase_price_vendor (from vendor) + self.assertEqual( + self.sale_order_line.purchase_price_actual, + self.sale_order_line.purchase_price_vendor, + "Purchase price should be from vendor when all quantity is missing from stock." + ) + + # Assert gross_profit is calculated correctly + expected_gross_profit = self.sale_order_line.price_subtotal - ( + self.sale_order_line.purchase_price_actual * self.sale_order_line.product_uom_qty + ) + self.assertAlmostEqual( + self.sale_order_line.gross_profit, + expected_gross_profit, + msg="Gross profit should be correctly calculated from vendor purchase price." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() + + def test_purchase_price_mixed_confirmed(self): + """Test when quantity is partially fulfilled from stock and partially from vendor.""" + self.sale_order.action_confirm() + with patch.object( + self.sale_order_line.product_id.__class__, + "qty_available", + new_callable=PropertyMock + ) as mock_qty_available: + mock_qty_available.return_value = 5 + # Calculate expected blended purchase price + qty_from_vendor = 5 + qty_from_stock = 5 + expected_purchase_price = ( + ( + qty_from_vendor * self.sale_order_line.purchase_price_vendor) + + ( + qty_from_stock * self.sale_order_line.purchase_price) + ) / self.sale_order_line.product_uom_qty + + self.assertAlmostEqual( + self.sale_order_line.purchase_price_actual, + expected_purchase_price, + msg="Purchase price should be a blend of stock and vendor prices." + ) + + # Assert gross_profit is calculated correctly + expected_gross_profit = self.sale_order_line.price_subtotal - ( + expected_purchase_price * self.sale_order_line.product_uom_qty + ) + self.assertAlmostEqual( + self.sale_order_line.gross_profit, + expected_gross_profit, + msg="Gross profit should be correctly calculated from blended purchase price." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() + + def test_negative_available_stock_zero_qty_ordered_confirmed(self): + """We had a division by zero error with a zero product_uom_qty field + and a negative available stock. This test aims to root out this and + other possible causes of a div by zero error.""" + self.sale_order.action_confirm() + with patch.object( + self.sale_order_line.product_id.__class__, + "qty_available", + new_callable=PropertyMock + ) as mock_qty_available: + mock_qty_available.return_value = -1 # Negative available stock + + # Set product_uom_qty to zero + self.sale_order_line.product_uom_qty = 0 + # Recompute the fields + self.sale_order_line._compute_actual_gp() + + # Assert gross_profit is 0 + self.assertEqual( + self.sale_order_line.gross_profit, + 0.0, + "Gross profit should be 0 when product_uom_qty is zero." + ) + + # Assert gross_profit_percent is 0 + self.assertEqual( + self.sale_order_line.gross_profit_percent, + 0.0, + "Gross profit percent should be 0 when product_uom_qty is zero." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() + + def test_determine_missing_stock_all_reserved(self): + """Test the actual _determine_missing_stock method when all stock is reserved.""" + # Set initial stock for the product + location = self.env["stock.warehouse"].search([])[0].lot_stock_id + quant = self.env["stock.quant"].create({ + "product_id": self.product.id, + "location_id": location.id, + "inventory_quantity": 10, + "product_uom_id": self.product.uom_id.id, + }) + quant.action_apply_inventory() + + self.sale_order.action_confirm() + # Force the move to be reserved + self.sale_order.picking_ids.action_assign() + + # Now, call _determine_missing_stock without mocking + missing_stock = self.sale_order_line._determine_missing_stock() + + # Assert that missing_stock is 0 + self.assertEqual( + missing_stock, + 0, + "_determine_missing_stock should return 0 when all stock is reserved." + ) + + # Additionally, verify that purchase_price_actual is from stock + self.sale_order_line._compute_actual_gp() + + self.assertEqual( + self.sale_order_line.purchase_price_actual, + self.sale_order_line.purchase_price, + "Purchase price should be from stock when all stock is reserved." + ) + + # Assert gross_profit is calculated correctly + expected_gross_profit = self.sale_order_line.price_subtotal - ( + self.sale_order_line.purchase_price_actual * self.sale_order_line.product_uom_qty + ) + self.assertAlmostEqual( + self.sale_order_line.gross_profit, + expected_gross_profit, + msg="Gross profit should be correctly calculated when all stock is reserved." + ) + + # Assert gross_profit_percent is calculated correctly + if self.sale_order_line.price_subtotal: + expected_gross_profit_percent = self.sale_order_line.gross_profit / self.sale_order_line.price_subtotal + else: + expected_gross_profit_percent = 0.0 + + self.assertAlmostEqual( + self.sale_order_line.gross_profit_percent, + expected_gross_profit_percent, + msg="Gross profit percent should be correctly calculated when all stock is reserved." + ) + # Ensure the SO is correct as well + self.assertSaleOrderComputesLikeSaleOrderLine() diff --git a/bemade_margin_vendor_pricelist/tests/test_sale_order_line.py b/bemade_margin_vendor_pricelist/tests/test_sale_order_line.py deleted file mode 100644 index 29b6a84..0000000 --- a/bemade_margin_vendor_pricelist/tests/test_sale_order_line.py +++ /dev/null @@ -1,164 +0,0 @@ -from odoo.tests import TransactionCase, tagged -from unittest.mock import patch, PropertyMock - - -@tagged("-at_install", "post_install") -class TestSaleOrderLine(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - # Create a test vendor - cls.vendor = cls.env['res.partner'].create({ - 'name': 'Test Vendor', - 'supplier_rank': 1, - }) - - # Create a test product - cls.product = cls.env['product.product'].create({ - 'name': 'Test Product', - 'type': 'product', - 'list_price': 100.0, - 'standard_price': 80.0, - }) - - # Link supplier info to the product - cls.supplierinfo = cls.env['product.supplierinfo'].create({ - 'partner_id': cls.vendor.id, - 'product_tmpl_id': cls.product.product_tmpl_id.id, - 'min_qty': 1, - 'price': 75.0, # Vendor price - 'currency_id': cls.env.ref('base.USD').id, # Assuming USD for simplicity - }) - - # Create a test sale order - cls.sale_order = cls.env['sale.order'].create({ - 'partner_id': cls.env.ref('base.res_partner_1').id, - # Assuming a default partner - }) - - # Create a test sale order line - cls.sale_order_line = cls.env['sale.order.line'].create({ - 'order_id': cls.sale_order.id, - 'product_id': cls.product.id, - 'product_uom_qty': 10, - 'price_unit': 120.0, - }) - - def test_purchase_price_all_from_stock(self): - """Test when all quantity is fulfilled from stock.""" - with patch.object( - self.sale_order_line.product_id.__class__, - "qty_available", - new_callable=PropertyMock - ) as mock_qty_available: - mock_qty_available.return_value = 10 # All available - - # Recompute the fields - self.sale_order_line._compute_actual_gp() - - # Assert purchase_price_actual equals purchase_price (from stock) - self.assertEqual( - self.sale_order_line.purchase_price_actual, - self.sale_order_line.purchase_price, - "Purchase price should be from stock when all quantity is available." - ) - - # Assert gross_profit is calculated correctly - expected_gross_profit = self.sale_order_line.price_subtotal - ( - self.sale_order_line.purchase_price_actual * self.sale_order_line.product_uom_qty - ) - self.assertAlmostEqual( - self.sale_order_line.gross_profit, - expected_gross_profit, - msg="Gross profit should be correctly calculated from stock purchase price." - ) - - def test_purchase_price_all_from_vendor(self): - """Test when all quantity is fulfilled from vendor.""" - with patch.object( - self.sale_order_line.product_id.__class__, - "qty_available", - new_callable=PropertyMock - ) as mock_qty_available: - mock_qty_available.return_value = 0 - - # Assert purchase_price_actual equals purchase_price_vendor (from vendor) - self.assertEqual( - self.sale_order_line.purchase_price_actual, - self.sale_order_line.purchase_price_vendor, - "Purchase price should be from vendor when all quantity is missing from stock." - ) - - # Assert gross_profit is calculated correctly - expected_gross_profit = self.sale_order_line.price_subtotal - ( - self.sale_order_line.purchase_price_actual * self.sale_order_line.product_uom_qty - ) - self.assertAlmostEqual( - self.sale_order_line.gross_profit, - expected_gross_profit, - msg="Gross profit should be correctly calculated from vendor purchase price." - ) - - def test_purchase_price_mixed(self): - """Test when quantity is partially fulfilled from stock and partially from vendor.""" - with patch.object( - self.sale_order_line.product_id.__class__, - "qty_available", - new_callable=PropertyMock - ) as mock_qty_available: - mock_qty_available.return_value = 5 - # Calculate expected blended purchase price - qty_from_vendor = 5 - qty_from_stock = 5 - expected_purchase_price = ( - ( - qty_from_vendor * self.sale_order_line.purchase_price_vendor) + - ( - qty_from_stock * self.sale_order_line.purchase_price) - ) / self.sale_order_line.product_uom_qty - - self.assertAlmostEqual( - self.sale_order_line.purchase_price_actual, - expected_purchase_price, - msg="Purchase price should be a blend of stock and vendor prices." - ) - - # Assert gross_profit is calculated correctly - expected_gross_profit = self.sale_order_line.price_subtotal - ( - expected_purchase_price * self.sale_order_line.product_uom_qty - ) - self.assertAlmostEqual( - self.sale_order_line.gross_profit, - expected_gross_profit, - msg="Gross profit should be correctly calculated from blended purchase price." - ) - - def test_negative_available_stock_zero_qty_ordered(self): - """We had a division by zero error with a zero product_uom_qty field - and a negative available stock. This test aims to root out this and - other possible causes of a div by zero error.""" - with patch.object( - self.sale_order_line.product_id.__class__, - "qty_available", - new_callable=PropertyMock - ) as mock_qty_available: - mock_qty_available.return_value = -1 # Negative available stock - - # Set product_uom_qty to zero - self.sale_order_line.product_uom_qty = 0 - # Recompute the fields - self.sale_order_line._compute_actual_gp() - - # Assert gross_profit is 0 - self.assertEqual( - self.sale_order_line.gross_profit, - 0.0, - "Gross profit should be 0 when product_uom_qty is zero." - ) - - # Assert gross_profit_percent is 0 - self.assertEqual( - self.sale_order_line.gross_profit_percent, - 0.0, - "Gross profit percent should be 0 when product_uom_qty is zero." - )