93 lines
3.8 KiB
Python
93 lines
3.8 KiB
Python
"""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'))
|