#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Standalone test runner for property-based tests. This allows running tests without the full Odoo test framework. """ import sys import os # Add the Odoo directory to the path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../../odoo')) from hypothesis import given, settings, strategies as st from decimal import Decimal class MockEnv: """Mock Odoo environment for testing""" def __getitem__(self, key): if key == 'decimal.precision': return MockDecimalPrecision() return self def precision_get(self, name): return 2 # Default precision for Product Price class MockDecimalPrecision: def precision_get(self, name): return 2 class MockLine: """Mock account.move.line for testing""" def __init__(self): self.price_subtotal = 0.0 self.quantity = 1.0 self.price_unit = 0.0 self.move_id = MockMove() self.env = MockEnv() def _onchange_price_subtotal(self): """Simplified version of the onchange method""" if self.move_id.move_type not in ('in_invoice', 'in_refund'): return if self.quantity == 0: raise ValueError("Cannot calculate unit price: quantity must be greater than zero") new_price_unit = self.price_subtotal / self.quantity precision = self.env['decimal.precision'].precision_get('Product Price') self.price_unit = round(new_price_unit, precision) class MockMove: def __init__(self): self.move_type = 'in_invoice' @given( price_subtotal=st.floats(min_value=0.01, max_value=1000000.0, allow_nan=False, allow_infinity=False), quantity=st.floats(min_value=1.0, max_value=10000.0, allow_nan=False, allow_infinity=False) ) @settings(max_examples=100, deadline=None) def test_property_price_subtotal_to_unit_price(price_subtotal, quantity): """ **Feature: vendor-bill-editable-totals, Property 1: Price subtotal to unit price calculation** **Validates: Requirements 1.2, 1.4** Property: For any invoice line with non-zero quantity and a user-modified price_subtotal value, the recalculated price_unit should equal price_subtotal divided by quantity. """ precision = 2 # Product Price precision # Create a mock line line = MockLine() line.quantity = quantity line.price_unit = 1.0 # Set the price_subtotal to trigger the onchange line.price_subtotal = price_subtotal line._onchange_price_subtotal() # Calculate expected price_unit expected_price_unit = price_subtotal / quantity expected_price_unit_rounded = round(expected_price_unit, precision) # Verify the property: price_unit should equal price_subtotal / quantity assert abs(line.price_unit - expected_price_unit_rounded) < 10 ** (-precision), \ f"Price unit calculation failed: expected {expected_price_unit_rounded}, got {line.price_unit}" print(f"✓ Test passed: price_subtotal={price_subtotal:.2f}, quantity={quantity:.2f}, price_unit={line.price_unit:.2f}") @given( price_subtotal=st.floats(min_value=0.01, max_value=1000000.0, allow_nan=False, allow_infinity=False), quantity=st.floats(min_value=1.0, max_value=10000.0, allow_nan=False, allow_infinity=False) ) @settings(max_examples=100, deadline=None) def test_property_quantity_invariance(price_subtotal, quantity): """ **Feature: vendor-bill-editable-totals, Property 2: Quantity invariance during price_subtotal modification** **Validates: Requirements 1.3** Property: For any invoice line, when price_subtotal is modified, the quantity value should remain unchanged after the onchange handler executes. """ # Create a mock line line = MockLine() line.quantity = quantity line.price_unit = 1.0 # Store the initial quantity initial_quantity = line.quantity # Set the price_subtotal to trigger the onchange line.price_subtotal = price_subtotal line._onchange_price_subtotal() # Verify the property: quantity should remain unchanged assert line.quantity == initial_quantity, \ f"Quantity changed during price_subtotal modification: expected {initial_quantity}, got {line.quantity}" print(f"✓ Test passed: price_subtotal={price_subtotal:.2f}, initial_quantity={initial_quantity:.2f}, final_quantity={line.quantity:.2f}") @given( price_subtotal=st.floats(min_value=0.01, max_value=1000000.0, allow_nan=False, allow_infinity=False), quantity=st.floats(min_value=1.0, max_value=10000.0, allow_nan=False, allow_infinity=False) ) @settings(max_examples=100, deadline=None) def test_property_price_subtotal_round_trip(price_subtotal, quantity): """ **Feature: vendor-bill-editable-totals, Property 3: Price subtotal round-trip accuracy** **Validates: Requirements 1.5** Property: For any invoice line with non-zero quantity, if a user inputs a price_subtotal value, the system calculates price_unit, and then Odoo recomputes price_subtotal from that price_unit and quantity, the final price_subtotal should match the user input within the configured decimal precision. """ precision = 2 # Product Price precision # Create a mock line line = MockLine() line.quantity = quantity line.price_unit = 1.0 # Store the user input price_subtotal user_input_subtotal = price_subtotal # Step 1: Set the price_subtotal to trigger the onchange line.price_subtotal = user_input_subtotal line._onchange_price_subtotal() # Step 2: Let Odoo recompute price_subtotal from price_unit and quantity # This simulates what happens when Odoo's standard compute methods run recomputed_subtotal = line.price_unit * line.quantity # Calculate the tolerance based on decimal precision # When price_unit is rounded to 'precision' decimal places, the rounding error # per unit can be up to 0.5 * 10^(-precision). When multiplied by quantity, # the cumulative error can be up to quantity * 0.5 * 10^(-precision). # We use a slightly more lenient tolerance to account for floating point arithmetic. unit_rounding_error = 0.5 * (10 ** (-precision)) tolerance = quantity * unit_rounding_error * 1.1 # 10% margin for floating point # Verify the property: recomputed price_subtotal should match user input # within the configured decimal precision accounting for cumulative rounding difference = abs(recomputed_subtotal - user_input_subtotal) assert difference <= tolerance, \ f"Round-trip accuracy failed: user input={user_input_subtotal:.{precision}f}, " \ f"price_unit={line.price_unit:.{precision}f}, " \ f"recomputed={recomputed_subtotal:.{precision}f}, " \ f"difference={difference:.{precision+2}f}" print(f"✓ Test passed: input={user_input_subtotal:.2f}, price_unit={line.price_unit:.2f}, recomputed={recomputed_subtotal:.2f}, diff={difference:.4f}") if __name__ == '__main__': print("Running Property Tests for vendor-bill-editable-totals") print("=" * 70) all_passed = True # Test 1: Price subtotal to unit price calculation print("\nProperty Test 1: Price subtotal to unit price calculation") print("-" * 70) try: test_property_price_subtotal_to_unit_price() print("✓ Property Test 1 passed!") except Exception as e: print(f"✗ Property Test 1 failed: {e}") all_passed = False # Test 2: Quantity invariance print("\n" + "=" * 70) print("\nProperty Test 2: Quantity invariance during price_subtotal modification") print("-" * 70) try: test_property_quantity_invariance() print("✓ Property Test 2 passed!") except Exception as e: print(f"✗ Property Test 2 failed: {e}") all_passed = False # Test 3: Price subtotal round-trip accuracy print("\n" + "=" * 70) print("\nProperty Test 3: Price subtotal round-trip accuracy") print("-" * 70) try: test_property_price_subtotal_round_trip() print("✓ Property Test 3 passed!") except Exception as e: print(f"✗ Property Test 3 failed: {e}") all_passed = False print("\n" + "=" * 70) if all_passed: print("✓ All property tests passed!") sys.exit(0) else: print("✗ Some property tests failed!") sys.exit(1)