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

167 lines
6.9 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import TransactionCase
from hypothesis import given, settings, strategies as st
from decimal import Decimal
class TestPriceSubtotalProperty(TransactionCase):
"""
Property-based tests for price_subtotal calculation.
**Feature: vendor-bill-editable-totals, Property 1: Price subtotal to unit price calculation**
"""
def setUp(self):
super().setUp()
# Create a vendor partner
self.vendor = self.env['res.partner'].create({
'name': 'Test Vendor',
'supplier_rank': 1,
})
# Create a product
self.product = self.env['res.product'].create({
'name': 'Test Product',
'type': 'service',
'list_price': 100.0,
})
# Create a vendor bill
self.vendor_bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': self.vendor.id,
'invoice_date': '2024-01-01',
})
# Get decimal precision for Product Price
self.precision = self.env['decimal.precision'].precision_get('Product Price')
@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(self, 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.
"""
# Create an invoice line
line = self.env['account.move.line'].create({
'move_id': self.vendor_bill.id,
'product_id': self.product.id,
'name': 'Test Line',
'quantity': quantity,
'price_unit': 1.0, # Initial value, will be recalculated
})
# 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, self.precision)
# Verify the property: price_unit should equal price_subtotal / quantity
self.assertAlmostEqual(
line.price_unit,
expected_price_unit_rounded,
places=self.precision,
msg=f"Price unit calculation failed: expected {expected_price_unit_rounded}, got {line.price_unit}"
)
@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(self, 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 an invoice line
line = self.env['account.move.line'].create({
'move_id': self.vendor_bill.id,
'product_id': self.product.id,
'name': 'Test Line',
'quantity': quantity,
'price_unit': 1.0, # Initial value, will be recalculated
})
# 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
self.assertEqual(
line.quantity,
initial_quantity,
msg=f"Quantity changed during price_subtotal modification: expected {initial_quantity}, got {line.quantity}"
)
@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(self, 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.
"""
# Create an invoice line
line = self.env['account.move.line'].create({
'move_id': self.vendor_bill.id,
'product_id': self.product.id,
'name': 'Test Line',
'quantity': quantity,
'price_unit': 1.0, # Initial value, will be recalculated
})
# 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
# We allow a small discrepancy due to rounding
tolerance = 10 ** (-self.precision)
# Verify the property: recomputed price_subtotal should match user input
# within the configured decimal precision
self.assertAlmostEqual(
recomputed_subtotal,
user_input_subtotal,
places=self.precision,
msg=f"Round-trip accuracy failed: user input={user_input_subtotal:.{self.precision}f}, "
f"price_unit={line.price_unit:.{self.precision}f}, "
f"recomputed={recomputed_subtotal:.{self.precision}f}, "
f"difference={abs(recomputed_subtotal - user_input_subtotal):.{self.precision+2}f}"
)