vendor_bill_editable_totals/tests/run_property_test.py
2025-11-21 18:02:20 +07:00

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)