This commit is contained in:
Mamalizz
2025-04-18 19:54:07 +03:30
14 changed files with 221 additions and 136 deletions
+15 -13
View File
@@ -1,7 +1,7 @@
from django.contrib import admin, messages
from .models import *
from unfold.admin import TabularInline, StackedInline
from django.db.models import Q
from import_export.admin import ImportExportModelAdmin
from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
@@ -47,11 +47,11 @@ class BankRecordInline(StackedInline):
class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
search_fields = ['order_id', 'user__phone', 'user__first_name', 'user__last_name', 'user__email']
search_fields = ['user__phone', 'user__first_name', 'user__last_name', 'user__email']
list_filter = ['is_paid', 'status']
actions_list = ['redirect_to_learn', 'udpate_bank_status']
list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address',]
readonly_fields = ('created_at', 'order_id', 'tax', 'final_price', 'cart_total', 'discount', 'discount_code', 'user', 'address', 'is_paid')
readonly_fields = ('created_at', 'tax', 'final_price', 'cart_total', 'discount_amount', 'discount_code', 'user', 'address', 'is_paid')
compressed_fields = True
warn_unsaved_form = True
# exclude = ('bank_records',)
@@ -61,19 +61,21 @@ class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
}
}
inlines = [OrderItemModelInline, BankRecordInline]
# def bank_links(self, obj):
# banks = obj.bank_records.all()
def order_id(self, obj):
return f"سفارش {obj.pk + 1000}"
order_id.short_description = "شماره سفارش"
# if not banks.exists():
# return "-"
# return format_html_join(
# "",
# '<a style="padding-bottom:10px;display:block;" href="/secret-admin/azbankgateways/bank/{}/change/" class="text-primary-600 dark:text-primary-500">{}</a>',
# [(bank.id, bank.tracking_code) for bank in banks]
# ) or "-"
# bank_links.short_description = "Bank Records"
def get_search_results(self, request, queryset, search_term):
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
if search_term.isdigit():
order_id_search = int(search_term) - 1000
queryset |= self.model.objects.filter(Q(pk=order_id_search))
return queryset, use_distinct
@action(description='اپدیت وضعیت رکورد های بانکی')
def udpate_bank_status(self, request):
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-03-29 13:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0025_alter_ordermodel_order_id'),
]
operations = [
migrations.AlterField(
model_name='orderitemmodel',
name='discount',
field=models.SmallIntegerField(verbose_name='درصد تخفیف'),
),
migrations.AlterField(
model_name='orderitemmodel',
name='price',
field=models.PositiveIntegerField(verbose_name='قیمت'),
),
]
@@ -0,0 +1,32 @@
# Generated by Django 5.1.2 on 2025-03-30 15:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0026_alter_orderitemmodel_discount_and_more'),
]
operations = [
migrations.RenameField(
model_name='orderitemmodel',
old_name='discount',
new_name='discount_percent',
),
migrations.RenameField(
model_name='ordermodel',
old_name='discount',
new_name='discount_amount',
),
migrations.RemoveField(
model_name='ordermodel',
name='order_id',
),
migrations.AlterField(
model_name='ordermodel',
name='status',
field=models.CharField(choices=[('CART', 'در سبد خرید'), ('ADMIN_PENDING', 'در انتظار تایید'), ('PENDING', 'درحال پردازش'), ('POSTED', 'ارسال شده'), ('RECEIVED', 'تحویل شده'), ('CANCELED', 'لغو شده'), ('REFUNDED', 'مرجوع شده')], default='CART', max_length=20, verbose_name='وضعیت سفارش'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-03-30 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0027_rename_discount_orderitemmodel_discount_percent_and_more'),
]
operations = [
migrations.AlterField(
model_name='orderitemmodel',
name='price',
field=models.BigIntegerField(verbose_name='قیمت'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-03-30 16:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0028_alter_orderitemmodel_price'),
]
operations = [
migrations.AlterField(
model_name='ordermodel',
name='discount_amount',
field=models.BigIntegerField(blank=True, null=True, verbose_name='مقدار کد تخفیف'),
),
]
+52 -42
View File
@@ -1,9 +1,10 @@
from django.db import models
from django.db import models, transaction
from account.models import User, UserAddressModel, PushSubscription
from product.models import ProductModel, ProductVariant, ProductImageModel
from django.utils import timezone
from django_jalali.db import models as jmodels
from django.core.exceptions import ValidationError
from django.conf import settings
class DiscountCode(models.Model):
code = models.CharField(max_length=50, verbose_name='کد تخفیف')
@@ -44,20 +45,19 @@ class OrderModel(models.Model):
('CANCELED', 'لغو شده'),
('REFUNDED', 'مرجوع شده'),
]
order_id = models.PositiveIntegerField(unique=True, null=True, blank=True, verbose_name='شماره سفارش')
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر')
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True, verbose_name='ادرس')
created_at = jmodels.jDateField(blank=True, null=True, verbose_name="تاریخ ثبت سفارش")
is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت")
discount_code = models.ForeignKey(DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name="وضعیت سفارش")
discount = models.BigIntegerField(null=True, blank=True, verbose_name='کل تخقیف')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='CART', verbose_name="وضعیت سفارش")
discount_amount = models.BigIntegerField(null=True, blank=True, verbose_name='مقدار کد تخفیف')
tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات')
final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی')
cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید')
def __str__(self):
return f'سفارش: {self.id + 1000}'
return f'سفارش: {self.pk + 1000}'
class Meta:
verbose_name = 'سفارش'
@@ -65,58 +65,68 @@ class OrderModel(models.Model):
def save(self, *args, **kwargs):
# genrate order id
if not self.pk:
last_instance = self.__class__.objects.order_by("pk").last()
self.order_id = (last_instance.pk + 1001) if last_instance else 1001
super().save(*args, **kwargs)
def _cal_discount_amount(self, cart_total):
discount_percent = self.discount_code.percent if self.discount_code else 0
return int(cart_total * discount_percent / 100)
def cal_discount(self):
# total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
# discount_percent = self.discount_code.percent
# return total_with_item_discount * ((100 - discount_percent) / 100)
pass
def cal_tax(self):
return self.total_without_tax() * 0.2
def _cal_tax(self, cart_total, discount_amount):
tax_rate = getattr(settings, 'DEFAULT_TAX_RATE', 20)
return int((cart_total - discount_amount) * tax_rate / 100)
def _cal_cart_total(self):
from django.db.models import Sum, F, FloatField
return self.items.aggregate(
total=Sum(F('price') * (1 - F('discount_percent')/100) * F('quantity'),
output_field=FloatField()
)).get('total') or 0
def cal_total(self):
pass
return self.total_with_discount() + self.tax()
def cal_final_price(self):
pass
def _cal_final_price(self, cart_total, discount_amount, tax):
return cart_total - discount_amount + tax
def update_order(self):
if self.status == 'CART':
cart_total = self._cal_cart_total()
discount_amount = self._cal_discount_amount(cart_total)
self.discount_amount = discount_amount
self.cart_total = cart_total
tax = self._cal_tax(cart_total, discount_amount)
self.tax = tax
self.final_price = self._cal_final_price(cart_total, discount_amount, tax)
self.save()
else:
pass
class OrderItemModel(models.Model):
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
price = models.PositiveIntegerField(verbose_name='قیمت', default=0)
price = models.BigIntegerField(verbose_name='قیمت')
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف')
class Meta:
verbose_name = 'ایتم سبد خرید'
verbose_name_plural = 'ایتم های سبد خرید'
# def total(self):
# return self.quantity * self.product.price
# def total_with_discount(self):
# return self.quantity * self.product.get_toman_price_after_discount()
def update_fields(self):
pass
def set_or_update_fields(self):
self.price = self.product.price
self.discount_percent = self.product.discount
def __str__(self):
return f'({self.product}) - ({self.order.user})'
def save(self, *args, **kwargs):
self.clean()
self.set_or_update_fields()
super().save(*args, **kwargs)
self.order.update_order()
def delete(self, *args, **kwargs):
self.clean()
order = self.order
super().delete(*args, **kwargs)
order.update_order()
def clean(self):
if self.pk and self.order.status != "CART":
raise ValidationError("ایتم ها فقط در حالت سبد خرید قابل ادیت هستند")
+7 -3
View File
@@ -53,7 +53,7 @@ class OrderItemSerailzier(serializers.ModelSerializer):
class Meta:
model = OrderItemModel
exclude = ('order',)
read_only_fields = ('order', 'product',)
read_only_fields = ('order', 'product', 'discount_percent')
def get_product(self, obj):
return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data
@@ -79,9 +79,10 @@ class CartSerializer(serializers.ModelSerializer):
tax = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
discount_code = serializers.SerializerMethodField()
address = UserAddressSerializer()
class Meta:
model = OrderModel
fields = [ 'discount_code', 'items', 'cart_total', 'tax', 'final_price']
fields = [ 'discount_code', 'items', 'cart_total', 'tax', 'final_price', 'address']
def get_discount_code(self, obj):
@@ -109,6 +110,7 @@ class OrderListSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
verbose_status = serializers.SerializerMethodField()
order_id = serializers.SerializerMethodField()
class Meta:
model = OrderModel
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status']
@@ -119,6 +121,8 @@ class OrderListSerializer(serializers.ModelSerializer):
def get_count(self, obj):
return obj.items.all().count()
def get_order_id(self, obj):
return obj.pk + 1000
def get_images(self, obj):
image_list = [
self.context.get('request').build_absolute_uri(image.image.url)
@@ -138,7 +142,7 @@ class OrderGetSerializer(serializers.ModelSerializer):
discount_code = DiscountCodeSerializer()
class Meta:
model = OrderModel
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status', 'address', 'items', 'tax' , 'cart_total', 'discount_code', 'discount']
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status', 'address', 'items', 'tax' , 'cart_total', 'discount_code', 'discount_amount']
def get_verbose_status(self, obj):
return obj.get_status_display()
+11 -5
View File
@@ -1,7 +1,7 @@
from django.db.models.signals import pre_save
from django.dispatch import receiver
from .models import OrderModel
from account.models import PushSubscription
from account.models import PushSubscription, UserAddressModel
import ghasedak_sms
from .tasks import send_change_status_notif, send_change_status_sms
@@ -13,8 +13,8 @@ def order_status_changed(sender, instance, **kwargs):
if previous.status != instance.status:
new_status = instance.get_status_display()
send_change_status_notif.delay(instance.pk, new_status)
send_change_status_sms.delay(instance.pk, new_status)
# send_change_status_notif.delay(instance.pk, new_status)
# send_change_status_sms.delay(instance.pk, new_status)
if previous.status == 'CART' and instance.status == 'ADMIN_PENDING':
# update_cart_price_fields()
@@ -23,11 +23,17 @@ def order_status_changed(sender, instance, **kwargs):
pass
@receiver(pre_save, sender=OrderModel)
def set_default_address(sender, instance, **kwargs):
if instance.address is None and instance.user:
default_address = UserAddressModel.objects.filter(user=instance.user, is_main=True).first()
if default_address:
instance.address = default_address
def update_cart_price_fields(order):
pass
# def update_cart_price_fields(order):
# pass
def update_sell_data(order):
pass
+1 -1
View File
@@ -297,7 +297,7 @@ class BankCallbackSerializer(serializers.ModelSerializer):
PaymentStatus.REDIRECT_TO_BANK,
PaymentStatus.RETURN_FROM_BANK,
}:
return "waiting"
return "pending"
elif obj.status in {
PaymentStatus.CANCEL_BY_USER,
PaymentStatus.EXPIRE_GATEWAY_TOKEN,
+3
View File
@@ -4,6 +4,9 @@
src="/img/footer-bg.jpg"
alt=""
class="absolute z-10 object-cover opacity-45"
:style="{
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%',
}"
/>
<div class="flex flex-col gap-4 items-center justify-center relative z-20">
+20 -18
View File
@@ -5,6 +5,7 @@ const { $gsap: gsap } = useNuxtApp();
const isSiteLoadingDisabled = useCookie("is-site-loading-disabled", {
default: () => false,
expires: new Date(Date.now() + 15 * 60 * 1000),
});
const shouldRenderLoadingOverlay = ref(true);
@@ -27,8 +28,8 @@ const progressStyle = computed(() => {
// methods
const onAssetLoaded = () => {
criticalLoad.value = false;
clearInterval(progressInterval.value!);
criticalLoad.value = false;
assetLoadingProgress.value = 100;
isAssetLoaded.value = true;
};
@@ -55,30 +56,31 @@ watch([assetLoadingProgress, criticalLoad], ([assetLoadingProgress, criticalLoad
// lifecycle
onMounted(() => {
isWindowScrollLocked.value = true;
if (!isSiteLoadingDisabled.value) {
isWindowScrollLocked.value = true;
const heymlzLoadingAnimation = document.querySelector("#heymlz-loading-animation") as HTMLVideoElement;
if (heymlzLoadingAnimation?.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
onAssetLoaded();
}
progressInterval.value = setInterval(() => {
assetLoadingProgress.value += Math.random() * 10;
}, 250);
gsap.to("#loading-overlay", {
opacity: 1,
});
} else {
shouldRenderLoadingOverlay.value = false;
}
const heymlzLoadingAnimation = document.querySelector("#heymlz-loading-animation") as HTMLVideoElement;
if (heymlzLoadingAnimation?.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
onAssetLoaded();
}
progressInterval.value = setInterval(() => {
assetLoadingProgress.value += Math.random() * 10;
}, 250);
gsap.to("#loading-overlay", {
opacity: 1,
});
});
</script>
<template>
<div
v-if="shouldRenderLoadingOverlay"
v-if="shouldRenderLoadingOverlay && !isSiteLoadingDisabled"
id="loading-overlay"
class="fixed inset-0 size-full z-9999 flex-center bg-black"
>
+1 -1
View File
@@ -131,7 +131,7 @@ watch(() => status.value, (value) => {
:key="id"
:index="index"
:autofocus="autofocus ? index === 0 ? true : 'off' : 'off'"
class="disabled:text-slate-400 focus-within:border-black transition-all size-12 sm:size-16 bg-slate-50 typo-label-lg rounded-md sm:rounded-lg text-center border-[1.5px] border-slate-200 outline-none"
class="disabled:text-slate-400 persian-number focus-within:border-black transition-all size-10 sm:size-16 bg-slate-50 typo-label-lg rounded-sm sm:rounded-lg text-center border-[1.5px] border-slate-200 outline-none"
/>
</PinInputRoot>
</div>
+2 -13
View File
@@ -22,19 +22,8 @@ const onSwiper = (swiper: SwiperClass) => {
ref="sectionTarget"
class="flex flex-col justify-center gap-4 bg-black h-[110svh] sm:h-[150svh] relative overflow-hidden"
>
<div class="w-full relative translate-y-[-90px] sm:translate-y-[-200px] z-10 container">
<div class="flex-col-center gap-6">
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">
دسته بندی ها
</span>
<!-- <p
class="text-slate-300 text-center max-w-[750px] typo-p-sm md:typo-p-lg xl:typo-p-xl"
>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و
با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه
و مجله در ستون و سطرآنچنان که
</p> -->
</div>
<div class="w-full relative translate-y-[-55px] sm:translate-y-[-130px] flex-center z-10 container">
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4"> دسته بندی ها </span>
</div>
<div class="w-full my-20 relative">
+18 -40
View File
@@ -19,7 +19,7 @@ const heymlzElementIsVisible = useElementVisibility(heymlzElement, {
rootMargin: "0px 0px -40% 0px",
});
const showHeymlzAnimation = ref(true);
const showHeymlzAnimation = ref(false);
const { x: dragAxisX } = useDraggable(draggableEl, {
initialValue: { x: 0, y: 0 },
@@ -32,10 +32,12 @@ watch(
heymlzElementIsVisible,
(newValue) => {
if (newValue) {
showHeymlzAnimation.value = true;
setTimeout(() => {
showHeymlzAnimation.value = false;
}, 3200);
showHeymlzAnimation.value = true;
setTimeout(() => {
showHeymlzAnimation.value = false;
}, 3200);
}, 400);
}
},
{
@@ -61,9 +63,7 @@ watch(
(newValue) => {
const clientRect = previewContainerEl.value?.getBoundingClientRect()!;
const percent = clientRect.width / 100;
const clipPercent =
(newValue + draggableEl.value!.clientWidth / 2 - clientRect.x - 8) /
percent;
const clipPercent = (newValue + draggableEl.value!.clientWidth / 2 - clientRect.x - 8) / percent;
if (clipPercent >= 5 && clipPercent <= 95) {
clipPathPercent.value = clipPercent;
}
@@ -75,12 +75,8 @@ watch(
<div class="container mb-40 lg:mb-80 mt-20">
<div>
<div class="flex flex-col items-center gap-3 mb-10 lg:mb-16">
<span class="typo-p-sm md:typo-p-md text-slate-500">
مقایسه محصولات
</span>
<span class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-black">
تفاوت محصلات ما را ببینید
</span>
<span class="typo-p-sm md:typo-p-md text-slate-500"> مقایسه محصولات </span>
<span class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-black"> تفاوت محصلات ما را ببینید </span>
</div>
<div
ref="previewContainerEl"
@@ -90,11 +86,7 @@ watch(
<NuxtImg
v-if="activeSlideVideo !== 'right'"
:src="homeData!.difreance_section.image1"
:class="
showHeymlzAnimation
? 'brightness-25 blur-sm'
: 'brightness-[95%] blur-[0px]'
"
:class="showHeymlzAnimation ? 'brightness-25 blur-sm' : 'brightness-[95%] blur-[0px]'"
class="select-none absolute size-full object-cover transition-[filter] duration-250"
:alt="homeData!.difreance_section.title1"
/>
@@ -117,11 +109,7 @@ watch(
<NuxtImg
v-if="activeSlideVideo !== 'left'"
:src="homeData!.difreance_section.image2"
:class="
showHeymlzAnimation
? 'brightness-25 blur-sm'
: 'brightness-[95%] blur-[0px]'
"
:class="showHeymlzAnimation ? 'brightness-25 blur-sm' : 'brightness-[95%] blur-[0px]'"
class="overlay-image select-none absolute object-cover size-full transition-[filter] duration-250"
:alt="homeData!.difreance_section.title2"
/>
@@ -136,7 +124,10 @@ watch(
/>
</Transition>
<Transition name="fade" :duration="250">
<Transition
name="fade"
:duration="250"
>
<NuxtImg
v-if="showHeymlzAnimation"
src="/img/heymlz/heymlz-pullingg.gif"
@@ -160,21 +151,13 @@ watch(
>
<div
ref="draggableEl"
:class="
showHeymlzAnimation
? 'bg-neutral-200'
: 'bg-black'
"
:class="showHeymlzAnimation ? 'bg-neutral-200' : 'bg-black'"
class="touch-none cursor-grab hover:scale-115 transition-transform rounded-full absolute size-9 sm:size-11 flex items-center justify-center"
>
<Icon
name="ci:arrows"
class="transition-all size-5 sm:size-6"
:class="
showHeymlzAnimation
? '**:stroke-black'
: '**:stroke-white'
"
:class="showHeymlzAnimation ? '**:stroke-black' : '**:stroke-white'"
/>
</div>
</div>
@@ -220,11 +203,6 @@ watch(
<style>
.overlay-image {
clip-path: polygon(
v-bind('clipPathPercent + "%"') 0,
100% 0,
100% 100%,
v-bind('clipPathPercent + "%"') 100%
);
clip-path: polygon(v-bind('clipPathPercent + "%"') 0, 100% 0, 100% 100%, v-bind('clipPathPercent + "%"') 100%);
}
</style>