229 lines
8.4 KiB
Python
229 lines
8.4 KiB
Python
#!/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)
|