Spaces:
Running
Running
| 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}" | |