Coverage for website/reimbursements/models.py: 89.13%

38 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1from django.core.exceptions import ValidationError 

2from django.core.validators import ( 

3 FileExtensionValidator, 

4 MinLengthValidator, 

5) 

6from django.db import models 

7from django.utils import timezone 

8 

9from payments.models import PaymentAmountField 

10from utils.media.services import get_upload_to_function 

11from utils.validators import RangeValueValidator 

12 

13 

14def validate_file_size(file): 

15 max_size_mb = 10 

16 if file.size > max_size_mb * 1000 * 1000: 

17 raise ValidationError(f"File size must be less than {max_size_mb} MB") 

18 

19 

20class Reimbursement(models.Model): 

21 class Verdict(models.TextChoices): 

22 APPROVED = "approved", "Approved" 

23 DENIED = "denied", "Denied" 

24 

25 owner = models.ForeignKey( 

26 "members.Member", 

27 related_name="reimbursements", 

28 on_delete=models.PROTECT, 

29 ) 

30 

31 amount = PaymentAmountField( 

32 max_digits=5, 

33 decimal_places=2, 

34 help_text="How much did you pay (in euros)?", 

35 allow_zero=True, # To make sure no double error msg is shown 

36 validators=[RangeValueValidator(lower=0, lower_inclusive=False)], 

37 ) 

38 

39 date_incurred = models.DateField( 

40 help_text="When was this payment made?", 

41 ) 

42 

43 description = models.TextField( 

44 max_length=512, 

45 help_text="Why did you make this payment?", 

46 validators=[MinLengthValidator(10)], 

47 ) 

48 

49 # FileField and not ImageField because companies often send invoices as pdf 

50 receipt = models.FileField( 

51 upload_to=get_upload_to_function("reimbursements/receipts"), 

52 validators=[ 

53 FileExtensionValidator( 

54 allowed_extensions=["pdf", "jpg", "jpeg", "png"], 

55 message="Only pdf, jpg, jpeg and png files are allowed.", 

56 ), 

57 validate_file_size, 

58 ], 

59 ) 

60 

61 created = models.DateTimeField(auto_now_add=True) 

62 

63 verdict = models.CharField( 

64 max_length=40, 

65 choices=Verdict.choices, 

66 null=True, 

67 blank=True, 

68 ) 

69 

70 verdict_clarification = models.TextField( 

71 help_text="Why did you choose this verdict?", 

72 null=True, 

73 blank=True, 

74 ) 

75 

76 evaluated_at = models.DateTimeField(null=True, editable=False) 

77 

78 evaluated_by = models.ForeignKey( 

79 "auth.User", 

80 related_name="reimbursements_approved", 

81 on_delete=models.SET_NULL, 

82 editable=False, 

83 null=True, 

84 ) 

85 

86 class Meta: 

87 ordering = ["created"] 

88 

89 def clean(self): 

90 super().clean() 

91 

92 errors = {} 

93 

94 if ( 

95 self.date_incurred is not None 

96 and self.date_incurred > timezone.now().date() 

97 ): 

98 errors["date_incurred"] = "The date incurred cannot be in the future." 

99 

100 if self.verdict == self.Verdict.DENIED and not self.verdict_clarification: 

101 errors["verdict_clarification"] = ( 

102 "You must provide a reason for the denial." 

103 ) 

104 

105 if errors: 

106 raise ValidationError(errors) 

107 

108 def __str__(self): 

109 return f"Reimbursement #{self.id}"