Browse Source

multi currency logic for course prices added

master
Mohsen Taba 1 month ago
parent
commit
deec75f670
  1. 24
      apps/course/admin/course.py
  2. 21
      apps/course/migrations/0010_course_rub_prices.py
  3. 50
      apps/course/models/course.py
  4. 22
      apps/course/serializers/admin.py
  5. 66
      apps/course/serializers/course.py
  6. 2
      apps/course/serializers/professor.py
  7. 38
      apps/course/tests/test_multiple_roles_api.py
  8. 11
      apps/course/views/admin.py

24
apps/course/admin/course.py

@ -269,7 +269,7 @@ class CourseAdmin(DirectCourseAdmin):
warn_unsaved_form = True
search_fields = ('id','title', 'description')
exclude = ('slug', )
readonly_fields = ('final_price',)
readonly_fields = ('final_price', 'final_price_rub')
autocomplete_fields = ('category', 'professor',)
list_filter_submit = True
change_form_show_cancel_button = True
@ -280,8 +280,10 @@ class CourseAdmin(DirectCourseAdmin):
conditional_fields = {
'price': "is_free == false",
'price_rub': "is_free == false",
'discount_percentage': "is_free == false",
'final_price': "is_free == false",
'final_price_rub': "is_free == false",
'online_link': "is_online",
'video_file': "video_type == 'video_file'",
'video_link': "video_type == 'youtube_link'",
@ -308,8 +310,8 @@ class CourseAdmin(DirectCourseAdmin):
}),
(_('Pricing'), {
'fields': (
('is_free', 'price'),
('discount_percentage', 'final_price')
('is_free', 'price', 'price_rub'),
('discount_percentage', 'final_price', 'final_price_rub')
),
'classes': ['tab'],
}),
@ -347,12 +349,18 @@ class CourseAdmin(DirectCourseAdmin):
if instance.discount_percentage > 0:
return format_html(
'<span class="line-through text-gray-400 mr-2">${}</span>'
'<span class="text-green-600 font-medium">${}</span>',
'<span class="block text-xs text-gray-400">USD: <span class="line-through">${}</span> -> <span class="text-green-600 font-medium">${}</span></span>'
'<span class="block text-xs text-gray-400 mt-1">RUB: <span class="line-through">₽{}</span> -> <span class="text-green-600 font-medium">₽{}</span></span>',
instance.price,
instance.final_price
instance.final_price,
instance.price_rub,
instance.final_price_rub,
)
return format_html('<span>${}</span>', instance.final_price)
return format_html(
'<span class="block">USD: ${}</span><span class="block mt-1">RUB: ₽{}</span>',
instance.final_price,
instance.final_price_rub,
)
actions_row = ["add_student_to_course"]
@ -592,4 +600,4 @@ class HiddenCourseAdmin(ModelAdmin):
def has_delete_permission(self, request, obj=None):
return False
dovoodi_admin_site.register(Course, HiddenCourseAdmin)
dovoodi_admin_site.register(Course, HiddenCourseAdmin)

21
apps/course/migrations/0010_course_rub_prices.py

@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0009_alter_course_description_and_more'),
]
operations = [
migrations.AddField(
model_name='course',
name='final_price_rub',
field=models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='This field is automatically calculated based on the discount percentage.', max_digits=10, verbose_name='Course Final Price (RUB)'),
),
migrations.AddField(
model_name='course',
name='price_rub',
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Course Price (RUB)'),
),
]

50
apps/course/models/course.py

@ -151,6 +151,9 @@ STATUS_TRANSLATIONS = {
}
}
CURRENCY_USD = 'USD'
CURRENCY_RUB = 'RUB'
def normalize_level(value):
if not value:
return 'beginner'
@ -292,11 +295,16 @@ class Course(models.Model):
status = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Course Status'))
is_free = models.BooleanField(default=True, verbose_name=_('Is Free'))
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name=_('Course Price'))
price_rub = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name=_('Course Price (RUB)'))
discount_percentage = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], verbose_name=_('Discount Percentage'))
final_price = models.DecimalField(
verbose_name=_('Course Final Price'), decimal_places=2, max_digits=10, default=0.00, blank=True,
help_text=_('This field is automatically calculated based on the discount percentage.')
)
final_price_rub = models.DecimalField(
verbose_name=_('Course Final Price (RUB)'), decimal_places=2, max_digits=10, default=0.00, blank=True,
help_text=_('This field is automatically calculated based on the discount percentage.')
)
is_group_chat_locked = models.BooleanField(
default=False,
@ -339,6 +347,26 @@ class Course(models.Model):
def recalculate_lessons_count(self):
return self.__class__.recalculate_lessons_count_for_course(self.pk)
def get_currency_for_language(self, lang):
normalized = (lang or 'en').lower()
if normalized.startswith('ru'):
return CURRENCY_RUB
return CURRENCY_USD
def get_price_for_language(self, lang):
if self.is_free:
return Decimal('0.00')
if self.get_currency_for_language(lang) == CURRENCY_RUB:
return Decimal(self.price_rub or 0)
return Decimal(self.price or 0)
def get_final_price_for_language(self, lang):
if self.is_free:
return Decimal('0.00')
if self.get_currency_for_language(lang) == CURRENCY_RUB:
return Decimal(self.final_price_rub or 0)
return Decimal(self.final_price or 0)
def save(self, *args, **kwargs):
self.title = format_multilingual_field(self.title)
@ -424,21 +452,31 @@ class Course(models.Model):
]
self.features = format_multilingual_field(self.features)
# Ensure consistency: if price is 0, set is_free to True and discount_percentage to 0
if self.price == 0:
usd_price = Decimal(self.price or 0)
rub_price = Decimal(self.price_rub or 0)
# Ensure consistency: if both prices are 0, set is_free to True and discount_percentage to 0
if usd_price == 0 and rub_price == 0:
self.is_free = True
self.discount_percentage = 0
self.final_price = Decimal('0.00')
self.final_price_rub = Decimal('0.00')
elif self.is_free:
self.price = Decimal('0.00')
self.price_rub = Decimal('0.00')
self.discount_percentage = 0
self.final_price = Decimal('0.00')
self.final_price_rub = Decimal('0.00')
elif self.discount_percentage > 0:
discount_amount = (self.price * self.discount_percentage) / 100
final_price = self.price - discount_amount
self.final_price = Decimal(math.ceil(final_price)).quantize(Decimal('0.00'))
usd_discount_amount = (usd_price * self.discount_percentage) / 100
rub_discount_amount = (rub_price * self.discount_percentage) / 100
usd_final_price = usd_price - usd_discount_amount
rub_final_price = rub_price - rub_discount_amount
self.final_price = Decimal(math.ceil(usd_final_price)).quantize(Decimal('0.00'))
self.final_price_rub = Decimal(math.ceil(rub_final_price)).quantize(Decimal('0.00'))
else:
self.final_price = Decimal(math.ceil(self.price)).quantize(Decimal('0.00'))
self.final_price = Decimal(math.ceil(usd_price)).quantize(Decimal('0.00'))
self.final_price_rub = Decimal(math.ceil(rub_price)).quantize(Decimal('0.00'))
super().save(*args, **kwargs)

22
apps/course/serializers/admin.py

@ -20,7 +20,21 @@ from apps.course.models.course import extract_text_from_json, get_localized_fiel
def get_request_lang(serializer_instance):
request = serializer_instance.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
language_code = getattr(request, 'LANGUAGE_CODE', None)
if language_code:
return str(language_code).lower()
header_value = ''
if request is not None:
if hasattr(request, 'headers'):
header_value = request.headers.get('Accept-Language', '')
if not header_value and hasattr(request, 'META'):
header_value = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
if header_value:
return header_value.split(',')[0].strip().lower()
return 'en'
class AdminLessonSerializer(serializers.ModelSerializer):
class Meta:
@ -102,8 +116,10 @@ class AdminCourseListSerializer(serializers.ModelSerializer):
'status',
'is_free',
'price',
'price_rub',
'discount_percentage',
'final_price',
'final_price_rub',
'participant_count',
'created_at'
]
@ -162,8 +178,10 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer):
'status',
'is_free',
'price',
'price_rub',
'discount_percentage',
'final_price',
'final_price_rub',
'is_group_chat_locked',
'is_professor_chat_locked',
'timing',
@ -172,7 +190,7 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer):
'created_at',
'updated_at'
]
read_only_fields = ['id', 'final_price', 'created_at', 'updated_at']
read_only_fields = ['id', 'final_price', 'final_price_rub', 'created_at', 'updated_at']
def get_participant_count(self, obj):
return obj.participants.count()

66
apps/course/serializers/course.py

@ -5,10 +5,31 @@ from apps.course.models import Course, CourseCategory, Attachment, Glossary, Les
from apps.course.models.course import get_localized_field
from apps.chat.models import RoomMessage
from apps.account.serializers import UserProfileSerializer
from decimal import Decimal
import ast
import json
def resolve_request_lang(request, default='en'):
language_code = getattr(request, 'LANGUAGE_CODE', None)
if language_code:
return str(language_code).lower()
if not request:
return default
header_value = ''
if hasattr(request, 'headers'):
header_value = request.headers.get('Accept-Language', '')
if not header_value and hasattr(request, 'META'):
header_value = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
if header_value:
return header_value.split(',')[0].strip().lower()
return default
class CourseCategorySerializer(serializers.ModelSerializer):
course_count = serializers.SerializerMethodField()
@ -21,7 +42,7 @@ class CourseCategorySerializer(serializers.ModelSerializer):
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
return resolve_request_lang(request)
def get_name(self, obj):
return get_localized_field(self._lang(), obj.name)
@ -41,6 +62,7 @@ class CourseListSerializer(serializers.ModelSerializer):
price = serializers.SerializerMethodField()
discount_percentage = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
currency = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
@ -67,12 +89,13 @@ class CourseListSerializer(serializers.ModelSerializer):
'is_free',
'price',
'discount_percentage',
'final_price',
'final_price',
'currency',
]
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
return resolve_request_lang(request)
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
@ -110,22 +133,27 @@ class CourseListSerializer(serializers.ModelSerializer):
return max(lessons_count, obj.lessons_count)
def get_price(self, obj):
if obj.is_free or obj.price == 0:
amount = obj.get_price_for_language(self._lang())
if obj.is_free or amount == 0:
return "0.00"
return str(obj.price)
return str(amount)
def get_discount_percentage(self, obj):
if obj.is_free or obj.price == 0:
if obj.is_free or obj.get_price_for_language(self._lang()) == 0:
return 0
return obj.discount_percentage
def get_final_price(self, obj):
if obj.is_free or obj.price == 0:
amount = obj.get_final_price_for_language(self._lang())
if obj.is_free or amount == 0:
return "0.00"
return str(obj.final_price)
return str(amount)
def get_currency(self, obj):
return obj.get_currency_for_language(self._lang())
def get_is_free(self, obj):
return obj.is_free or obj.price == 0
return obj.is_free or (Decimal(obj.price or 0) == 0 and Decimal(obj.price_rub or 0) == 0)
class CourseDetailSerializer(serializers.ModelSerializer):
@ -142,6 +170,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
price = serializers.SerializerMethodField()
discount_percentage = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
currency = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField()
is_professor = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
@ -181,6 +210,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
'price',
'discount_percentage',
'final_price',
'currency',
'timing',
'features',
'last_lesson_id',
@ -389,25 +419,29 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return obj.participants.count()
def get_price(self, obj):
if obj.is_free or obj.price == 0:
amount = obj.get_price_for_language(self._lang())
if obj.is_free or amount == 0:
return "0.00"
return str(obj.price)
return str(amount)
def get_discount_percentage(self, obj):
if obj.is_free or obj.price == 0:
if obj.is_free or obj.get_price_for_language(self._lang()) == 0:
return 0
return obj.discount_percentage
def get_final_price(self, obj):
if obj.is_free or obj.price == 0:
amount = obj.get_final_price_for_language(self._lang())
if obj.is_free or amount == 0:
return "0.00"
return str(obj.final_price)
return str(amount)
def get_currency(self, obj):
return obj.get_currency_for_language(self._lang())
def get_is_free(self, obj):
return obj.is_free or obj.price == 0
return obj.is_free or (Decimal(obj.price or 0) == 0 and Decimal(obj.price_rub or 0) == 0)
def _lang(self):
request = self.context.get('request')
return getattr(request, 'LANGUAGE_CODE', None) or 'en'
return resolve_request_lang(request)
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)

2
apps/course/serializers/professor.py

@ -15,7 +15,7 @@ class ProfessorListSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'slug', 'fullname', 'avatar','experience_years', 'course_count', 'lesson_count']
fields = ['id', 'slug', 'fullname', 'email', 'avatar', 'experience_years', 'course_count', 'lesson_count']
class ProfessorDetailSerializer(UserProfileSerializer):

38
apps/course/tests/test_multiple_roles_api.py

@ -6,6 +6,7 @@ from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework import status
from django.contrib.auth.models import Group
from decimal import Decimal
from apps.account.models import User
from apps.course.models import Course, CourseCategory, Participant
from apps.transaction.models import TransactionParticipant
@ -214,3 +215,40 @@ class MultipleRolesAPITestCase(TestCase):
# user_type field باید بروزرسانی شود
self.assertEqual(self.user.user_type, User.UserType.PROFESSOR)
def test_course_detail_serializer_uses_rub_price_for_russian_language(self):
"""برای زبان روسی باید قیمت روبلی برگردانده شود، نه دلار"""
self.user.add_role('professor')
course = Course.objects.create(
title=[
{"title": "English Title", "language_code": "en"},
{"title": "Русский заголовок", "language_code": "ru"},
],
slug='localized-course',
category=self.category,
professor=self.user,
level='beginner',
duration=10,
lessons_count=5,
description='Test description',
is_free=False,
price=Decimal('100.00'),
price_rub=Decimal('7500.00'),
discount_percentage=10,
)
from apps.course.serializers import CourseDetailSerializer
from django.test import RequestFactory
factory = RequestFactory()
request = factory.get('/', HTTP_ACCEPT_LANGUAGE='ru')
request.user = self.user
request.LANGUAGE_CODE = 'ru'
serializer = CourseDetailSerializer(course, context={'request': request})
data = serializer.data
self.assertEqual(data['currency'], 'RUB')
self.assertEqual(data['price'], '7500.00')
self.assertEqual(data['final_price'], '6750.00')

11
apps/course/views/admin.py

@ -64,11 +64,20 @@ class AdminCourseViewSet(viewsets.ModelViewSet):
# Search Query
search_query = self.request.query_params.get('search', None)
has_search = False
if search_query:
has_search = True
from django.db.models import Case, When, Value, IntegerField
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(description__icontains=search_query) |
Q(short_description__icontains=search_query)
).annotate(
search_relevance=Case(
When(title__icontains=search_query, then=Value(2)),
default=Value(1),
output_field=IntegerField()
)
)
# Filters
@ -95,6 +104,8 @@ class AdminCourseViewSet(viewsets.ModelViewSet):
if student_id:
queryset = queryset.filter(participants__student_id=student_id)
if has_search:
return queryset.order_by('-search_relevance', '-id')
return queryset.order_by('-id')
def perform_create(self, serializer):

Loading…
Cancel
Save