Compare commits

...

2 Commits

Author SHA1 Message Date
imperosol 177002b8b8 tweak django-countries settings 2026-06-05 00:43:56 +02:00
imperosol e629b36465 make BillingInfo.phone_number non-nullable 2026-06-05 00:43:56 +02:00
9 changed files with 48 additions and 79 deletions
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
blank=True, blank=True,
help_text=( help_text=(
"If a limit is set, the product won't be purchasable " "If a limit is set, the product won't be purchasable "
"anymore once the latter is reached." "anymore on the eboutic once the latter is reached."
), ),
null=True, null=True,
verbose_name="clic limit", verbose_name="clic limit",
@@ -0,0 +1,26 @@
# Generated by Django 5.2.14 on 2026-06-02 10:45
import django_countries.fields
import phonenumber_field.modelfields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("counter", "0040_product_clic_limit")]
operations = [
migrations.AlterField(
model_name="billinginfo",
name="country",
field=django_countries.fields.CountryField(
max_length=2, verbose_name="Country"
),
),
migrations.AlterField(
model_name="billinginfo",
name="phone_number",
field=phonenumber_field.modelfields.PhoneNumberField(
max_length=128, region=None, verbose_name="Phone number"
),
),
]
+2 -9
View File
@@ -228,15 +228,8 @@ class BillingInfo(models.Model):
address_2 = models.CharField(_("Address 2"), max_length=50, blank=True, null=True) address_2 = models.CharField(_("Address 2"), max_length=50, blank=True, null=True)
zip_code = models.CharField(_("Zip code"), max_length=16) # code postal zip_code = models.CharField(_("Zip code"), max_length=16) # code postal
city = models.CharField(_("City"), max_length=50) city = models.CharField(_("City"), max_length=50)
country = CountryField(blank_label=_("Country")) country = CountryField(_("Country"))
phone_number = PhoneNumberField(_("Phone number"))
# This table was created during the A22 semester.
# However, later on, CA asked for the phone number to be added to the billing info.
# As the table was already created, this new field had to be nullable,
# even tough it is required by the bank and shouldn't be null.
# If one day there is no null phone number remaining,
# please make the field non-nullable.
phone_number = PhoneNumberField(_("Phone number"), null=True, blank=False)
def __str__(self): def __str__(self):
return f"{self.first_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
+1 -30
View File
@@ -16,7 +16,6 @@ from __future__ import annotations
import hmac import hmac
from datetime import datetime from datetime import datetime
from enum import Enum
from typing import Self from typing import Self
from dict2xml import dict2xml from dict2xml import dict2xml
@@ -40,30 +39,6 @@ from counter.models import (
) )
class BillingInfoState(Enum):
VALID = 1
EMPTY = 2
MISSING_PHONE_NUMBER = 3
@classmethod
def from_model(cls, info: BillingInfo | None) -> BillingInfoState:
if info is None:
return cls.EMPTY
for attr in [
"first_name",
"last_name",
"address_1",
"zip_code",
"city",
"country",
]:
if getattr(info, attr) == "":
return cls.EMPTY
if info.phone_number is None:
return cls.MISSING_PHONE_NUMBER
return cls.VALID
class Basket(models.Model): class Basket(models.Model):
"""Basket is built when the user connects to an eboutic page.""" """Basket is built when the user connects to an eboutic page."""
@@ -162,11 +137,7 @@ class Basket(models.Model):
if self.is_expired: if self.is_expired:
raise ValueError("This method cannot be called on an expired basket.") raise ValueError("This method cannot be called on an expired basket.")
customer = user.customer customer = user.customer
if ( if not hasattr(user.customer, "billing_infos"):
not hasattr(user.customer, "billing_infos")
or BillingInfoState.from_model(user.customer.billing_infos)
!= BillingInfoState.VALID
):
raise BillingInfo.DoesNotExist raise BillingInfo.DoesNotExist
cart = { cart = {
"shoppingcart": {"total": {"totalQuantity": min(self.items.count(), 99)}} "shoppingcart": {"total": {"totalQuantity": min(self.items.count(), 99)}}
@@ -24,7 +24,7 @@
x-cloak x-cloak
> >
{% csrf_token %} {% csrf_token %}
{{ form.as_p() }} {{ form }}
<br> <br>
<input <input
type="submit" class="btn btn-blue clickable" type="submit" class="btn btn-blue clickable"
+2 -5
View File
@@ -37,12 +37,9 @@ class TestBillingInfo:
def test_edit_infos(self, client: Client, payload: dict[str, str]): def test_edit_infos(self, client: Client, payload: dict[str, str]):
user = subscriber_user.make() user = subscriber_user.make()
baker.make(BillingInfo, customer=user.customer) baker.make(BillingInfo, customer=user.customer, phone_number="06 01 02 03 04")
client.force_login(user) client.force_login(user)
response = client.post( response = client.post(reverse("eboutic:billing_infos"), payload)
reverse("eboutic:billing_infos"),
payload,
)
user.refresh_from_db() user.refresh_from_db()
infos = BillingInfo.objects.get(customer__user=user) infos = BillingInfo.objects.get(customer__user=user)
assert response.status_code == 302 assert response.status_code == 302
+10 -21
View File
@@ -58,7 +58,7 @@ from counter.models import (
Selling, Selling,
get_eboutic, get_eboutic,
) )
from eboutic.models import Basket, BasketItem, BillingInfoState, Invoice, InvoiceItem from eboutic.models import Basket, BasketItem, Invoice, InvoiceItem
if TYPE_CHECKING: if TYPE_CHECKING:
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
@@ -187,7 +187,7 @@ def payment_result(request, result: str) -> HttpResponse:
class BillingInfoFormFragment( class BillingInfoFormFragment(
LoginRequiredMixin, FragmentMixin, SuccessMessageMixin, UpdateView LoginRequiredMixin, FragmentMixin, SuccessMessageMixin, UpdateView
): ):
"""Update billing info""" """Update or create billing info"""
model = BillingInfo model = BillingInfo
form_class = BillingInfoForm form_class = BillingInfoForm
@@ -218,26 +218,15 @@ class BillingInfoFormFragment(
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["billing_infos_state"] = BillingInfoState.from_model(self.object)
kwargs["action"] = reverse("eboutic:billing_infos") kwargs["action"] = reverse("eboutic:billing_infos")
match BillingInfoState.from_model(self.object): if not self.object:
case BillingInfoState.EMPTY: messages.warning(
messages.warning( self.request,
self.request, _(
_( "You must fill your billing infos "
"You must fill your billing infos if you want to pay with your credit card" "if you want to pay with your credit card"
), ),
) )
case BillingInfoState.MISSING_PHONE_NUMBER:
messages.warning(
self.request,
_(
"The Crédit Agricole changed its policy related to the billing "
+ "information that must be provided in order to pay with a credit card. "
+ "If you want to pay with your credit card, you must add a phone number "
+ "to the data you already provided.",
),
)
return kwargs return kwargs
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
-12
View File
@@ -4511,18 +4511,6 @@ msgstr ""
"Vous devez renseigner vos coordonnées de facturation si vous voulez payer " "Vous devez renseigner vos coordonnées de facturation si vous voulez payer "
"par carte bancaire" "par carte bancaire"
#: eboutic/views.py
msgid ""
"The Crédit Agricole changed its policy related to the billing information "
"that must be provided in order to pay with a credit card. If you want to pay "
"with your credit card, you must add a phone number to the data you already "
"provided."
msgstr ""
"Le Crédit Agricole a changé sa politique relative aux informations à "
"fournir pour effectuer un paiement par carte bancaire. De ce fait, si vous "
"souhaitez payer par carte, vous devez rajouter un numéro de téléphone aux "
"données que vous aviez déjà fourni."
#: eboutic/views.py #: eboutic/views.py
msgid "Basket expired" msgid "Basket expired"
msgstr "Panier expiré" msgstr "Panier expiré"
+5
View File
@@ -115,6 +115,7 @@ INSTALLED_APPS = (
"django_jinja", "django_jinja",
"ninja_extra", "ninja_extra",
"haystack", "haystack",
"django_countries",
"django_celery_results", "django_celery_results",
"django_celery_beat", "django_celery_beat",
"captcha", "captcha",
@@ -294,7 +295,11 @@ USE_TZ = True
LOCALE_PATHS = [BASE_DIR / "locale"] LOCALE_PATHS = [BASE_DIR / "locale"]
# for PhoneNumberField
PHONENUMBER_DEFAULT_REGION = "FR" PHONENUMBER_DEFAULT_REGION = "FR"
# for CountryField
COUNTRIES_FIRST = ["FR", "CH", "DE"]
COUNTRIES_FIRST_BREAK = "───────────"
# Medias # Medias
MEDIA_URL = "/data/" MEDIA_URL = "/data/"