Implémentation 3DSv2 + résolution bugs eboutic + amélioration pages admin (#558)

Eboutic :
- Implémentation de la norme 3DSecure v2 pour les paiement par carte bancaire
- Amélioration générale de l'interface utilisateur
- Résolution du problème avec les caractères spéciaux dans le panier sur Safari
- Réparation du cookie du panier de l'eboutic qui n'était pas fonctionnel

Autre :
- Mise à jour de la documentation
- Mise à jour des dépendances Javascript
- Suppression du code inutilisé dans `subscription/models.py`
- Amélioration des pages administrateur (back-office Django)

Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>
Co-authored-by: Théo DURR <git@theodurr.fr>
Co-authored-by: Julien Constant <julienconstant190@gmail.com>
This commit is contained in:
thomas girod
2023-01-09 20:53:12 +01:00
committed by GitHub
parent 310f1a2283
commit 73305c0b28
53 changed files with 3461 additions and 2248 deletions

View File

@ -21,27 +21,123 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from haystack.admin import SearchModelAdmin
from counter.models import *
@admin.register(Product)
class ProductAdmin(SearchModelAdmin):
search_fields = ["name", "code"]
list_display = (
"name",
"code",
"product_type",
"selling_price",
"profit",
"archived",
)
search_fields = ("name", "code")
@admin.register(Customer)
class CustomerAdmin(SearchModelAdmin):
search_fields = ["account_id"]
list_display = ("user", "account_id", "amount")
search_fields = (
"account_id",
"user__username",
"user__first_name",
"user__last_name",
)
form = make_ajax_form(Customer, {"user": "users"})
admin.site.register(Customer, CustomerAdmin)
admin.site.register(Product, ProductAdmin)
admin.site.register(ProductType)
admin.site.register(Counter)
admin.site.register(Refilling)
admin.site.register(Selling)
admin.site.register(Permanency)
admin.site.register(CashRegisterSummary)
admin.site.register(Eticket)
@admin.register(BillingInfo)
class BillingInfoAdmin(admin.ModelAdmin):
list_display = ("first_name", "last_name", "address_1", "city", "country")
@admin.register(Counter)
class CounterAdmin(admin.ModelAdmin):
list_display = ("name", "club", "type")
form = make_ajax_form(
Counter,
{
"products": "products",
"sellers": "users",
},
)
@admin.register(Refilling)
class RefillingAdmin(SearchModelAdmin):
list_display = ("customer", "amount", "counter", "payment_method", "date")
search_fields = (
"customer__user__username",
"customer__user__first_name",
"customer__user__last_name",
"customer__account_id",
"counter__name",
)
form = make_ajax_form(
Refilling,
{
"customer": "customers",
"operator": "users",
},
)
@admin.register(Selling)
class SellingAdmin(SearchModelAdmin):
list_display = ("customer", "label", "unit_price", "quantity", "counter", "date")
search_fields = (
"customer__user__username",
"customer__user__first_name",
"customer__user__last_name",
"customer__account_id",
"counter__name",
)
form = make_ajax_form(
Selling,
{
"customer": "customers",
"seller": "users",
},
)
@admin.register(Permanency)
class PermanencyAdmin(SearchModelAdmin):
list_display = ("user", "counter", "start", "duration")
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"counter__name",
)
form = make_ajax_form(Permanency, {"user": "users"})
@admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
list_display = ("name", "priority")
@admin.register(CashRegisterSummary)
class CashRegisterSummaryAdmin(SearchModelAdmin):
list_display = ("user", "counter", "date")
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"counter__name",
)
form = make_ajax_form(CashRegisterSummary, {"user": "users"})
@admin.register(Eticket)
class EticketAdmin(SearchModelAdmin):
list_display = ("product", "event_date", "event_title")
search_fields = ("product__name", "event_title")

177
counter/forms.py Normal file
View File

@ -0,0 +1,177 @@
from ajax_select import make_ajax_field
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from django import forms
from django.utils.translation import gettext_lazy as _
from core.views.forms import TzAwareDateTimeField, SelectDate
from counter.models import (
BillingInfo,
StudentCard,
Customer,
Refilling,
Counter,
Product,
Eticket,
)
class BillingInfoForm(forms.ModelForm):
class Meta:
model = BillingInfo
exclude = ["customer"]
class StudentCardForm(forms.ModelForm):
"""
Form for adding student cards
Only used for user profile since CounterClick is to complicated
"""
class Meta:
model = StudentCard
fields = ["uid"]
def clean(self):
cleaned_data = super(StudentCardForm, self).clean()
uid = cleaned_data.get("uid", None)
if not uid or not StudentCard.is_valid(uid):
raise forms.ValidationError(_("This UID is invalid"), code="invalid")
return cleaned_data
class GetUserForm(forms.Form):
"""
The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
reverse function, or any other use.
The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
some nickname, first name, or last name (TODO)
"""
code = forms.CharField(
label="Code", max_length=StudentCard.UID_SIZE, required=False
)
id = AutoCompleteSelectField(
"users", required=False, label=_("Select user"), help_text=None
)
def as_p(self):
self.fields["code"].widget.attrs["autofocus"] = True
return super(GetUserForm, self).as_p()
def clean(self):
cleaned_data = super(GetUserForm, self).clean()
cus = None
if cleaned_data["code"] != "":
if len(cleaned_data["code"]) == StudentCard.UID_SIZE:
card = StudentCard.objects.filter(uid=cleaned_data["code"])
if card.exists():
cus = card.first().customer
if cus is None:
cus = Customer.objects.filter(
account_id__iexact=cleaned_data["code"]
).first()
elif cleaned_data["id"] is not None:
cus = Customer.objects.filter(user=cleaned_data["id"]).first()
if cus is None or not cus.can_buy:
raise forms.ValidationError(_("User not found"))
cleaned_data["user_id"] = cus.user.id
cleaned_data["user"] = cus.user
return cleaned_data
class RefillForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
amount = forms.FloatField(
min_value=0, widget=forms.NumberInput(attrs={"class": "focus"})
)
class Meta:
model = Refilling
fields = ["amount", "payment_method", "bank"]
class CounterEditForm(forms.ModelForm):
class Meta:
model = Counter
fields = ["sellers", "products"]
sellers = make_ajax_field(Counter, "sellers", "users", help_text="")
products = make_ajax_field(Counter, "products", "products", help_text="")
class ProductEditForm(forms.ModelForm):
class Meta:
model = Product
fields = [
"name",
"description",
"product_type",
"code",
"parent_product",
"buying_groups",
"purchase_price",
"selling_price",
"special_selling_price",
"icon",
"club",
"limit_age",
"tray",
"archived",
]
parent_product = AutoCompleteSelectField(
"products", show_help_text=False, label=_("Parent product"), required=False
)
buying_groups = AutoCompleteSelectMultipleField(
"groups",
show_help_text=False,
help_text="",
label=_("Buying groups"),
required=True,
)
club = AutoCompleteSelectField("clubs", show_help_text=False)
counters = AutoCompleteSelectMultipleField(
"counters",
show_help_text=False,
help_text="",
label=_("Counters"),
required=False,
)
def __init__(self, *args, **kwargs):
super(ProductEditForm, self).__init__(*args, **kwargs)
if self.instance.id:
self.fields["counters"].initial = [
str(c.id) for c in self.instance.counters.all()
]
def save(self, *args, **kwargs):
ret = super(ProductEditForm, self).save(*args, **kwargs)
if self.fields["counters"].initial:
for cid in self.fields["counters"].initial:
c = Counter.objects.filter(id=int(cid)).first()
c.products.remove(self.instance)
c.save()
for cid in self.cleaned_data["counters"]:
c = Counter.objects.filter(id=int(cid)).first()
c.products.add(self.instance)
c.save()
return ret
class CashSummaryFormBase(forms.Form):
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
class EticketForm(forms.ModelForm):
class Meta:
model = Eticket
fields = ["product", "banner", "event_title", "event_date"]
widgets = {"event_date": SelectDate}
product = AutoCompleteSelectField(
"products", show_help_text=False, label=_("Product"), required=True
)

View File

@ -0,0 +1,62 @@
# Generated by Django 3.2.16 on 2023-01-08 12:49
from django.db import migrations, models
import django.db.models.deletion
import django_countries.fields
class Migration(migrations.Migration):
dependencies = [
("counter", "0018_producttype_priority"),
]
operations = [
migrations.AlterModelOptions(
name="producttype",
options={"ordering": ["-priority", "name"], "verbose_name": "product type"},
),
migrations.CreateModel(
name="BillingInfo",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"first_name",
models.CharField(max_length=22, verbose_name="First name"),
),
(
"last_name",
models.CharField(max_length=22, verbose_name="Last name"),
),
(
"address_1",
models.CharField(max_length=50, verbose_name="Address 1"),
),
(
"address_2",
models.CharField(
blank=True, max_length=50, null=True, verbose_name="Address 2"
),
),
("zip_code", models.CharField(max_length=16, verbose_name="Zip code")),
("city", models.CharField(max_length=50, verbose_name="City")),
("country", django_countries.fields.CountryField(max_length=2)),
(
"customer",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="billing_infos",
to="counter.customer",
),
),
],
),
]

View File

@ -22,8 +22,8 @@
#
#
from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB
from django.db import models
from django.db.models.functions import Length
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.conf import settings
@ -38,16 +38,20 @@ import string
import os
import base64
import datetime
from dict2xml import dict2xml
from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB
from club.models import Club, Membership
from accounting.models import CurrencyField
from core.models import Group, User, Notification
from subscription.models import Subscription
from django_countries.fields import CountryField
class Customer(models.Model):
"""
This class extends a user to make a customer. It adds some basic customers informations, such as the accound ID, and
This class extends a user to make a customer. It adds some basic customers' information, such as the account ID, and
is used by other accounting classes as reference to the customer, rather than using User
"""
@ -89,13 +93,28 @@ class Customer(models.Model):
.subscription_end
) < timedelta(days=90)
@staticmethod
def generate_account_id(number):
number = str(number)
letter = random.choice(string.ascii_lowercase)
while Customer.objects.filter(account_id=number + letter).exists():
letter = random.choice(string.ascii_lowercase)
return number + letter
@classmethod
def new_for_user(cls, user: User):
"""
Create a new Customer instance for the user given in parameter without saving it
The account if is automatically generated and the amount set at 0
"""
# account_id are number with a letter appended
account_id = (
Customer.objects.order_by(Length("account_id"), "account_id")
.values("account_id")
.last()
)
if account_id is None:
# legacy from the old site
return cls(user=user, account_id="1504a", amount=0)
account_id = account_id["account_id"]
num = int(account_id[:-1])
while Customer.objects.filter(account_id=account_id).exists():
num += 1
account_id = str(num) + random.choice(string.ascii_lowercase)
return cls(user=user, account_id=account_id, amount=0)
def save(self, allow_negative=False, is_selling=False, *args, **kwargs):
"""
@ -122,6 +141,52 @@ class Customer(models.Model):
return "".join(["https://", settings.SITH_URL, self.get_absolute_url()])
class BillingInfo(models.Model):
"""
Represent the billing information of a user, which are required
by the 3D-Secure v2 system used by the etransaction module
"""
customer = models.OneToOneField(
Customer, related_name="billing_infos", on_delete=models.CASCADE
)
# declaring surname and name even though they are already defined
# in User add some redundancy, but ensures that the billing infos
# shall stay correct, whatever shenanigans the user commits on its profile
first_name = models.CharField(_("First name"), max_length=22)
last_name = models.CharField(_("Last name"), max_length=22)
address_1 = models.CharField(_("Address 1"), max_length=50)
address_2 = models.CharField(_("Address 2"), max_length=50, blank=True, null=True)
zip_code = models.CharField(_("Zip code"), max_length=16) # code postal
city = models.CharField(_("City"), max_length=50)
country = CountryField(blank_label=_("Country"))
def to_3dsv2_xml(self) -> str:
"""
Convert the data from this model into a xml usable
by the online paying service of the Crédit Agricole bank.
see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`
"""
data = {
"Address": {
"FirstName": self.first_name,
"LastName": self.last_name,
"Address1": self.address_1,
"ZipCode": self.zip_code,
"City": self.city,
"CountryCode": self.country.numeric, # ISO-3166-1 numeric code
}
}
if self.address_2:
data["Address"]["Address2"] = self.address_2
xml = dict2xml(data, wrap="Billing", newlines=False)
return '<?xml version="1.0" encoding="UTF-8" ?>' + xml
def __str__(self):
return f"{self.first_name} {self.last_name}"
class ProductType(models.Model):
"""
This describes a product type
@ -240,6 +305,10 @@ class Product(models.Model):
return True
return False
@property
def profit(self):
return self.selling_price - self.purchase_price
def __str__(self):
return "%s (%s)" % (self.name, self.code)
@ -697,6 +766,12 @@ class Permanency(models.Model):
self.end.strftime("%Y-%m-%d %H:%M:%S") if self.end else "",
)
@property
def duration(self):
if self.end is None:
return self.activity - self.start
return self.end - self.start
class CashRegisterSummary(models.Model):
user = models.ForeignKey(

View File

@ -21,7 +21,7 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import json
import re
from django.test import TestCase
@ -29,7 +29,7 @@ from django.urls import reverse
from django.core.management import call_command
from core.models import User
from counter.models import Counter
from counter.models import Counter, Customer, BillingInfo
class CounterTest(TestCase):
@ -67,7 +67,7 @@ class CounterTest(TestCase):
response = self.client.get(response.get("location"))
self.assertTrue(">Richard Batsbak</" in str(response.content))
response = self.client.post(
self.client.post(
location,
{
"action": "refill",
@ -76,15 +76,11 @@ class CounterTest(TestCase):
"bank": "OTHER",
},
)
response = self.client.post(location, {"action": "code", "code": "BARB"})
response = self.client.post(
location, {"action": "add_product", "product_id": "4"}
)
response = self.client.post(
location, {"action": "del_product", "product_id": "4"}
)
response = self.client.post(location, {"action": "code", "code": "2xdeco"})
response = self.client.post(location, {"action": "code", "code": "1xbarb"})
self.client.post(location, {"action": "code", "code": "BARB"})
self.client.post(location, {"action": "add_product", "product_id": "4"})
self.client.post(location, {"action": "del_product", "product_id": "4"})
self.client.post(location, {"action": "code", "code": "2xdeco"})
self.client.post(location, {"action": "code", "code": "1xbarb"})
response = self.client.post(location, {"action": "code", "code": "fin"})
response_get = self.client.get(response.get("location"))
@ -96,7 +92,7 @@ class CounterTest(TestCase):
in str(response_content)
)
response = self.client.post(
self.client.post(
reverse("counter:login", kwargs={"counter_id": self.mde.id}),
{"username": self.sli.username, "password": "plop"},
)
@ -154,6 +150,234 @@ class CounterStatsTest(TestCase):
self.assertTrue(response.status_code == 403)
class BillingInfoTest(TestCase):
@classmethod
def setUpClass(cls):
cls.payload_1 = {
"first_name": "Subscribed",
"last_name": "User",
"address_1": "1 rue des Huns",
"zip_code": "90000",
"city": "Belfort",
"country": "FR",
}
cls.payload_2 = {
"first_name": "Subscribed",
"last_name": "User",
"address_1": "3, rue de Troyes",
"zip_code": "34301",
"city": "Sète",
"country": "FR",
}
super().setUpClass()
call_command("populate")
def test_edit_infos(self):
user = User.objects.get(username="subscriber")
BillingInfo.objects.get_or_create(
customer=user.customer, defaults=self.payload_1
)
self.client.login(username=user.username, password="plop")
response = self.client.post(
reverse("counter:edit_billing_info", args=[user.id]),
json.dumps(self.payload_2),
content_type="application/json",
)
user = User.objects.get(username="subscriber")
infos = BillingInfo.objects.get(customer__user=user)
self.assertEqual(200, response.status_code)
self.assertJSONEqual(response.content, {"errors": None})
self.assertTrue(hasattr(user.customer, "billing_infos"))
self.assertEqual(user.customer, infos.customer)
self.assertEqual("Subscribed", infos.first_name)
self.assertEqual("User", infos.last_name)
self.assertEqual("3, rue de Troyes", infos.address_1)
self.assertEqual(None, infos.address_2)
self.assertEqual("34301", infos.zip_code)
self.assertEqual("Sète", infos.city)
self.assertEqual("FR", infos.country)
def test_create_infos_for_user_with_account(self):
user = User.objects.get(username="subscriber")
if hasattr(user.customer, "billing_infos"):
user.customer.billing_infos.delete()
self.client.login(username=user.username, password="plop")
response = self.client.post(
reverse("counter:create_billing_info", args=[user.id]),
json.dumps(self.payload_1),
content_type="application/json",
)
user = User.objects.get(username="subscriber")
infos = BillingInfo.objects.get(customer__user=user)
self.assertEqual(200, response.status_code)
self.assertJSONEqual(response.content, {"errors": None})
self.assertTrue(hasattr(user.customer, "billing_infos"))
self.assertEqual(user.customer, infos.customer)
self.assertEqual("Subscribed", infos.first_name)
self.assertEqual("User", infos.last_name)
self.assertEqual("1 rue des Huns", infos.address_1)
self.assertEqual(None, infos.address_2)
self.assertEqual("90000", infos.zip_code)
self.assertEqual("Belfort", infos.city)
self.assertEqual("FR", infos.country)
def test_create_infos_for_user_without_account(self):
user = User.objects.get(username="subscriber")
if hasattr(user, "customer"):
user.customer.delete()
self.client.login(username=user.username, password="plop")
response = self.client.post(
reverse("counter:create_billing_info", args=[user.id]),
json.dumps(self.payload_1),
content_type="application/json",
)
user = User.objects.get(username="subscriber")
self.assertTrue(hasattr(user, "customer"))
self.assertTrue(hasattr(user.customer, "billing_infos"))
self.assertEqual(200, response.status_code)
self.assertJSONEqual(response.content, {"errors": None})
infos = BillingInfo.objects.get(customer__user=user)
self.assertEqual(user.customer, infos.customer)
self.assertEqual("Subscribed", infos.first_name)
self.assertEqual("User", infos.last_name)
self.assertEqual("1 rue des Huns", infos.address_1)
self.assertEqual(None, infos.address_2)
self.assertEqual("90000", infos.zip_code)
self.assertEqual("Belfort", infos.city)
self.assertEqual("FR", infos.country)
def test_create_invalid(self):
user = User.objects.get(username="subscriber")
if hasattr(user.customer, "billing_infos"):
user.customer.billing_infos.delete()
self.client.login(username=user.username, password="plop")
# address_1, zip_code and country are missing
payload = {
"first_name": user.first_name,
"last_name": user.last_name,
"city": "Belfort",
}
response = self.client.post(
reverse("counter:create_billing_info", args=[user.id]),
json.dumps(payload),
content_type="application/json",
)
user = User.objects.get(username="subscriber")
self.assertEqual(400, response.status_code)
self.assertFalse(hasattr(user.customer, "billing_infos"))
expected_errors = {
"errors": [
{"field": "Adresse 1", "messages": ["Ce champ est obligatoire."]},
{"field": "Code postal", "messages": ["Ce champ est obligatoire."]},
{"field": "Country", "messages": ["Ce champ est obligatoire."]},
]
}
self.assertJSONEqual(response.content, expected_errors)
def test_edit_invalid(self):
user = User.objects.get(username="subscriber")
BillingInfo.objects.get_or_create(
customer=user.customer, defaults=self.payload_1
)
self.client.login(username=user.username, password="plop")
# address_1, zip_code and country are missing
payload = {
"first_name": user.first_name,
"last_name": user.last_name,
"city": "Belfort",
}
response = self.client.post(
reverse("counter:edit_billing_info", args=[user.id]),
json.dumps(payload),
content_type="application/json",
)
user = User.objects.get(username="subscriber")
self.assertEqual(400, response.status_code)
self.assertTrue(hasattr(user.customer, "billing_infos"))
expected_errors = {
"errors": [
{"field": "Adresse 1", "messages": ["Ce champ est obligatoire."]},
{"field": "Code postal", "messages": ["Ce champ est obligatoire."]},
{"field": "Country", "messages": ["Ce champ est obligatoire."]},
]
}
self.assertJSONEqual(response.content, expected_errors)
def test_edit_other_user(self):
user = User.objects.get(username="sli")
self.client.login(username="subscriber", password="plop")
BillingInfo.objects.get_or_create(
customer=user.customer, defaults=self.payload_1
)
response = self.client.post(
reverse("counter:edit_billing_info", args=[user.id]),
json.dumps(self.payload_2),
content_type="application/json",
)
self.assertEqual(403, response.status_code)
def test_edit_not_existing_infos(self):
user = User.objects.get(username="subscriber")
if hasattr(user.customer, "billing_infos"):
user.customer.billing_infos.delete()
self.client.login(username=user.username, password="plop")
response = self.client.post(
reverse("counter:edit_billing_info", args=[user.id]),
json.dumps(self.payload_2),
content_type="application/json",
)
self.assertEqual(404, response.status_code)
def test_edit_by_root(self):
user = User.objects.get(username="subscriber")
BillingInfo.objects.get_or_create(
customer=user.customer, defaults=self.payload_1
)
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("counter:edit_billing_info", args=[user.id]),
json.dumps(self.payload_2),
content_type="application/json",
)
self.assertEqual(200, response.status_code)
user = User.objects.get(username="subscriber")
infos = BillingInfo.objects.get(customer__user=user)
self.assertJSONEqual(response.content, {"errors": None})
self.assertTrue(hasattr(user.customer, "billing_infos"))
self.assertEqual(user.customer, infos.customer)
self.assertEqual("Subscribed", infos.first_name)
self.assertEqual("User", infos.last_name)
self.assertEqual("3, rue de Troyes", infos.address_1)
self.assertEqual(None, infos.address_2)
self.assertEqual("34301", infos.zip_code)
self.assertEqual("Sète", infos.city)
self.assertEqual("FR", infos.country)
def test_create_by_root(self):
user = User.objects.get(username="subscriber")
if hasattr(user.customer, "billing_infos"):
user.customer.billing_infos.delete()
self.client.login(username="root", password="plop")
response = self.client.post(
reverse("counter:create_billing_info", args=[user.id]),
json.dumps(self.payload_2),
content_type="application/json",
)
self.assertEqual(200, response.status_code)
user = User.objects.get(username="subscriber")
infos = BillingInfo.objects.get(customer__user=user)
self.assertJSONEqual(response.content, {"errors": None})
self.assertTrue(hasattr(user.customer, "billing_infos"))
self.assertEqual(user.customer, infos.customer)
self.assertEqual("Subscribed", infos.first_name)
self.assertEqual("User", infos.last_name)
self.assertEqual("3, rue de Troyes", infos.address_1)
self.assertEqual(None, infos.address_2)
self.assertEqual("34301", infos.zip_code)
self.assertEqual("Sète", infos.city)
self.assertEqual("FR", infos.country)
class BarmanConnectionTest(TestCase):
def setUp(self):
call_command("populate")
@ -519,3 +743,20 @@ class StudentCardTest(TestCase):
{"uid": "8B90734A802A8F"},
)
self.assertEqual(response.status_code, 403)
class AccountIdTest(TestCase):
def setUp(self):
user_a = User.objects.create(username="a", password="plop", email="a.a@a.fr")
user_b = User.objects.create(username="b", password="plop", email="b.b@b.fr")
user_c = User.objects.create(username="c", password="plop", email="c.c@c.fr")
Customer.objects.create(user=user_a, amount=0, account_id="1111a")
Customer.objects.create(user=user_b, amount=0, account_id="9999z")
Customer.objects.create(user=user_c, amount=0, account_id="12345f")
def test_create_customer(self):
user_d = User.objects.create(username="d", password="plop")
customer_d = Customer.new_for_user(user_d)
customer_d.save()
number = customer_d.account_id[:-1]
self.assertEqual(number, "12346")

View File

@ -22,7 +22,7 @@
#
#
from django.urls import re_path
from django.urls import re_path, path
from counter.views import *
@ -66,6 +66,16 @@ urlpatterns = [
StudentCardDeleteView.as_view(),
name="delete_student_card",
),
path(
"customer/<int:user_id>/billing_info/create",
create_billing_info,
name="create_billing_info",
),
path(
"customer/<int:user_id>/billing_info/edit",
edit_billing_info,
name="edit_billing_info",
),
re_path(r"^admin/(?P<counter_id>[0-9]+)$", CounterEditView.as_view(), name="admin"),
re_path(
r"^admin/(?P<counter_id>[0-9]+)/prop$",

View File

@ -21,10 +21,13 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import json
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
from django.http import Http404
from django.core.exceptions import PermissionDenied
from django.views.decorators.http import require_POST
from django.views.generic import ListView, DetailView, RedirectView, TemplateView
from django.views.generic.base import View
from django.views.generic.edit import (
@ -49,12 +52,20 @@ import re
import pytz
from datetime import date, timedelta, datetime
from http import HTTPStatus
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from ajax_select import make_ajax_field
from core.views import CanViewMixin, TabedViewMixin, CanEditMixin
from core.views.forms import LoginForm, SelectDate, SelectDateTime
from core.views.forms import LoginForm
from core.models import User
from counter.forms import (
BillingInfoForm,
StudentCardForm,
GetUserForm,
RefillForm,
CounterEditForm,
ProductEditForm,
CashSummaryFormBase,
EticketForm,
)
from subscription.models import Subscription
from counter.models import (
Counter,
@ -68,9 +79,9 @@ from counter.models import (
CashRegisterSummaryItem,
Eticket,
Permanency,
BillingInfo,
)
from accounting.models import CurrencyField
from core.views.forms import TzAwareDateTimeField
class CounterAdminMixin(View):
@ -103,24 +114,6 @@ class CounterAdminMixin(View):
return super(CounterAdminMixin, self).dispatch(request, *args, **kwargs)
class StudentCardForm(forms.ModelForm):
"""
Form for adding student cards
Only used for user profile since CounterClick is to complicated
"""
class Meta:
model = StudentCard
fields = ["uid"]
def clean(self):
cleaned_data = super(StudentCardForm, self).clean()
uid = cleaned_data.get("uid", None)
if not uid or not StudentCard.is_valid(uid):
raise forms.ValidationError(_("This UID is invalid"), code="invalid")
return cleaned_data
class StudentCardDeleteView(DeleteView, CanEditMixin):
"""
View used to delete a card from a user
@ -140,59 +133,6 @@ class StudentCardDeleteView(DeleteView, CanEditMixin):
)
class GetUserForm(forms.Form):
"""
The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
reverse function, or any other use.
The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
some nickname, first name, or last name (TODO)
"""
code = forms.CharField(
label="Code", max_length=StudentCard.UID_SIZE, required=False
)
id = AutoCompleteSelectField(
"users", required=False, label=_("Select user"), help_text=None
)
def as_p(self):
self.fields["code"].widget.attrs["autofocus"] = True
return super(GetUserForm, self).as_p()
def clean(self):
cleaned_data = super(GetUserForm, self).clean()
cus = None
if cleaned_data["code"] != "":
if len(cleaned_data["code"]) == StudentCard.UID_SIZE:
card = StudentCard.objects.filter(uid=cleaned_data["code"])
if card.exists():
cus = card.first().customer
if cus is None:
cus = Customer.objects.filter(
account_id__iexact=cleaned_data["code"]
).first()
elif cleaned_data["id"] is not None:
cus = Customer.objects.filter(user=cleaned_data["id"]).first()
if cus is None or not cus.can_buy:
raise forms.ValidationError(_("User not found"))
cleaned_data["user_id"] = cus.user.id
cleaned_data["user"] = cus.user
return cleaned_data
class RefillForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
amount = forms.FloatField(
min_value=0, widget=forms.NumberInput(attrs={"class": "focus"})
)
class Meta:
model = Refilling
fields = ["amount", "payment_method", "bank"]
class CounterTabsMixin(TabedViewMixin):
def get_tabs_title(self):
if hasattr(self.object, "stock_owner"):
@ -867,15 +807,6 @@ class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
current_tab = "counters"
class CounterEditForm(forms.ModelForm):
class Meta:
model = Counter
fields = ["sellers", "products"]
sellers = make_ajax_field(Counter, "sellers", "users", help_text="")
products = make_ajax_field(Counter, "products", "products", help_text="")
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
"""
Edit a counter's main informations (for the counter's manager)
@ -995,66 +926,6 @@ class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
current_tab = "products"
class ProductEditForm(forms.ModelForm):
class Meta:
model = Product
fields = [
"name",
"description",
"product_type",
"code",
"parent_product",
"buying_groups",
"purchase_price",
"selling_price",
"special_selling_price",
"icon",
"club",
"limit_age",
"tray",
"archived",
]
parent_product = AutoCompleteSelectField(
"products", show_help_text=False, label=_("Parent product"), required=False
)
buying_groups = AutoCompleteSelectMultipleField(
"groups",
show_help_text=False,
help_text="",
label=_("Buying groups"),
required=True,
)
club = AutoCompleteSelectField("clubs", show_help_text=False)
counters = AutoCompleteSelectMultipleField(
"counters",
show_help_text=False,
help_text="",
label=_("Counters"),
required=False,
)
def __init__(self, *args, **kwargs):
super(ProductEditForm, self).__init__(*args, **kwargs)
if self.instance.id:
self.fields["counters"].initial = [
str(c.id) for c in self.instance.counters.all()
]
def save(self, *args, **kwargs):
ret = super(ProductEditForm, self).save(*args, **kwargs)
if self.fields["counters"].initial:
for cid in self.fields["counters"].initial:
c = Counter.objects.filter(id=int(cid)).first()
c.products.remove(self.instance)
c.save()
for cid in self.cleaned_data["counters"]:
c = Counter.objects.filter(id=int(cid)).first()
c.products.add(self.instance)
c.save()
return ret
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
"""
A create view for the admins
@ -1482,7 +1353,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
def get_context_data(self, **kwargs):
"""Add stats to the context"""
from django.db.models import Sum, Case, When, F, DecimalField
from django.db.models import Sum, Case, When, F
kwargs = super(CounterStatView, self).get_context_data(**kwargs)
kwargs["Customer"] = Customer
@ -1585,11 +1456,6 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
return reverse("counter:cash_summary_list")
class CashSummaryFormBase(forms.Form):
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
"""Display a list of cash summaries"""
@ -1669,7 +1535,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
end_date = (start_date + timedelta(days=32)).replace(
day=1, hour=0, minute=0, microsecond=0
)
from django.db.models import Sum, Case, When, F, DecimalField
from django.db.models import Sum, Case, When, F
kwargs["sum_cb"] = sum(
[
@ -1725,17 +1591,6 @@ class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
current_tab = "etickets"
class EticketForm(forms.ModelForm):
class Meta:
model = Eticket
fields = ["product", "banner", "event_title", "event_date"]
widgets = {"event_date": SelectDate}
product = AutoCompleteSelectField(
"products", show_help_text=False, label=_("Product"), required=True
)
class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
"""
Create an eticket
@ -1895,3 +1750,55 @@ class StudentCardFormView(FormView):
return reverse_lazy(
"core:user_prefs", kwargs={"user_id": self.customer.user.pk}
)
def __manage_billing_info_req(request, user_id, delete_if_fail=False):
data = json.loads(request.body)
form = BillingInfoForm(data)
if not form.is_valid():
if delete_if_fail:
Customer.objects.get(user__id=user_id).billing_infos.delete()
errors = [
{"field": str(form.fields[k].label), "messages": v}
for k, v in form.errors.items()
]
content = json.dumps({"errors": errors})
return HttpResponse(status=400, content=content)
if form.is_valid():
infos = Customer.objects.get(user__id=user_id).billing_infos
for field in form.fields:
infos.__dict__[field] = form[field].value()
infos.save()
content = json.dumps({"errors": None})
return HttpResponse(status=200, content=content)
@login_required
@require_POST
def create_billing_info(request, user_id):
user = request.user
if user.id != user_id and not user.has_perm("counter:add_billinginfo"):
raise PermissionDenied()
user = get_object_or_404(User, pk=user_id)
if not hasattr(user, "customer"):
customer = Customer.new_for_user(user)
customer.save()
else:
customer = get_object_or_404(Customer, user_id=user_id)
BillingInfo.objects.create(customer=customer)
return __manage_billing_info_req(request, user_id, True)
@login_required
@require_POST
def edit_billing_info(request, user_id):
user = request.user
if user.id != user_id and not user.has_perm("counter:change_billinginfo"):
raise PermissionDenied()
user = get_object_or_404(User, pk=user_id)
if not hasattr(user, "customer"):
raise Http404
if not hasattr(user.customer, "billing_infos"):
raise Http404
return __manage_billing_info_req(request, user_id)