From 7c0c4329b1373434a90b18be1eb102ad39da2c9e Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Sun, 9 Mar 2025 01:13:21 +0330 Subject: [PATCH] cron job added if it wont work fck this --- .github/workflows/deploy.yaml | 2 +- backend/core/settings/base.py | 12 ++++- backend/dockerfile | 1 + .../migrations/0011_orderitemmodel_price.py | 19 +++++++ backend/order/models.py | 3 +- backend/order/serializers.py | 5 +- backend/product/admin.py | 17 ++++++- backend/product/cron.py | 5 ++ ...emove_productvariant_max_price_and_more.py | 37 ++++++++++++++ backend/product/models.py | 50 +++++++++++++------ backend/product/serializers.py | 7 +-- backend/requirements.txt | 1 + docker-compose.yml | 2 +- 13 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 backend/order/migrations/0011_orderitemmodel_price.py create mode 100644 backend/product/cron.py create mode 100644 backend/product/migrations/0034_remove_productvariant_max_price_and_more.py diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 6c287c1..255860e 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -31,5 +31,5 @@ jobs: script: | cd /root/hshop/ docker compose down - docker compose build + docker compose build --no-cache docker compose up -d \ No newline at end of file diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py index ee792e4..38808a3 100644 --- a/backend/core/settings/base.py +++ b/backend/core/settings/base.py @@ -30,7 +30,7 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY") # Security and Debugging -SECRET_KEY = os.getenv("SECRET_KEY") +SECRET_KEY = os.getenv("SECRET_KEY") or 'this is just for cron job dont judge me' DEBUG = True BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -65,6 +65,7 @@ INSTALLED_APPS = [ "rest_framework.authtoken", "import_export", "django_jalali", + 'django_crontab', # Custom Apps "product", "account", @@ -233,3 +234,12 @@ AWS_S3_OBJECT_PARAMETERS = { 'ACL': 'public-read', } +# ============================================================================== +# django CRONJOBS +# ============================================================================== + +CRONJOBS = [ + ('* * * * *', 'product.cron.update_product_prices', f'>> {BASE_DIR}/logfile.log 2>&1'), +] + + diff --git a/backend/dockerfile b/backend/dockerfile index 16ceef1..09c155a 100644 --- a/backend/dockerfile +++ b/backend/dockerfile @@ -13,5 +13,6 @@ COPY . /app/ CMD ["sh", "-c", "python manage.py makemigrations && \ python manage.py migrate && \ + python manage.py crontab add && \ python manage.py collectstatic --no-input && \ gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3"] \ No newline at end of file diff --git a/backend/order/migrations/0011_orderitemmodel_price.py b/backend/order/migrations/0011_orderitemmodel_price.py new file mode 100644 index 0000000..ceaf65f --- /dev/null +++ b/backend/order/migrations/0011_orderitemmodel_price.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-03-08 18:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0010_alter_orderitemmodel_quantity'), + ] + + operations = [ + migrations.AddField( + model_name='orderitemmodel', + name='price', + field=models.PositiveIntegerField(default=0, verbose_name='قیمت'), + preserve_default=False, + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index c400e1c..1251999 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -97,13 +97,14 @@ class OrderModel(models.Model): 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='قیمت') product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") class Meta: verbose_name = 'ایتم سبد خرید' verbose_name_plural = 'ایتم های سبد خرید' def total(self): - return self.quantity * self.product.get_toman_price() + return self.quantity * self.product.price def total_with_discount(self): return self.quantity * self.product.get_toman_price_after_discount() diff --git a/backend/order/serializers.py b/backend/order/serializers.py index 37f5cca..f796e5b 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -1,12 +1,15 @@ from rest_framework import serializers from .models import OrderItemModel, OrderModel - +from product.serializers import ProductVariantSerialzier class OrderItemSerailzier(serializers.ModelSerializer): + product = serializers.SerializerMethodField() class Meta: model = OrderItemModel fields = "__all__" read_only_fields = ('order', 'product') + def get_product(self, obj): + return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data class CartSerializer(serializers.ModelSerializer): items = OrderItemSerailzier(many=True) diff --git a/backend/product/admin.py b/backend/product/admin.py index 11359e1..a7d6e19 100644 --- a/backend/product/admin.py +++ b/backend/product/admin.py @@ -9,7 +9,7 @@ from django.contrib.postgres.fields import ArrayField from unfold.widgets import UnfoldAdminColorInputWidget from unfold.decorators import action, display from utils.admin import ModelAdmin - +from django.shortcuts import redirect @@ -140,8 +140,10 @@ class ProductVariantInLine(StackedInline): show_change_link = True tab = True min_num = 1 + readonly_fields = ['price'] # inlines = [DetailModelInLine] autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details'] + fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details','sell'] # search_fields = [''] @@ -156,6 +158,7 @@ class ProductVariantAdmin(ModelAdmin, ImportExportModelAdmin): export_form_class = ExportForm autocomplete_fields = ['product_attributes', 'images', 'in_pack_items', 'details'] warn_unsaved_form = True + readonly_fields = ['price'] # inlines = [DetailModelInLine] @admin.register(ProductModel) @@ -169,6 +172,7 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): autocomplete_fields = ['related_products', ] # compressed_fields = True warn_unsaved_form = True + actions_list = ['redirect_to_learn', 'update_products_price'] list_display = ['display_image', 'display_price', 'view', 'show', 'rating', 'category', ] fieldsets = ( ('فیلد های اصلی', {'fields': ('name', 'description', 'category', 'related_products', 'show',), "classes": ["tab"],}), @@ -188,7 +192,7 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): def display_price(self, obj): if obj.variants.all().first(): - return obj.variants.all().first().get_toman_price() + return obj.variants.all().first().price display_price.short_description = 'قیمت تومانی' @display(description='محصول', header=True) @@ -209,6 +213,15 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin): # "squared": True, }, ] + + @action(description=f"اپدیت قیمت ها") + def update_products_price(self, request): + print('from the button') + ProductVariant.update_all_prices() + messages.success(request, f"قیمت {ProductVariant.objects.all().count()} تنوع محصول اپدیت شد") + return redirect("admin:product_productmodel_changelist") + + # @display( # description=("نمایش در صفحه ی اصلی"), # label={ diff --git a/backend/product/cron.py b/backend/product/cron.py new file mode 100644 index 0000000..883774f --- /dev/null +++ b/backend/product/cron.py @@ -0,0 +1,5 @@ +from product.models import ProductVariant + +def update_product_prices(): + print('calling the update product prices from cron') + ProductVariant.update_all_prices() \ No newline at end of file diff --git a/backend/product/migrations/0034_remove_productvariant_max_price_and_more.py b/backend/product/migrations/0034_remove_productvariant_max_price_and_more.py new file mode 100644 index 0000000..786bb3a --- /dev/null +++ b/backend/product/migrations/0034_remove_productvariant_max_price_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1.2 on 2025-03-08 18:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0033_alter_productvariant_details'), + ] + + operations = [ + migrations.RemoveField( + model_name='productvariant', + name='max_price', + ), + migrations.AddField( + model_name='productvariant', + name='input_price', + field=models.PositiveIntegerField(default=0, verbose_name='قیمت ورودی'), + ), + migrations.AlterField( + model_name='productvariant', + name='color', + field=models.CharField(blank=True, max_length=7, null=True, verbose_name='رنگ'), + ), + migrations.AlterField( + model_name='productvariant', + name='details', + field=models.ManyToManyField(related_name='product', to='product.productdetailmodel', verbose_name='جزییات محصول'), + ), + migrations.AlterField( + model_name='productvariant', + name='price', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قیمت محاسبه شده'), + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index de8aca0..ea20a76 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -215,14 +215,13 @@ class ProductDetailModel(models.Model): # def __str__(self): # return f'جزيیات محصول {self.product}' - class ProductVariant(models.Model): product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول') product_attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگی‌ها', related_name='variant') in_stock = models.PositiveIntegerField(default=0, verbose_name='تعداد موجود') - price = models.PositiveIntegerField(default=0, verbose_name='قیمت') + price = models.PositiveIntegerField(verbose_name='قیمت محاسبه شده', blank=True, null=True) + input_price = models.PositiveIntegerField(default=0, verbose_name='قیمت ورودی') min_price = models.PositiveIntegerField(verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود') - max_price = models.PositiveIntegerField(verbose_name='قیمت سقف', help_text='این قیمت برای سقف قیمتی محصول در نظر گرفته میشود') currency_type = ( ('dollor', 'دلار'), ('toman', 'تومان'), @@ -232,33 +231,56 @@ class ProductVariant(models.Model): sell = models.IntegerField(default=0, verbose_name='فروش') currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type) discount = models.SmallIntegerField(default=0, verbose_name='تخفیف') - color = models.CharField(verbose_name='رنک', max_length=7, blank=True, null=True) + color = models.CharField(verbose_name='رنگ', max_length=7, blank=True, null=True) images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها') video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو') - details = models.ManyToManyField(ProductDetailModel, verbose_name='جزيیات محصول', related_name='product') + details = models.ManyToManyField(ProductDetailModel, verbose_name='جزییات محصول', related_name='product') class Meta: verbose_name = 'تنوع محصول' verbose_name_plural = 'تنوع‌های محصول' def __str__(self): return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}" - - def get_toman_price(self, dollor_price=None): + + def set_or_update_price(self, dollor_price=None): if not dollor_price: dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') dollor_price = dollor_object.price - dollar_to_dirham = 0.27 if dollor_price is None: raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) + + dollar_to_dirham = 0.27 + if self.currency == 'toman': - toman_price = self.price + toman_price = self.input_price elif self.currency == 'dollor': - toman_price = self.price * dollor_price + toman_price = self.input_price * dollor_price elif self.currency == 'derham': - toman_price = self.price * dollor_price * dollar_to_dirham - toman_price = toman_price if toman_price > self.min_price else self.min_price - return toman_price + toman_price = self.input_price * dollor_price * dollar_to_dirham + else: + toman_price = self.input_price + + self.price = max(toman_price, self.min_price) + + def save(self, *args, **kwargs): + self.set_or_update_price() + super().save(*args, **kwargs) def get_toman_price_after_discount(self): - return self.get_toman_price() * ((100 - self.discount) / 100) + return self.price * ((100 - self.discount) / 100) + + @classmethod + def update_all_prices(cls): + print('calling the update all prices ') + dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') + print(dollor_object.price) + dollor_object.update_price() + dollor_object.save() + dollor_price = dollor_object.price + print(dollor_object.price) + print('classmethod dollor price update ') + products = cls.objects.all() + for product in products: + product.set_or_update_price(dollor_price=dollor_price) + product.save() \ No newline at end of file diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 6a1320a..68ac5a6 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -46,18 +46,13 @@ class ProductImageSerailizer(serializers.ModelSerializer): class ProductVariantSerialzier(serializers.ModelSerializer): product_attributes = AttributeValueSerialzier(many=True) - price = serializers.SerializerMethodField() in_pack_items = InPackItemsSerialzier(many=True) images = ProductImageSerailizer(many=True) details = ProductDetailSerializer(many=True, read_only=True) class Meta: model = ProductVariant - exclude = ('min_price', 'max_price','sell', 'currency', 'product') + exclude = ('min_price', 'sell', 'currency', 'product', 'input_price') - def get_price(self, obj): - dollor_price = self.context.get('dollor_price') - toman_price = obj.get_toman_price(dollor_price=dollor_price) - return "{:,.0f} تومان".format(toman_price) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/backend/requirements.txt b/backend/requirements.txt index 29093dc..a3a2e7b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -25,6 +25,7 @@ django-cleanup==8.1.0 django-colorfield==0.11.0 django-cors-headers==4.4.0 django-cron==0.6.0 +django-crontab==0.7.1 django-dbbackup==4.2.1 django-dirtyfields==1.9.3 django-filter==24.3 diff --git a/docker-compose.yml b/docker-compose.yml index 66ea3cc..14dbfba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,7 @@ services: [ "sh", "-c", - "python manage.py migrate && python manage.py collectstatic --no-input && gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3", + "python manage.py migrate && python manage.py collectstatic --no-input && python manage.py crontab add && gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3", ] networks: - default