import uuid from django.conf import settings from django.core.files.storage import default_storage from django.db import models from django.utils.translation import gettext_lazy as _ from .validators import validate_receipt_file class Project(models.Model): name = models.CharField(max_length=255) code = models.CharField(max_length=50, blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["name"] def __str__(self): if self.code: return f"{self.code} – {self.name}" return self.name class Claim(models.Model): class Status(models.TextChoices): PENDING = "pending", _("Pending") APPROVED = "approved", _("Approved") REJECTED = "rejected", _("Rejected") class Currency(models.TextChoices): SEK = "SEK", _("Swedish krona (SEK)") EUR = "EUR", _("Euro (EUR)") USD = "USD", _("US dollar (USD)") GBP = "GBP", _("British pound (GBP)") submitted_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="claims_submitted", ) full_name = models.CharField(max_length=255) email = models.EmailField() amount = models.DecimalField(max_digits=10, decimal_places=2) currency = models.CharField( max_length=3, choices=Currency.choices, default=Currency.SEK, ) description = models.TextField(help_text=_("Describe what the reimbursement is for")) account_number = models.CharField(max_length=50) receipt = models.FileField( upload_to="receipts/", blank=True, null=True, validators=[validate_receipt_file], ) project = models.ForeignKey( Project, null=True, blank=True, on_delete=models.SET_NULL, related_name="claims", ) status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING) decision_note = models.TextField(blank=True) paid_at = models.DateTimeField(null=True, blank=True) paid_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="claims_marked_paid", ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-created_at"] def __str__(self): project = f" [{self.project}]" if self.project else "" return f"{self.full_name} – {self.amount} {self.currency}{project} ({self.get_status_display()})" @property def is_paid(self): return self.paid_at is not None def add_log(self, *, action, performed_by=None, from_status=None, to_status=None, note=""): return ClaimLog.objects.create( claim=self, action=action, from_status=from_status, to_status=to_status or self.status, note=note or "", performed_by=performed_by, ) def save(self, *args, **kwargs): if self.receipt and not kwargs.pop("skip_receipt_rename", False): original_name = self.receipt.name or "" ext = original_name.rsplit(".", 1)[-1] if "." in original_name else "dat" new_name = self._generate_unique_receipt_name(ext) self.receipt.name = new_name super().save(*args, **kwargs) def _generate_unique_receipt_name(self, ext): ext = ext.lower() for _ in range(10): candidate = f"receipts/{uuid.uuid4().hex}.{ext}" if not default_storage.exists(candidate): return candidate return f"receipts/{uuid.uuid4().hex}.{ext}" class ClaimLog(models.Model): class Action(models.TextChoices): CREATED = "created", _("Submitted") STATUS_CHANGED = "status_changed", _("Status changed") MARKED_PAID = "marked_paid", _("Marked as paid") PROJECT_CHANGED = "project_changed", _("Project changed") DETAILS_EDITED = "details_edited", _("Details edited") claim = models.ForeignKey(Claim, related_name="logs", on_delete=models.CASCADE) action = models.CharField(max_length=32, choices=Action.choices) from_status = models.CharField( max_length=20, choices=Claim.Status.choices, null=True, blank=True, ) to_status = models.CharField(max_length=20, choices=Claim.Status.choices) note = models.TextField(blank=True) performed_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="claim_logs", ) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at"] def __str__(self): return f"{self.get_action_display()} ({self.created_at:%Y-%m-%d %H:%M})"