test(fusion_accounting_followup): Hypothesis property-based invariants
Made-with: Cursor
This commit is contained in:
@@ -14,3 +14,4 @@ from . import test_followup_controller
|
||||
from . import test_followup_adapter
|
||||
from . import test_followup_tools
|
||||
from . import test_followup_cron
|
||||
from . import test_engine_property
|
||||
|
||||
92
fusion_accounting_followup/tests/test_engine_property.py
Normal file
92
fusion_accounting_followup/tests/test_engine_property.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Property-based invariants for follow-up services."""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from hypothesis import given, settings, strategies as st, HealthCheck
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.fusion_accounting_followup.services.overdue_aging import (
|
||||
compute_aging, BUCKETS,
|
||||
)
|
||||
from odoo.addons.fusion_accounting_followup.services.risk_scorer import score_partner
|
||||
from odoo.addons.fusion_accounting_followup.services.tone_selector import select_tone
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'property_based')
|
||||
class TestAgingInvariants(TransactionCase):
|
||||
|
||||
@given(
|
||||
as_of=st.dates(min_value=date(2020, 1, 1), max_value=date(2030, 12, 31)),
|
||||
amounts=st.lists(
|
||||
st.tuples(
|
||||
st.integers(min_value=-180, max_value=180),
|
||||
st.floats(min_value=0.01, max_value=100000,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
),
|
||||
min_size=0, max_size=20,
|
||||
),
|
||||
)
|
||||
@settings(max_examples=80, deadline=2000,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture])
|
||||
def test_buckets_sum_equals_total(self, as_of, amounts):
|
||||
lines = [
|
||||
{'date_maturity': as_of + timedelta(days=offset),
|
||||
'amount_residual': round(amt, 2)}
|
||||
for offset, amt in amounts
|
||||
]
|
||||
report = compute_aging(move_lines=lines, as_of=as_of)
|
||||
bucket_sum = sum(b.amount for b in report.buckets)
|
||||
self.assertAlmostEqual(bucket_sum, report.total_amount, places=1)
|
||||
|
||||
@given(
|
||||
as_of=st.dates(min_value=date(2020, 1, 1), max_value=date(2030, 12, 31)),
|
||||
days_overdue=st.integers(min_value=1, max_value=365),
|
||||
amount=st.floats(min_value=0.01, max_value=10000,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
)
|
||||
@settings(max_examples=50, deadline=2000,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture])
|
||||
def test_overdue_amount_excludes_current(self, as_of, days_overdue, amount):
|
||||
lines = [
|
||||
{'date_maturity': as_of - timedelta(days=days_overdue),
|
||||
'amount_residual': round(amount, 2)},
|
||||
{'date_maturity': as_of + timedelta(days=10),
|
||||
'amount_residual': 100.0},
|
||||
]
|
||||
report = compute_aging(move_lines=lines, as_of=as_of)
|
||||
self.assertAlmostEqual(report.total_overdue_amount, round(amount, 2), places=1)
|
||||
|
||||
@given(
|
||||
invoices=st.integers(min_value=0, max_value=100),
|
||||
late=st.integers(min_value=0, max_value=100),
|
||||
days_late=st.floats(min_value=0, max_value=180,
|
||||
allow_nan=False, allow_infinity=False),
|
||||
)
|
||||
@settings(max_examples=80, deadline=2000,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture])
|
||||
def test_risk_score_in_range(self, invoices, late, days_late):
|
||||
late = min(late, invoices) if invoices > 0 else 0
|
||||
result = score_partner(
|
||||
total_invoices=invoices, paid_late_count=late,
|
||||
avg_days_late=days_late,
|
||||
longest_overdue_days=int(days_late),
|
||||
open_overdue_amount=invoices * 1000.0,
|
||||
average_invoice_amount=1000.0,
|
||||
)
|
||||
self.assertGreaterEqual(result.score, 0)
|
||||
self.assertLessEqual(result.score, 100)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'property_based')
|
||||
class TestToneInvariants(TransactionCase):
|
||||
|
||||
@given(
|
||||
sequence=st.integers(min_value=1, max_value=10),
|
||||
risk=st.integers(min_value=0, max_value=100),
|
||||
)
|
||||
@settings(max_examples=50, deadline=1000,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture])
|
||||
def test_tone_always_in_valid_set(self, sequence, risk):
|
||||
tone = select_tone(level_sequence=sequence, risk_score=risk)
|
||||
self.assertIn(tone, ('gentle', 'firm', 'legal'))
|
||||
Reference in New Issue
Block a user