AkomptaBackend / api /models.py
rinogeek's picture
V2.1
26c0cd1
from django.db import models
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.core.validators import RegexValidator
from decimal import Decimal
class UserManager(BaseUserManager):
"""Custom user manager for email-based authentication"""
def create_user(self, email, password=None, **extra_fields):
"""Create and save a regular user with the given email and password"""
if not email:
raise ValueError('L\'adresse email est obligatoire')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
"""Create and save a superuser with the given email and password"""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Le superutilisateur doit avoir is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Le superutilisateur doit avoir is_superuser=True.')
return self.create_user(email, password, **extra_fields)
class User(AbstractUser):
"""Modèle utilisateur étendu pour Akompta"""
ACCOUNT_TYPE_CHOICES = [
('personal', 'Personnel'),
('business', 'Professionnel'),
]
# Utiliser email comme identifiant
username = None
email = models.EmailField(unique=True, verbose_name="Email")
# Champs communs
phone_number = models.CharField(max_length=20, blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
account_type = models.CharField(
max_length=10,
choices=ACCOUNT_TYPE_CHOICES,
default='personal'
)
is_premium = models.BooleanField(default=False)
initial_balance = models.DecimalField(max_digits=15, decimal_places=2, default=Decimal('0.00'))
# Champs Business
business_name = models.CharField(max_length=255, blank=True)
sector = models.CharField(max_length=100, blank=True)
location = models.CharField(max_length=255, blank=True)
ifu = models.CharField(
max_length=50,
blank=True,
verbose_name="Identifiant Fiscal Unique"
)
business_logo = models.ImageField(upload_to='business_logos/', blank=True, null=True)
# Settings (Stockés en JSON)
currency = models.CharField(max_length=10, default='XOF')
language = models.CharField(max_length=5, default='FR')
dark_mode = models.BooleanField(default=False)
# Acceptation des conditions
agreed_terms = models.BooleanField(default=False)
business_agreed = models.BooleanField(default=False)
business_agreed_at = models.DateTimeField(blank=True, null=True)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Custom manager
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']
class Meta:
verbose_name = "Utilisateur"
verbose_name_plural = "Utilisateurs"
def __str__(self):
return self.email
def save(self, *args, **kwargs):
# Validation IFU pour les comptes business
if self.account_type == 'business' and not self.ifu:
from django.core.exceptions import ValidationError
raise ValidationError(
"Le champ IFU est obligatoire pour les comptes professionnels."
)
super().save(*args, **kwargs)
class Product(models.Model):
"""Modèle pour les produits/inventaire"""
CATEGORY_CHOICES = [
('vente', 'Vente'),
('depense', 'Dépense'),
('stock', 'Stock'),
]
STOCK_STATUS_CHOICES = [
('ok', 'OK'),
('low', 'Faible'),
('rupture', 'Rupture'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products')
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=15, decimal_places=2)
unit = models.CharField(max_length=50, default='Unité')
image = models.ImageField(upload_to='products/', blank=True, null=True)
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
stock_status = models.CharField(
max_length=10,
choices=STOCK_STATUS_CHOICES,
default='ok'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Produit"
verbose_name_plural = "Produits"
ordering = ['-created_at']
def __str__(self):
return f"{self.name} - {self.user.email}"
class Transaction(models.Model):
"""Modèle pour les transactions financières"""
TYPE_CHOICES = [
('income', 'Revenu'),
('expense', 'Dépense'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transactions')
name = models.CharField(max_length=255)
amount = models.DecimalField(max_digits=15, decimal_places=2)
type = models.CharField(max_length=10, choices=TYPE_CHOICES)
category = models.CharField(max_length=100)
date = models.DateTimeField()
currency = models.CharField(max_length=10, default='FCFA')
# Support pour la synchro hors-ligne
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Transaction"
verbose_name_plural = "Transactions"
ordering = ['-date']
indexes = [
models.Index(fields=['user', 'type']),
models.Index(fields=['user', 'date']),
models.Index(fields=['user', 'category']),
]
def __str__(self):
return f"{self.name} - {self.amount} {self.currency}"
class Budget(models.Model):
"""Modèle pour les budgets suivis"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='budgets')
category = models.CharField(max_length=100)
limit = models.DecimalField(max_digits=15, decimal_places=2)
color = models.CharField(max_length=7, default='#4F46E5') # Hex color
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Budget"
verbose_name_plural = "Budgets"
unique_together = ['user', 'category']
def __str__(self):
return f"{self.category} - {self.limit}"
def get_spent_amount(self):
"""Calcule le montant dépensé pour cette catégorie"""
from django.db.models import Sum
result = self.user.transactions.filter(
type='expense',
category=self.category
).aggregate(total=Sum('amount'))
return result['total'] or Decimal('0.00')
class Ad(models.Model):
"""Modèle pour les annonces partenaires"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ads')
product_name = models.CharField(max_length=255)
owner_name = models.CharField(max_length=255)
description = models.TextField()
image = models.ImageField(upload_to='ads/')
whatsapp = models.CharField(max_length=20)
website = models.URLField(blank=True, null=True)
location = models.CharField(max_length=255)
is_verified = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Annonce"
verbose_name_plural = "Annonces"
ordering = ['-created_at']
def __str__(self):
return f"{self.product_name} - {self.owner_name}"
class Notification(models.Model):
"""Modèle pour les notifications"""
TYPE_CHOICES = [
('reminder', 'Rappel'),
('profit', 'Profit'),
('promo', 'Promo'),
('system', 'Système'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
type = models.CharField(max_length=20, choices=TYPE_CHOICES, default='system')
title = models.CharField(max_length=255)
message = models.TextField()
is_read = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Notification"
verbose_name_plural = "Notifications"
ordering = ['-created_at']
def __str__(self):
return f"{self.title} - {self.user.email}"
class SupportTicket(models.Model):
"""Modèle pour le support client"""
STATUS_CHOICES = [
('open', 'Ouvert'),
('in_progress', 'En cours'),
('closed', 'Fermé'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='support_tickets')
subject = models.CharField(max_length=255)
message = models.TextField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Ticket Support"
verbose_name_plural = "Tickets Support"
ordering = ['-created_at']
def __str__(self):
return f"{self.subject} - {self.status}"
class AIInsight(models.Model):
"""Modèle pour stocker les insights générés par l'IA"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ai_insights')
content = models.JSONField() # Liste de phrases
context_hash = models.CharField(max_length=64) # Hash des données utilisées pour la génération
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Insight IA"
verbose_name_plural = "Insights IA"
ordering = ['-created_at']
def __str__(self):
return f"Insight pour {self.user.email} - {self.created_at}"