Coverage for website/sales/models/shift.py: 85.54%

71 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.db import models 

3from django.db.models import Count, Q, Sum 

4from django.db.models.expressions import Value 

5from django.db.models.functions import Coalesce 

6from django.utils import timezone 

7from django.utils.translation import gettext_lazy as _ 

8 

9from queryable_properties.managers import QueryablePropertiesManager 

10from queryable_properties.properties import AggregateProperty, RangeCheckProperty 

11 

12from activemembers.models import MemberGroup 

13from payments.models import PaymentAmountField 

14from sales.models.product import ProductList 

15 

16 

17class Shift(models.Model): 

18 class Meta: 

19 permissions = [ 

20 ("override_manager", _("Can access all shifts as manager")), 

21 ] 

22 

23 objects = QueryablePropertiesManager() 

24 

25 start = models.DateTimeField( 

26 verbose_name=_("start"), 

27 blank=False, 

28 null=False, 

29 ) 

30 end = models.DateTimeField( 

31 verbose_name=_("end"), 

32 blank=False, 

33 null=False, 

34 help_text=_( 

35 "The end time is only indicative and does not prevent orders being created after the shift has ended. This only happens after locking the shift." 

36 ), 

37 ) 

38 

39 title = models.CharField( 

40 verbose_name=_("title"), blank=True, null=True, max_length=100 

41 ) 

42 

43 product_list = models.ForeignKey( 

44 ProductList, 

45 verbose_name=_("product list"), 

46 blank=False, 

47 null=False, 

48 on_delete=models.PROTECT, 

49 ) 

50 

51 managers = models.ManyToManyField( 

52 MemberGroup, verbose_name=_("managers"), related_name="manager_shifts" 

53 ) 

54 

55 locked = models.BooleanField( 

56 verbose_name=_("locked"), 

57 blank=False, 

58 null=False, 

59 default=False, 

60 help_text=_( 

61 "Prevent orders being changed or created for this shift. This will also clean up all unpaid orders in this shift." 

62 ), 

63 ) 

64 

65 def clean(self): 

66 super().clean() 

67 errors = {} 

68 

69 if self.pk is not None and self.orders.filter(created_at__lt=self.start): 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true

70 errors.update( 

71 { 

72 "start": _( 

73 "There are already orders created in this shift before this start time." 

74 ) 

75 } 

76 ) 

77 

78 if self.end and self.start and self.end <= self.start: 78 ↛ 81line 78 didn't jump to line 81 because the condition on line 78 was always true

79 errors.update({"end": _("End cannot be before start.")}) 

80 

81 if errors: 81 ↛ exitline 81 didn't return from function 'clean' because the condition on line 81 was always true

82 raise ValidationError(errors) 

83 

84 def save( 

85 self, force_insert=False, force_update=False, using=None, update_fields=None 

86 ): 

87 if self.locked: 

88 self.orders.filter( 

89 (Q(payment__isnull=True) & Q(total_amount__gt=0)) 

90 | Q(order_items__isnull=True) 

91 ).delete() 

92 

93 return super().save(force_insert, force_update, using, update_fields) 

94 

95 active = RangeCheckProperty("start", "end", timezone.now) 

96 

97 total_revenue = AggregateProperty( 

98 Sum( 

99 Coalesce("orders___total_amount", Value(0.00)), 

100 output_field=PaymentAmountField(allow_zero=True), 

101 ) 

102 ) 

103 

104 total_revenue_paid = AggregateProperty( 

105 Sum( 

106 Coalesce("orders__payment__amount", Value(0.00)), 

107 output_field=PaymentAmountField(allow_zero=True), 

108 ) 

109 ) 

110 

111 num_orders = AggregateProperty( 

112 Count( 

113 "orders", 

114 ) 

115 ) 

116 

117 num_orders_paid = AggregateProperty( 

118 Count( 

119 "orders", 

120 filter=Q(orders___is_free=True) 

121 | Q( 

122 orders__payment__isnull=False, # or the order is free 

123 ), 

124 ) 

125 ) 

126 

127 @property 

128 def product_sales(self): 

129 qs = ( 

130 self.orders.exclude(order_items__isnull=True) 

131 .values("order_items__product") 

132 .annotate(sold=Sum("order_items__amount")) 

133 .order_by() 

134 ) 

135 return { 

136 item[0]: item[1] 

137 for item in qs.values_list("order_items__product__product__name", "sold") 

138 } 

139 

140 @property 

141 def payment_method_sales(self): 

142 qs = ( 

143 self.orders.values("payment__type") 

144 .annotate(sold=Sum("order_items__total")) 

145 .order_by() 

146 ) 

147 return {item[0]: item[1] for item in qs.values_list("payment__type", "sold")} 

148 

149 @property 

150 def user_orders_allowed(self): 

151 return self.selforderperiod_set.filter( 

152 start__lte=timezone.now(), end__gt=timezone.now() 

153 ).exists() 

154 

155 @property 

156 def user_order_period(self): 

157 qs = self.selforderperiod_set.filter( 

158 start__lte=timezone.now(), end__gt=timezone.now() 

159 ) 

160 if qs.exists(): 

161 return qs.first() 

162 return None 

163 

164 def __str__(self): 

165 if self.title and self.title != "": 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true

166 return f"Shift {self.pk} - {self.title}" 

167 return f"Shift {self.pk}" 

168 

169 

170class SelfOrderPeriod(models.Model): 

171 class Meta: 

172 verbose_name = _("self-order period") 

173 verbose_name_plural = _("self-order periods") 

174 ordering = ["start"] 

175 

176 shift = models.ForeignKey(Shift, blank=False, null=False, on_delete=models.CASCADE) 

177 start = models.DateTimeField( 

178 verbose_name=_("start"), 

179 blank=False, 

180 null=False, 

181 ) 

182 end = models.DateTimeField( 

183 verbose_name=_("end"), 

184 blank=False, 

185 null=False, 

186 help_text=_( 

187 "After this moment, users cannot place orders themselves anymore in this shift." 

188 ), 

189 ) 

190 

191 def __str__(self): 

192 return f"Self-order period for shift {self.shift.pk}"