fix: Calcul du montant total des tarif par RF + affichage des tarifs

actifs lors de la création d'un RF [#26]
This commit is contained in:
N3WT DE COMPET
2025-02-07 20:36:02 +01:00
parent f2628bb45a
commit c269b89d3d
12 changed files with 192 additions and 70 deletions

View File

@ -8,27 +8,6 @@ from School.models import SchoolClass, Fee, Discount
from datetime import datetime
class RegistrationFee(models.Model):
"""
Représente un tarif ou frais dinscription avec différentes options de paiement.
"""
class PaymentOptions(models.IntegerChoices):
SINGLE_PAYMENT = 0, _('Paiement en une seule fois')
MONTHLY_PAYMENT = 1, _('Paiement mensuel')
QUARTERLY_PAYMENT = 2, _('Paiement trimestriel')
name = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=True)
base_amount = models.DecimalField(max_digits=10, decimal_places=2)
discounts = models.JSONField(blank=True, null=True)
supplements = models.JSONField(blank=True, null=True)
validity_start_date = models.DateField()
validity_end_date = models.DateField()
payment_option = models.IntegerField(choices=PaymentOptions, default=PaymentOptions.SINGLE_PAYMENT)
def __str__(self):
return self.name
class Language(models.Model):
"""
Représente une langue parlée par lélève.

View File

@ -1,6 +1,6 @@
from rest_framework import serializers
from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationFee
from School.models import SchoolClass, Fee, Discount
from .models import RegistrationFileTemplate, RegistrationFile, RegistrationForm, Student, Guardian, Sibling, Language
from School.models import SchoolClass, Fee, Discount, FeeType
from School.serializers import FeeSerializer, DiscountSerializer
from Auth.models import Profile
from Auth.serializers import ProfileSerializer
@ -11,12 +11,6 @@ from django.utils import timezone
import pytz
from datetime import datetime
class RegistrationFeeSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
class Meta:
model = RegistrationFee
fields = '__all__'
class RegistrationFileSerializer(serializers.ModelSerializer):
class Meta:
model = RegistrationFile
@ -136,6 +130,8 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
registration_files = RegistrationFileSerializer(many=True, required=False)
fees = serializers.PrimaryKeyRelatedField(queryset=Fee.objects.all(), many=True, required=False)
discounts = serializers.PrimaryKeyRelatedField(queryset=Discount.objects.all(), many=True, required=False)
totalRegistrationFees = serializers.SerializerMethodField()
totalTuitionFees = serializers.SerializerMethodField()
class Meta:
model = RegistrationForm
@ -184,6 +180,14 @@ class RegistrationFormSerializer(serializers.ModelSerializer):
return local_time.strftime("%d-%m-%Y %H:%M")
def get_totalRegistrationFees(self, obj):
for fee in obj.fees.filter(type=FeeType.REGISTRATION_FEE):
print(fee.base_amount)
return sum(fee.base_amount for fee in obj.fees.filter(type=FeeType.REGISTRATION_FEE))
def get_totalTuitionFees(self, obj):
return sum(fee.base_amount for fee in obj.fees.filter(type=FeeType.TUITION_FEE))
class StudentByParentSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)

View File

@ -1,7 +1,7 @@
from django.urls import path, re_path
from . import views
from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFeeView, RegistrationFileView
from .views import RegistrationFileTemplateView, RegisterFormListView, RegisterFormView, StudentView, GuardianView, ChildrenListView, StudentListView, RegistrationFileView
urlpatterns = [
re_path(r'^registerForms/(?P<_filter>[a-zA-z]+)$', RegisterFormListView.as_view(), name="registerForms"),
@ -30,9 +30,6 @@ urlpatterns = [
# Page INSCRIPTION - Liste des élèves
re_path(r'^students$', StudentListView.as_view(), name="students"),
# Frais d'inscription
re_path(r'^registrationFees$', RegistrationFeeView.as_view(), name="registrationFees"),
# modèles de fichiers d'inscription
re_path(r'^registrationFileTemplates$', RegistrationFileTemplateView.as_view(), name='registrationFileTemplates'),
re_path(r'^registrationFileTemplates/(?P<_id>[0-9]+)$', RegistrationFileTemplateView.as_view(), name="registrationFileTemplate"),

View File

@ -22,10 +22,10 @@ import Subscriptions.mailManager as mailer
import Subscriptions.util as util
from Subscriptions.automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine
from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFeeSerializer
from .serializers import RegistrationFormSerializer, StudentSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer, RegistrationFileSerializer, RegistrationFileTemplateSerializer, RegistrationFormByParentSerializer, StudentByRFCreationSerializer
from .pagination import CustomPagination
from .signals import clear_cache
from .models import Student, Guardian, RegistrationForm, RegistrationFee, RegistrationFileTemplate, RegistrationFile
from .models import Student, Guardian, RegistrationForm, RegistrationFileTemplate, RegistrationFile
from .automate import Automate_RF_Register, load_config, getStateMachineObjectState, updateStateMachine
from Auth.models import Profile
@ -335,16 +335,6 @@ class StudentListView(APIView):
students_serializer = StudentByRFCreationSerializer(students, many=True)
return JsonResponse(students_serializer.data, safe=False)
# API utilisée pour la vue de personnalisation des frais d'inscription pour la structure
class RegistrationFeeView(APIView):
"""
Liste les frais dinscription.
"""
def get(self, request):
tarifs = bdd.getAllObjects(RegistrationFee)
tarifs_serializer = RegistrationFeeSerializer(tarifs, many=True)
return JsonResponse(tarifs_serializer.data, safe=False)
class RegistrationFileTemplateView(APIView):
"""
Gère les fichiers templates pour les dossiers dinscription.

View File

@ -410,7 +410,9 @@ useEffect(()=>{
associated_profile: response.id
}],
sibling: []
}
},
fees: allFeesIds,
discounts: allDiscountsds
};
createRegisterForm(data, csrfToken)
@ -422,6 +424,7 @@ useEffect(()=>{
sendConfirmRegisterForm(data.student.id, updatedData.studentLastName, updatedData.studentFirstName);
}
closeModal();
console.log('Success:', data);
// Forcer le rechargement complet des données
setReloadFetch(true);
})
@ -810,8 +813,8 @@ const handleFileUpload = ({file, name, is_required, order}) => {
<InscriptionForm students={students}
registrationDiscounts={registrationDiscounts}
tuitionDiscounts={tuitionDiscounts}
registrationFees={registrationFees}
tuitionFees={tuitionFees}
registrationFees={registrationFees.filter(fee => fee.is_active)}
tuitionFees={tuitionFees.filter(fee => fee.is_active)}
onSubmit={createRF}
/>
)}

View File

@ -123,6 +123,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
};
const submit = () => {
console.log('Submitting form data:', formData);
onSubmit(formData);
}

View File

@ -4,10 +4,15 @@ import ReactDOM from 'react-dom';
const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => {
if (!visible) return null;
// Diviser le message en lignes
const messageLines = message.split('\n');
return ReactDOM.createPortal(
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white p-6 rounded-md shadow-md">
<p className="mb-4">{message}</p>
{messageLines.map((line, index) => (
<p key={index} className="mb-4">{line}</p>
))}
<div className={`flex ${uniqueConfirmButton ? 'justify-center' : 'justify-end'} gap-4`}>
{!uniqueConfirmButton && (
<button className="px-4 py-2 bg-gray-200 rounded-md" onClick={onCancel}>Annuler</button>

View File

@ -94,6 +94,9 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
const [localErrors, setLocalErrors] = useState({});
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const niveauxPremierCycle = [
{ id: 1, name: 'TPS', age: 2 },
@ -377,7 +380,25 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
</button>
<button
type="button"
onClick={() => handleDelete(classe.id)}
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer la classe " + classe.atmosphere_name + ".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
setRemovePopupOnConfirm(() => () => {
handleDelete(classe.id)
.then(data => {
console.log('Success:', data);
setPopupMessage("La classe " + classe.atmosphere_name + " a été correctement supprimée");
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch(error => {
console.error('Error archiving data:', error);
setPopupMessage("Erreur lors de la suppression de la classe " + classe.atmosphere_name);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
@ -432,6 +453,12 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
</DndProvider>
);

View File

@ -16,6 +16,10 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
// Récupération des messages d'erreur
const getError = (field) => {
return localErrors?.[field]?.[0];
@ -26,7 +30,7 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
};
const handleRemoveSpeciality = (id) => {
handleDelete(id)
return handleDelete(id)
.then(() => {
setSpecialities(prevSpecialities => prevSpecialities.filter(speciality => speciality.id !== id));
})
@ -161,7 +165,25 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
</button>
<button
type="button"
onClick={() => handleRemoveSpeciality(speciality.id)}
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer la spécialité " + speciality.name + ".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
setRemovePopupOnConfirm(() => () => {
handleRemoveSpeciality(speciality.id)
.then(data => {
console.log('Success:', data);
setPopupMessage("La spécialité " + speciality.name + " a été correctement supprimée");
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch(error => {
console.error('Error archiving data:', error);
setPopupMessage("Erreur lors de la suppression de la spécialité " + speciality.name);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
@ -204,6 +226,12 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
</DndProvider>
);

View File

@ -98,6 +98,10 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
// Récupération des messages d'erreur
const getError = (field) => {
return localErrors?.[field]?.[0];
@ -109,7 +113,7 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
};
const handleRemoveTeacher = (id) => {
handleDelete(id)
return handleDelete(id)
.then(() => {
setTeachers(prevTeachers => prevTeachers.filter(teacher => teacher.id !== id));
})
@ -361,7 +365,25 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
</button>
<button
type="button"
onClick={() => handleRemoveTeacher(teacher.id)}
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer l'enseignant " + teacher.last_name + " " + teacher.first_name + ".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
setRemovePopupOnConfirm(() => () => {
handleRemoveTeacher(teacher.id)
.then(data => {
console.log('Success:', data);
setPopupMessage("L'enseignant " + teacher.last_name + " " + teacher.first_name + " a été correctement supprimé");
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch(error => {
console.error('Error archiving data:', error);
setPopupMessage("Erreur lors de la suppression de l'enseignant " + teacher.last_name + " " + teacher.first_name);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
@ -407,6 +429,12 @@ const TeachersSection = ({ teachers, setTeachers, specialities, handleCreate, ha
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
</DndProvider>
);

View File

@ -12,13 +12,16 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
const [localErrors, setLocalErrors] = useState({});
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const handleAddDiscount = () => {
setNewDiscount({ id: Date.now(), name: '', amount: '', description: '', discount_type: 0, type: type });
};
const handleRemoveDiscount = (id) => {
handleDelete(id)
return handleDelete(id)
.then(() => {
setDiscounts(prevDiscounts => prevDiscounts.filter(discount => discount.id !== id));
})
@ -204,7 +207,25 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
</button>
<button
type="button"
onClick={() => handleRemoveDiscount(discount.id)}
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer un tarif personnalisé.\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
setRemovePopupOnConfirm(() => () => {
handleRemoveDiscount(discount.id)
.then(data => {
console.log('Success:', data);
setPopupMessage("Réduction correctement supprimé");
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch(error => {
console.error('Error archiving data:', error);
setPopupMessage("Erreur lors de la suppression de la réduction");
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
@ -269,6 +290,12 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
);
};

View File

@ -12,13 +12,22 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
const [localErrors, setLocalErrors] = useState({});
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const labelTypeFrais = (type === 0 ? 'Frais d\'inscription' : 'Frais de scolarité');
// Récupération des messages d'erreur
const getError = (field) => {
return localErrors?.[field]?.[0];
};
const handleAddFee = () => {
setNewFee({ id: Date.now(), name: '', base_amount: '', description: '', validity_start_date: '', validity_end_date: '', discounts: [], type: type });
};
const handleRemoveFee = (id) => {
handleDelete(id)
return handleDelete(id)
.then(() => {
setFees(prevFees => prevFees.filter(fee => fee.id !== id));
})
@ -37,11 +46,11 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
setNewFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
.catch((error) => {
console.error('Error:', error.message);
if (error.details) {
console.error('Form errors:', error.details);
setLocalErrors(error.details);
}
});
} else {
@ -60,11 +69,11 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
setEditingFee(null);
setLocalErrors({});
})
.catch(error => {
if (error && typeof error === 'object') {
setLocalErrors(error);
} else {
console.error(error);
.catch((error) => {
console.error('Error:', error.message);
if (error.details) {
console.error('Form errors:', error.details);
setLocalErrors(error.details);
}
});
} else {
@ -118,7 +127,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
value={value}
onChange={onChange}
placeholder={placeholder}
errorMsg={localErrors && localErrors[field] && Array.isArray(localErrors[field]) ? localErrors[field][0] : ''}
errorMsg={getError(field)}
/>
</div>
);
@ -187,7 +196,25 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
</button>
<button
type="button"
onClick={() => handleRemoveFee(fee.id)}
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage("Attentions ! \nVous êtes sur le point de supprimer un " + labelTypeFrais + ".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?");
setRemovePopupOnConfirm(() => () => {
handleRemoveFee(fee.id)
.then(data => {
console.log('Success:', data);
setPopupMessage(labelTypeFrais + " correctement supprimé");
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch(error => {
console.error('Error archiving data:', error);
setPopupMessage("Erreur lors de la suppression du " + labelTypeFrais);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
@ -232,7 +259,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">{type === 0 ? 'Frais d\'inscription' : 'Frais de scolarité'}</h2>
<h2 className="text-xl font-semibold">{labelTypeFrais}</h2>
</div>
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
@ -251,6 +278,12 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
);
};