We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Still provide payment url for orders if order is free but age restricted
### Is your feature request related to a problem? Please describe.
Free orders don't need payment, so the API doesn't provide a payment url for free orders. However, age restricted orders still need member identification. For that, the API should still provide the URL that sets the payer.

### Describe the solution you'd like
Provide the payment url if the order is free but age restricted. In such cases, The sales order payment view should display a succes message 'you correctly identified yourself' or something like that.

### Motivation
Scanning a QR code for identification is nice

### Describe alternatives you've considered
We could also provide an identification url, which could make more sense as the url isn't exactly used for payments, but that would also make our API unnecessarily more complex as we can just re-use the existing field and view for it.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `website/sales/views.py`
Content:
```
1 from django.contrib import messages
2 from django.contrib.auth.decorators import login_required
3 from django.shortcuts import get_object_or_404, redirect, render
4 from django.utils.decorators import method_decorator
5 from django.utils.translation import gettext_lazy as _
6 from django.views import View
7 
8 from sales import services
9 from sales.models.order import Order
10 
11 
12 @method_decorator(login_required, name="dispatch")
13 class OrderPaymentView(View):
14     def get(self, request, *args, **kwargs):
15         order = get_object_or_404(Order, pk=kwargs["pk"])
16         if order.payment:
17             messages.warning(request, _("This order was already paid for."))
18             return redirect("index")
19         if order.payer is not None and order.payer != request.member:
20             messages.warning(request, _("This order is not yours."))
21             return redirect("index")
22 
23         order.payer = request.member
24         order.save()
25 
26         if order.total_amount == 0:
27             messages.warning(request, _("This order doesn't require payment."))
28             return redirect("index")
29 
30         if order.age_restricted and not services.is_adult(request.member):
31             messages.error(
32                 request,
33                 _(
34                     "The age restrictions on this order do not allow you to pay for this order."
35                 ),
36             )
37             return redirect("index")
38         return render(request, "sales/order_payment.html", {"order": order})
39
```

Path: `website/sales/models/order.py`
Content:
```
1 from decimal import Decimal
2 
3 from django.conf import settings
4 from django.core.exceptions import ValidationError
5 from django.core.validators import MinValueValidator
6 from django.db import models
7 from django.db.models import (
8     Sum,
9     Value,
10     F,
11     DecimalField,
12     Q,
13     IntegerField,
14     BooleanField,
15     Count,
16 )
17 from django.db.models.functions import Coalesce
18 from django.urls import reverse
19 from django.utils import timezone
20 from django.utils.translation import gettext_lazy as _
21 from queryable_properties.managers import QueryablePropertiesManager
22 from queryable_properties.properties import AnnotationProperty
23 
24 from members.models import uuid, Member
25 from payments.models import Payment
26 from sales.models.product import ProductListItem
27 from sales.models.shift import Shift
28 
29 
30 def default_order_shift():
31     return Shift.objects.filter(active=True).first()
32 
33 
34 class Order(models.Model):
35 
36     objects = QueryablePropertiesManager()
37 
38     class Meta:
39         verbose_name = _("order")
40         verbose_name_plural = _("orders")
41         permissions = [
42             ("custom_prices", _("Can use custom prices and discounts in orders")),
43         ]
44         ordering = ["created_at"]
45 
46     id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
47 
48     created_at = models.DateTimeField(
49         verbose_name=_("created at"), default=timezone.now
50     )
51 
52     shift = models.ForeignKey(
53         Shift,
54         verbose_name=_("shift"),
55         related_name="orders",
56         default=default_order_shift,
57         null=False,
58         blank=False,
59         on_delete=models.PROTECT,
60     )
61 
62     items = models.ManyToManyField(
63         ProductListItem, through="OrderItem", verbose_name=_("items"),
64     )
65 
66     payment = models.OneToOneField(
67         Payment,
68         verbose_name=_("payment"),
69         related_name="sales_order",
70         on_delete=models.SET_NULL,
71         blank=True,
72         null=True,
73     )
74 
75     discount = models.DecimalField(
76         verbose_name=_("discount"),
77         max_digits=6,
78         decimal_places=2,
79         null=True,
80         blank=True,
81         validators=[MinValueValidator(Decimal("0.00"))],
82     )
83 
84     payer = models.ForeignKey(
85         Member,
86         models.SET_NULL,
87         verbose_name=_("payer"),
88         related_name="sales_order",
89         blank=True,
90         null=True,
91     )
92 
93     age_restricted = AnnotationProperty(
94         Count(
95             "order_items__pk",
96             filter=Q(order_items__product__product__age_restricted=True),
97             output_field=BooleanField(),
98         )
99     )
100 
101     subtotal = AnnotationProperty(
102         Coalesce(Sum("order_items__total"), Value(0.00), output_field=DecimalField())
103     )
104 
105     total_amount = AnnotationProperty(
106         Coalesce(Sum("order_items__total"), Value(0.00), output_field=DecimalField())
107         - Coalesce(F("discount"), Value(0.00), output_field=DecimalField())
108     )
109 
110     num_items = AnnotationProperty(
111         Coalesce(Sum("order_items__amount"), Value(0), output_field=IntegerField())
112     )
113 
114     def save(
115         self, force_insert=False, force_update=False, using=None, update_fields=None
116     ):
117         if self.shift.locked:
118             raise ValueError("The shift this order belongs to is locked.")
119         if self.shift.start > timezone.now():
120             raise ValueError("The shift hasn't started yet.")
121         if (
122             self.payment
123             and self.subtotal - Decimal(self.discount or 0) != self.payment.amount
124         ):
125             # We cannot use self.total_amount as it is a requires a database query and hence will not use any updated values
126             raise ValueError(
127                 "The payment amount does not match the order total amount."
128             )
129         if self.payment and not self.payer:
130             self.payer = self.payment.paid_by
131 
132         return super(Order, self).save(force_insert, force_update, using, update_fields)
133 
134     def clean(self):
135         super().clean()
136         errors = {}
137 
138         if self.shift.start > timezone.now():
139             errors.update({"shift": _("The shift hasn't started yet.")})
140 
141         if self.shift.locked:
142             errors.update({"shift": _("The shift this order belongs to is locked.")})
143 
144         if self.discount and self.discount > self.total_amount:
145             errors.update(
146                 {"discount": _("Discount cannot be higher than total amount.")}
147             )
148 
149         if errors:
150             raise ValidationError(errors)
151 
152     @property
153     def order_description(self):
154         return ", ".join(str(x) for x in self.order_items.all())
155 
156     @property
157     def accept_payment_from_any_user(self):
158         return True
159 
160     @property
161     def payment_url(self):
162         return (
163             settings.BASE_URL + reverse("sales:order-pay", kwargs={"pk": self.pk})
164             if not self.payment
165             and (self.total_amount is not None and self.total_amount != 0)
166             else None
167         )
168 
169     def __str__(self):
170         return f"Order {self.id} ({self.shift})"
171 
172 
173 class OrderItem(models.Model):
174     class Meta:
175         verbose_name = "item"
176         verbose_name_plural = "items"
177         ordering = ["pk"]
178         indexes = [
179             models.Index(fields=["order"]),
180         ]
181 
182     product = models.ForeignKey(
183         ProductListItem,
184         verbose_name=_("product"),
185         null=False,
186         blank=False,
187         on_delete=models.PROTECT,
188     )
189     order = models.ForeignKey(
190         Order,
191         verbose_name=_("order"),
192         related_name="order_items",
193         null=False,
194         blank=False,
195         on_delete=models.CASCADE,
196     )
197     total = models.DecimalField(
198         verbose_name=_("total"),
199         max_digits=6,
200         decimal_places=2,
201         null=False,
202         blank=True,
203         validators=[MinValueValidator(Decimal("0.00"))],
204         help_text="Only when overriding the default",
205     )
206     amount = models.PositiveSmallIntegerField(
207         verbose_name=_("amount"), null=False, blank=False
208     )
209 
210     def save(
211         self, force_insert=False, force_update=False, using=None, update_fields=None
212     ):
213         if self.order.shift.locked:
214             raise ValueError("The shift this order belongs to is locked.")
215         if self.order.payment:
216             raise ValueError("This order has already been paid for.")
217 
218         if not self.total:
219             self.total = self.product.price * self.amount
220 
221         return super(OrderItem, self).save(
222             force_insert, force_update, using, update_fields
223         )
224 
225     def clean(self):
226         super().clean()
227         errors = {}
228 
229         if self.order.shift.locked:
230             errors.update({"order": _("The shift is locked.")})
231 
232         if self.product not in self.order.shift.product_list.product_items.all():
233             errors.update({"product": _("This product is not available.")})
234 
235         if errors:
236             raise ValidationError(errors)
237 
238     def __str__(self):
239         return f"{self.amount}x {self.product.product.name}"
240
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/website/sales/models/order.py b/website/sales/models/order.py
--- a/website/sales/models/order.py
+++ b/website/sales/models/order.py
@@ -162,7 +162,6 @@
         return (
             settings.BASE_URL + reverse("sales:order-pay", kwargs={"pk": self.pk})
             if not self.payment
-            and (self.total_amount is not None and self.total_amount != 0)
             else None
         )
 
diff --git a/website/sales/views.py b/website/sales/views.py
--- a/website/sales/views.py
+++ b/website/sales/views.py
@@ -23,10 +23,6 @@
         order.payer = request.member
         order.save()
 
-        if order.total_amount == 0:
-            messages.warning(request, _("This order doesn't require payment."))
-            return redirect("index")
-
         if order.age_restricted and not services.is_adult(request.member):
             messages.error(
                 request,
@@ -35,4 +31,19 @@
                 ),
             )
             return redirect("index")
+
+        if (
+            order.age_restricted
+            and services.is_adult(request.member)
+            and order.total_amount == 0
+        ):
+            messages.success(
+                request, _("You have successfully identified yourself for this order.")
+            )
+            return redirect("index")
+
+        if order.total_amount == 0:
+            messages.info(request, _("This order doesn't require payment."))
+            return redirect("index")
+
         return render(request, "sales/order_payment.html", {"order": order})

{"golden_diff": "diff --git a/website/sales/models/order.py b/website/sales/models/order.py\n--- a/website/sales/models/order.py\n+++ b/website/sales/models/order.py\n@@ -162,7 +162,6 @@\n         return (\n             settings.BASE_URL + reverse(\"sales:order-pay\", kwargs={\"pk\": self.pk})\n             if not self.payment\n-            and (self.total_amount is not None and self.total_amount != 0)\n             else None\n         )\n \ndiff --git a/website/sales/views.py b/website/sales/views.py\n--- a/website/sales/views.py\n+++ b/website/sales/views.py\n@@ -23,10 +23,6 @@\n         order.payer = request.member\n         order.save()\n \n-        if order.total_amount == 0:\n-            messages.warning(request, _(\"This order doesn't require payment.\"))\n-            return redirect(\"index\")\n-\n         if order.age_restricted and not services.is_adult(request.member):\n             messages.error(\n                 request,\n@@ -35,4 +31,19 @@\n                 ),\n             )\n             return redirect(\"index\")\n+\n+        if (\n+            order.age_restricted\n+            and services.is_adult(request.member)\n+            and order.total_amount == 0\n+        ):\n+            messages.success(\n+                request, _(\"You have successfully identified yourself for this order.\")\n+            )\n+            return redirect(\"index\")\n+\n+        if order.total_amount == 0:\n+            messages.info(request, _(\"This order doesn't require payment.\"))\n+            return redirect(\"index\")\n+\n         return render(request, \"sales/order_payment.html\", {\"order\": order})\n", "issue": "Still provide payment url for orders if order is free but age restricted\n### Is your feature request related to a problem? Please describe.\r\nFree orders don't need payment, so the API doesn't provide a payment url for free orders. However, age restricted orders still need member identification. For that, the API should still provide the URL that sets the payer.\r\n\r\n### Describe the solution you'd like\r\nProvide the payment url if the order is free but age restricted. In such cases, The sales order payment view should display a succes message 'you correctly identified yourself' or something like that.\r\n\r\n### Motivation\r\nScanning a QR code for identification is nice\r\n\r\n### Describe alternatives you've considered\r\nWe could also provide an identification url, which could make more sense as the url isn't exactly used for payments, but that would also make our API unnecessarily more complex as we can just re-use the existing field and view for it.\r\n\n", "before_files": [{"content": "from django.contrib import messages\nfrom django.contrib.auth.decorators import login_required\nfrom django.shortcuts import get_object_or_404, redirect, render\nfrom django.utils.decorators import method_decorator\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views import View\n\nfrom sales import services\nfrom sales.models.order import Order\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass OrderPaymentView(View):\n    def get(self, request, *args, **kwargs):\n        order = get_object_or_404(Order, pk=kwargs[\"pk\"])\n        if order.payment:\n            messages.warning(request, _(\"This order was already paid for.\"))\n            return redirect(\"index\")\n        if order.payer is not None and order.payer != request.member:\n            messages.warning(request, _(\"This order is not yours.\"))\n            return redirect(\"index\")\n\n        order.payer = request.member\n        order.save()\n\n        if order.total_amount == 0:\n            messages.warning(request, _(\"This order doesn't require payment.\"))\n            return redirect(\"index\")\n\n        if order.age_restricted and not services.is_adult(request.member):\n            messages.error(\n                request,\n                _(\n                    \"The age restrictions on this order do not allow you to pay for this order.\"\n                ),\n            )\n            return redirect(\"index\")\n        return render(request, \"sales/order_payment.html\", {\"order\": order})\n", "path": "website/sales/views.py"}, {"content": "from decimal import Decimal\n\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django.core.validators import MinValueValidator\nfrom django.db import models\nfrom django.db.models import (\n    Sum,\n    Value,\n    F,\n    DecimalField,\n    Q,\n    IntegerField,\n    BooleanField,\n    Count,\n)\nfrom django.db.models.functions import Coalesce\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\nfrom queryable_properties.managers import QueryablePropertiesManager\nfrom queryable_properties.properties import AnnotationProperty\n\nfrom members.models import uuid, Member\nfrom payments.models import Payment\nfrom sales.models.product import ProductListItem\nfrom sales.models.shift import Shift\n\n\ndef default_order_shift():\n    return Shift.objects.filter(active=True).first()\n\n\nclass Order(models.Model):\n\n    objects = QueryablePropertiesManager()\n\n    class Meta:\n        verbose_name = _(\"order\")\n        verbose_name_plural = _(\"orders\")\n        permissions = [\n            (\"custom_prices\", _(\"Can use custom prices and discounts in orders\")),\n        ]\n        ordering = [\"created_at\"]\n\n    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)\n\n    created_at = models.DateTimeField(\n        verbose_name=_(\"created at\"), default=timezone.now\n    )\n\n    shift = models.ForeignKey(\n        Shift,\n        verbose_name=_(\"shift\"),\n        related_name=\"orders\",\n        default=default_order_shift,\n        null=False,\n        blank=False,\n        on_delete=models.PROTECT,\n    )\n\n    items = models.ManyToManyField(\n        ProductListItem, through=\"OrderItem\", verbose_name=_(\"items\"),\n    )\n\n    payment = models.OneToOneField(\n        Payment,\n        verbose_name=_(\"payment\"),\n        related_name=\"sales_order\",\n        on_delete=models.SET_NULL,\n        blank=True,\n        null=True,\n    )\n\n    discount = models.DecimalField(\n        verbose_name=_(\"discount\"),\n        max_digits=6,\n        decimal_places=2,\n        null=True,\n        blank=True,\n        validators=[MinValueValidator(Decimal(\"0.00\"))],\n    )\n\n    payer = models.ForeignKey(\n        Member,\n        models.SET_NULL,\n        verbose_name=_(\"payer\"),\n        related_name=\"sales_order\",\n        blank=True,\n        null=True,\n    )\n\n    age_restricted = AnnotationProperty(\n        Count(\n            \"order_items__pk\",\n            filter=Q(order_items__product__product__age_restricted=True),\n            output_field=BooleanField(),\n        )\n    )\n\n    subtotal = AnnotationProperty(\n        Coalesce(Sum(\"order_items__total\"), Value(0.00), output_field=DecimalField())\n    )\n\n    total_amount = AnnotationProperty(\n        Coalesce(Sum(\"order_items__total\"), Value(0.00), output_field=DecimalField())\n        - Coalesce(F(\"discount\"), Value(0.00), output_field=DecimalField())\n    )\n\n    num_items = AnnotationProperty(\n        Coalesce(Sum(\"order_items__amount\"), Value(0), output_field=IntegerField())\n    )\n\n    def save(\n        self, force_insert=False, force_update=False, using=None, update_fields=None\n    ):\n        if self.shift.locked:\n            raise ValueError(\"The shift this order belongs to is locked.\")\n        if self.shift.start > timezone.now():\n            raise ValueError(\"The shift hasn't started yet.\")\n        if (\n            self.payment\n            and self.subtotal - Decimal(self.discount or 0) != self.payment.amount\n        ):\n            # We cannot use self.total_amount as it is a requires a database query and hence will not use any updated values\n            raise ValueError(\n                \"The payment amount does not match the order total amount.\"\n            )\n        if self.payment and not self.payer:\n            self.payer = self.payment.paid_by\n\n        return super(Order, self).save(force_insert, force_update, using, update_fields)\n\n    def clean(self):\n        super().clean()\n        errors = {}\n\n        if self.shift.start > timezone.now():\n            errors.update({\"shift\": _(\"The shift hasn't started yet.\")})\n\n        if self.shift.locked:\n            errors.update({\"shift\": _(\"The shift this order belongs to is locked.\")})\n\n        if self.discount and self.discount > self.total_amount:\n            errors.update(\n                {\"discount\": _(\"Discount cannot be higher than total amount.\")}\n            )\n\n        if errors:\n            raise ValidationError(errors)\n\n    @property\n    def order_description(self):\n        return \", \".join(str(x) for x in self.order_items.all())\n\n    @property\n    def accept_payment_from_any_user(self):\n        return True\n\n    @property\n    def payment_url(self):\n        return (\n            settings.BASE_URL + reverse(\"sales:order-pay\", kwargs={\"pk\": self.pk})\n            if not self.payment\n            and (self.total_amount is not None and self.total_amount != 0)\n            else None\n        )\n\n    def __str__(self):\n        return f\"Order {self.id} ({self.shift})\"\n\n\nclass OrderItem(models.Model):\n    class Meta:\n        verbose_name = \"item\"\n        verbose_name_plural = \"items\"\n        ordering = [\"pk\"]\n        indexes = [\n            models.Index(fields=[\"order\"]),\n        ]\n\n    product = models.ForeignKey(\n        ProductListItem,\n        verbose_name=_(\"product\"),\n        null=False,\n        blank=False,\n        on_delete=models.PROTECT,\n    )\n    order = models.ForeignKey(\n        Order,\n        verbose_name=_(\"order\"),\n        related_name=\"order_items\",\n        null=False,\n        blank=False,\n        on_delete=models.CASCADE,\n    )\n    total = models.DecimalField(\n        verbose_name=_(\"total\"),\n        max_digits=6,\n        decimal_places=2,\n        null=False,\n        blank=True,\n        validators=[MinValueValidator(Decimal(\"0.00\"))],\n        help_text=\"Only when overriding the default\",\n    )\n    amount = models.PositiveSmallIntegerField(\n        verbose_name=_(\"amount\"), null=False, blank=False\n    )\n\n    def save(\n        self, force_insert=False, force_update=False, using=None, update_fields=None\n    ):\n        if self.order.shift.locked:\n            raise ValueError(\"The shift this order belongs to is locked.\")\n        if self.order.payment:\n            raise ValueError(\"This order has already been paid for.\")\n\n        if not self.total:\n            self.total = self.product.price * self.amount\n\n        return super(OrderItem, self).save(\n            force_insert, force_update, using, update_fields\n        )\n\n    def clean(self):\n        super().clean()\n        errors = {}\n\n        if self.order.shift.locked:\n            errors.update({\"order\": _(\"The shift is locked.\")})\n\n        if self.product not in self.order.shift.product_list.product_items.all():\n            errors.update({\"product\": _(\"This product is not available.\")})\n\n        if errors:\n            raise ValidationError(errors)\n\n    def __str__(self):\n        return f\"{self.amount}x {self.product.product.name}\"\n", "path": "website/sales/models/order.py"}], "after_files": [{"content": "from django.contrib import messages\nfrom django.contrib.auth.decorators import login_required\nfrom django.shortcuts import get_object_or_404, redirect, render\nfrom django.utils.decorators import method_decorator\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views import View\n\nfrom sales import services\nfrom sales.models.order import Order\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass OrderPaymentView(View):\n    def get(self, request, *args, **kwargs):\n        order = get_object_or_404(Order, pk=kwargs[\"pk\"])\n        if order.payment:\n            messages.warning(request, _(\"This order was already paid for.\"))\n            return redirect(\"index\")\n        if order.payer is not None and order.payer != request.member:\n            messages.warning(request, _(\"This order is not yours.\"))\n            return redirect(\"index\")\n\n        order.payer = request.member\n        order.save()\n\n        if order.age_restricted and not services.is_adult(request.member):\n            messages.error(\n                request,\n                _(\n                    \"The age restrictions on this order do not allow you to pay for this order.\"\n                ),\n            )\n            return redirect(\"index\")\n\n        if (\n            order.age_restricted\n            and services.is_adult(request.member)\n            and order.total_amount == 0\n        ):\n            messages.success(\n                request, _(\"You have successfully identified yourself for this order.\")\n            )\n            return redirect(\"index\")\n\n        if order.total_amount == 0:\n            messages.info(request, _(\"This order doesn't require payment.\"))\n            return redirect(\"index\")\n\n        return render(request, \"sales/order_payment.html\", {\"order\": order})\n", "path": "website/sales/views.py"}, {"content": "from decimal import Decimal\n\nfrom django.conf import settings\nfrom django.core.exceptions import ValidationError\nfrom django.core.validators import MinValueValidator\nfrom django.db import models\nfrom django.db.models import (\n    Sum,\n    Value,\n    F,\n    DecimalField,\n    Q,\n    IntegerField,\n    BooleanField,\n    Count,\n)\nfrom django.db.models.functions import Coalesce\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\nfrom queryable_properties.managers import QueryablePropertiesManager\nfrom queryable_properties.properties import AnnotationProperty\n\nfrom members.models import uuid, Member\nfrom payments.models import Payment\nfrom sales.models.product import ProductListItem\nfrom sales.models.shift import Shift\n\n\ndef default_order_shift():\n    return Shift.objects.filter(active=True).first()\n\n\nclass Order(models.Model):\n\n    objects = QueryablePropertiesManager()\n\n    class Meta:\n        verbose_name = _(\"order\")\n        verbose_name_plural = _(\"orders\")\n        permissions = [\n            (\"custom_prices\", _(\"Can use custom prices and discounts in orders\")),\n        ]\n        ordering = [\"created_at\"]\n\n    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)\n\n    created_at = models.DateTimeField(\n        verbose_name=_(\"created at\"), default=timezone.now\n    )\n\n    shift = models.ForeignKey(\n        Shift,\n        verbose_name=_(\"shift\"),\n        related_name=\"orders\",\n        default=default_order_shift,\n        null=False,\n        blank=False,\n        on_delete=models.PROTECT,\n    )\n\n    items = models.ManyToManyField(\n        ProductListItem, through=\"OrderItem\", verbose_name=_(\"items\"),\n    )\n\n    payment = models.OneToOneField(\n        Payment,\n        verbose_name=_(\"payment\"),\n        related_name=\"sales_order\",\n        on_delete=models.SET_NULL,\n        blank=True,\n        null=True,\n    )\n\n    discount = models.DecimalField(\n        verbose_name=_(\"discount\"),\n        max_digits=6,\n        decimal_places=2,\n        null=True,\n        blank=True,\n        validators=[MinValueValidator(Decimal(\"0.00\"))],\n    )\n\n    payer = models.ForeignKey(\n        Member,\n        models.SET_NULL,\n        verbose_name=_(\"payer\"),\n        related_name=\"sales_order\",\n        blank=True,\n        null=True,\n    )\n\n    age_restricted = AnnotationProperty(\n        Count(\n            \"order_items__pk\",\n            filter=Q(order_items__product__product__age_restricted=True),\n            output_field=BooleanField(),\n        )\n    )\n\n    subtotal = AnnotationProperty(\n        Coalesce(Sum(\"order_items__total\"), Value(0.00), output_field=DecimalField())\n    )\n\n    total_amount = AnnotationProperty(\n        Coalesce(Sum(\"order_items__total\"), Value(0.00), output_field=DecimalField())\n        - Coalesce(F(\"discount\"), Value(0.00), output_field=DecimalField())\n    )\n\n    num_items = AnnotationProperty(\n        Coalesce(Sum(\"order_items__amount\"), Value(0), output_field=IntegerField())\n    )\n\n    def save(\n        self, force_insert=False, force_update=False, using=None, update_fields=None\n    ):\n        if self.shift.locked:\n            raise ValueError(\"The shift this order belongs to is locked.\")\n        if self.shift.start > timezone.now():\n            raise ValueError(\"The shift hasn't started yet.\")\n        if (\n            self.payment\n            and self.subtotal - Decimal(self.discount or 0) != self.payment.amount\n        ):\n            # We cannot use self.total_amount as it is a requires a database query and hence will not use any updated values\n            raise ValueError(\n                \"The payment amount does not match the order total amount.\"\n            )\n        if self.payment and not self.payer:\n            self.payer = self.payment.paid_by\n\n        return super(Order, self).save(force_insert, force_update, using, update_fields)\n\n    def clean(self):\n        super().clean()\n        errors = {}\n\n        if self.shift.start > timezone.now():\n            errors.update({\"shift\": _(\"The shift hasn't started yet.\")})\n\n        if self.shift.locked:\n            errors.update({\"shift\": _(\"The shift this order belongs to is locked.\")})\n\n        if self.discount and self.discount > self.total_amount:\n            errors.update(\n                {\"discount\": _(\"Discount cannot be higher than total amount.\")}\n            )\n\n        if errors:\n            raise ValidationError(errors)\n\n    @property\n    def order_description(self):\n        return \", \".join(str(x) for x in self.order_items.all())\n\n    @property\n    def accept_payment_from_any_user(self):\n        return True\n\n    @property\n    def payment_url(self):\n        return (\n            settings.BASE_URL + reverse(\"sales:order-pay\", kwargs={\"pk\": self.pk})\n            if not self.payment\n            else None\n        )\n\n    def __str__(self):\n        return f\"Order {self.id} ({self.shift})\"\n\n\nclass OrderItem(models.Model):\n    class Meta:\n        verbose_name = \"item\"\n        verbose_name_plural = \"items\"\n        ordering = [\"pk\"]\n        indexes = [\n            models.Index(fields=[\"order\"]),\n        ]\n\n    product = models.ForeignKey(\n        ProductListItem,\n        verbose_name=_(\"product\"),\n        null=False,\n        blank=False,\n        on_delete=models.PROTECT,\n    )\n    order = models.ForeignKey(\n        Order,\n        verbose_name=_(\"order\"),\n        related_name=\"order_items\",\n        null=False,\n        blank=False,\n        on_delete=models.CASCADE,\n    )\n    total = models.DecimalField(\n        verbose_name=_(\"total\"),\n        max_digits=6,\n        decimal_places=2,\n        null=False,\n        blank=True,\n        validators=[MinValueValidator(Decimal(\"0.00\"))],\n        help_text=\"Only when overriding the default\",\n    )\n    amount = models.PositiveSmallIntegerField(\n        verbose_name=_(\"amount\"), null=False, blank=False\n    )\n\n    def save(\n        self, force_insert=False, force_update=False, using=None, update_fields=None\n    ):\n        if self.order.shift.locked:\n            raise ValueError(\"The shift this order belongs to is locked.\")\n        if self.order.payment:\n            raise ValueError(\"This order has already been paid for.\")\n\n        if not self.total:\n            self.total = self.product.price * self.amount\n\n        return super(OrderItem, self).save(\n            force_insert, force_update, using, update_fields\n        )\n\n    def clean(self):\n        super().clean()\n        errors = {}\n\n        if self.order.shift.locked:\n            errors.update({\"order\": _(\"The shift is locked.\")})\n\n        if self.product not in self.order.shift.product_list.product_items.all():\n            errors.update({\"product\": _(\"This product is not available.\")})\n\n        if errors:\n            raise ValidationError(errors)\n\n    def __str__(self):\n        return f\"{self.amount}x {self.product.product.name}\"\n", "path": "website/sales/models/order.py"}]}