Spaces:
Sleeping
Sleeping
File size: 6,711 Bytes
833c388 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from django.test import TestCase
from django.utils import timezone
from .models import (
Transaction,
User,
SyscohadaCRMappingRule,
SyscohadaBilanBalance,
)
from .syscohada_reports import compute_compte_resultat, generate_bilan_csv, generate_compte_resultat_csv
from .serializers import TransactionSerializer
class SyscohadaComputationTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email="syscohada@test.com",
password="pass12345",
first_name="Sys",
last_name="Coha",
account_type="personal",
initial_balance=Decimal("1000.00"),
agreed_terms=True,
)
def _dt(self, year: int, month: int, day: int):
return timezone.make_aware(datetime(year, month, day, 12, 0, 0))
def test_compte_resultat_marks_unmapped_transactions(self):
# Year N
tx_income = Transaction.objects.create(
user=self.user,
name="Vente marchandises",
amount=Decimal("5000.00"),
type="income",
category="Ventes",
date=self._dt(2026, 2, 2),
currency="XOF",
)
tx_transport = Transaction.objects.create(
user=self.user,
name="Taxi",
amount=Decimal("200.00"),
type="expense",
category="Transport",
date=self._dt(2026, 2, 3),
currency="XOF",
)
tx_unknown = Transaction.objects.create(
user=self.user,
name="Dépense inconnue",
amount=Decimal("50.00"),
type="expense",
category="Inconnu",
date=self._dt(2026, 2, 4),
currency="XOF",
)
compte = compute_compte_resultat(self.user, 2026)
# TA should include the income (default mapping sees "ventes")
self.assertGreaterEqual(compte.values_n.get("TA", Decimal("0")), Decimal("5000.00"))
# RG should include transport
self.assertGreaterEqual(compte.values_n.get("RG", Decimal("0")), Decimal("200.00"))
# Unknown should be marked as unmapped (and goes to RJ fallback)
self.assertIn(tx_unknown.id, compte.unmapped_tx_ids_n)
self.assertGreaterEqual(compte.values_n.get("RJ", Decimal("0")), Decimal("50.00"))
# If we add an explicit rule, it should stop being "unmapped"
SyscohadaCRMappingRule.objects.create(
user=self.user,
ref="RH",
tx_type="expense",
category_pattern="Inconnu",
match_mode="contains",
priority=1,
is_active=True,
)
compte2 = compute_compte_resultat(self.user, 2026)
self.assertNotIn(tx_unknown.id, compte2.unmapped_tx_ids_n)
self.assertGreaterEqual(compte2.values_n.get("RH", Decimal("0")), Decimal("50.00"))
def test_bilan_can_balance_with_user_inputs(self):
# Create a simple year with net profit
Transaction.objects.create(
user=self.user,
name="Vente",
amount=Decimal("1000.00"),
type="income",
category="Ventes",
date=self._dt(2026, 1, 10),
currency="XOF",
)
Transaction.objects.create(
user=self.user,
name="Transport",
amount=Decimal("100.00"),
type="expense",
category="Transport",
date=self._dt(2026, 1, 11),
currency="XOF",
)
compte = compute_compte_resultat(self.user, 2026)
cash = self.user.initial_balance + compte.total_income_n - compte.total_expense_n
resultat = compte.resultat_net_n
# Provide capital so that: CA + CJ == BS (minimal bilan)
SyscohadaBilanBalance.objects.create(
user=self.user,
year=2026,
section="PASSIF",
ref="CA",
net=(cash - resultat),
note="Capital (test)",
)
bilan_csv = generate_bilan_csv(self.user, compte).decode("utf-8")
# Sanity: contains key refs
self.assertIn("ACTIF,BS", bilan_csv)
self.assertIn("PASSIF,CA", bilan_csv)
self.assertIn("PASSIF,CJ", bilan_csv)
def test_exports_generate_csv_bytes(self):
Transaction.objects.create(
user=self.user,
name="Vente",
amount=Decimal("100.00"),
type="income",
category="Ventes",
date=self._dt(2026, 3, 1),
currency="XOF",
)
compte = compute_compte_resultat(self.user, 2026)
cr_csv = generate_compte_resultat_csv(compte)
bilan_csv = generate_bilan_csv(self.user, compte)
self.assertIsInstance(cr_csv, (bytes, bytearray))
self.assertIsInstance(bilan_csv, (bytes, bytearray))
self.assertGreater(len(cr_csv), 50)
self.assertGreater(len(bilan_csv), 50)
def test_uses_created_at_when_transaction_date_is_way_off(self):
"""
If client sends a wrong `date` (ex: device clock in 2024) but the server-side
`created_at` is in 2026, SYSCOHADA calculations should include it in 2026.
"""
tx = Transaction.objects.create(
user=self.user,
name="Vente (bad date)",
amount=Decimal("100.00"),
type="income",
category="Ventes",
date=self._dt(2024, 3, 16), # wrong
currency="XOF",
)
# Force created_at to 2026 (simulate real server record time)
Transaction.objects.filter(id=tx.id).update(created_at=self._dt(2026, 5, 17))
compte = compute_compte_resultat(self.user, 2026)
self.assertGreaterEqual(compte.values_n.get("TA", Decimal("0")), Decimal("100.00"))
def test_transaction_serializer_clamps_bad_client_dates(self):
# Date far away (client clock wrong) should be clamped to "now" by default.
payload = {
"name": "Bad date tx",
"amount": "100.00",
"type": "income",
"category": "Ventes",
"currency": "XOF",
"date": self._dt(2024, 3, 16).isoformat().replace("+00:00", "Z"),
}
class _Req:
query_params = {}
user = self.user
serializer = TransactionSerializer(data=payload, context={"request": _Req()})
self.assertTrue(serializer.is_valid(), serializer.errors)
value = serializer.validated_data["date"]
now = timezone.now()
self.assertLessEqual(abs((value - now).days), 1)
|