167 lines
6.9 KiB
Python
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}"
|
|
)
|