From 55fa03d25048f86fbdaec7167d795cbdc6e6841b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BB=D0=B5=D1=80?= <101361201+DarkSteelD@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:42:51 +0300 Subject: [PATCH] Made pricing --- .gitignore | 3 + .../__pycache__/__init__.cpython-310.pyc | Bin 154 -> 166 bytes inventory/__pycache__/apps.cpython-310.pyc | Bin 439 -> 451 bytes inventory/__pycache__/models.cpython-310.pyc | Bin 4142 -> 7147 bytes .../__pycache__/0001_initial.cpython-310.pyc | Bin 3264 -> 3276 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 165 -> 177 bytes inventory/serializers.py | 10 +- inventory/urls.py | 38 +++--- pricing/__init__.py | 0 pricing/admin.py | 3 + pricing/apps.py | 6 + pricing/migrations/0001_initial.py | 113 ++++++++++++++++++ pricing/migrations/__init__.py | 0 pricing/models.py | 108 +++++++++++++++++ pricing/serializers.py | 80 +++++++++++++ pricing/tests.py | 46 +++++++ pricing/urls.py | 39 ++++++ pricing/views.py | 46 +++++++ settings/__pycache__/__init__.cpython-310.pyc | Bin 153 -> 165 bytes settings/__pycache__/base.cpython-310.pyc | Bin 374 -> 1553 bytes settings/__pycache__/dev.cpython-310.pyc | Bin 211 -> 269 bytes settings/base.py | 1 + urls.py | 5 +- 23 files changed, 476 insertions(+), 22 deletions(-) create mode 100644 .gitignore create mode 100644 pricing/__init__.py create mode 100644 pricing/admin.py create mode 100644 pricing/apps.py create mode 100644 pricing/migrations/0001_initial.py create mode 100644 pricing/migrations/__init__.py create mode 100644 pricing/models.py create mode 100644 pricing/serializers.py create mode 100644 pricing/tests.py create mode 100644 pricing/urls.py create mode 100644 pricing/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebd7ddf --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__/* +inventory/migrations/__pycache__/0001_initial.cpython-310.pyc diff --git a/inventory/__pycache__/__init__.cpython-310.pyc b/inventory/__pycache__/__init__.cpython-310.pyc index c625c7fa45eb7be942e85c49637971d2022c3134..cbdfa45bcb1cd89ef15305075b82801257e96227 100644 GIT binary patch delta 75 zcmbQmxQvlIpO=@50SK-})=lKLG4a&T$j?pHPf0Aw)_2KIF3nBND=F4@&n)pMP0}we b$uCOP%}vZpOiu+#=oVKNm!#%S%-040WD^;I delta 63 zcmZ3+IE#@xpO=@50SI1O*G}ZN(XiDIElw>e)=x<+$}BC`cgasK%}vcKDb`ObO3X{n R$Sh8cFD}V1N}X7(4FFSn6+Zv~ diff --git a/inventory/__pycache__/apps.cpython-310.pyc b/inventory/__pycache__/apps.cpython-310.pyc index 0af69011ba997543826200c2cae0a64677d0db2a..978f29185380e1b0b8e9fbc5a0ea74675eb1ac27 100644 GIT binary patch delta 77 zcmdnae3+R#pO=@50SK-})@|e#VKi~o&&bbB)lW$*%GP(uPcF?(%_}L^ch4;GC{5BY dF3B%S)y+-JOH5A%N$3_=7MG;vPIhN3005E88Pos( delta 65 zcmX@iyq%dlpO=@50SI1O*KXt%VbrkH4=qkDD%MX)EXph`)_2KIF3nBND=F4bD@x2u T&d4lIjV~_AFG`&p%vb;bdW02{ diff --git a/inventory/__pycache__/models.cpython-310.pyc b/inventory/__pycache__/models.cpython-310.pyc index 8242b78b9d1e2cd5e7ea2cfbba5ccd6ff47e5cf2..a87835cac4db81ee7ffb9b0b17f835dd30010887 100644 GIT binary patch literal 7147 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 Os#vNOw_?_TA?siL2ply4 diff --git a/inventory/migrations/__pycache__/0001_initial.cpython-310.pyc b/inventory/migrations/__pycache__/0001_initial.cpython-310.pyc index 173570502aa3243e0968dc82efc930f61942877b..c4ea00a6ccd2dab29049dc09c6ab150d300218df 100644 GIT binary patch delta 78 zcmX>gc}9{upO=@50SK-})@|gj;WmlT&&bbB)lW$*%GP(uPcF?(%_}L^ch4;GC{5BY eF3B%S)y+-JOH5A%N$3_=7MG;vZeGtV$Or)5qZ-Qq delta 66 zcmX>jc|ejopO=@50SMY0YBzG%aBKMJhZd(673-%Y7G;(e>$~J9m*%GCl@#lz6(!~+ UXJi(q#uu057o~3A%`M0X0HzTZDF6Tf diff --git a/inventory/migrations/__pycache__/__init__.cpython-310.pyc b/inventory/migrations/__pycache__/__init__.cpython-310.pyc index 1a2af7b57c4d57d0fa0e393b878dfe4fdbf176b9..0a89eba36551b833a5b98e8c39d1e8378ea1269e 100644 GIT binary patch delta 75 zcmZ3=xRH@NpO=@50SK-})=lKLF$vMn$j?pHPf0Aw)_2KIF3nBND=F4@&n)pMP0}we b$uCOP%}vZpOiu+#=oVKNm!#%S%r^r7Y}XmB delta 63 zcmdnUxRjAQpO=@50SI1O*G}ZN(eTg@Elw>e)=x<+$}BC`cgasK%}vcKDb`ObO3X{n R$Sh8cFD}V1N}X731^`;K6>9(h diff --git a/inventory/serializers.py b/inventory/serializers.py index d8f7513..2e8c110 100644 --- a/inventory/serializers.py +++ b/inventory/serializers.py @@ -5,39 +5,45 @@ class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = '__all__' - + ref_name = 'InventoryProductSerializer' # Unique ref_name for inventory class PositionSerializer(serializers.ModelSerializer): class Meta: model = Position fields = '__all__' + ref_name = 'InventoryPositionSerializer' # Unique ref_name for inventory class EmployeeSerializer(serializers.ModelSerializer): class Meta: model = Employee fields = '__all__' + ref_name = 'InventoryEmployeeSerializer' # Unique ref_name for inventory class StorageLocationSerializer(serializers.ModelSerializer): class Meta: model = StorageLocation fields = '__all__' + ref_name = 'InventoryStorageLocationSerializer' # Unique ref_name for inventory class ContractorSerializer(serializers.ModelSerializer): class Meta: model = Contractor fields = '__all__' + ref_name = 'InventoryContractorSerializer' # Unique ref_name for inventory class SupplyContractSerializer(serializers.ModelSerializer): class Meta: model = SupplyContract fields = '__all__' + ref_name = 'InventorySupplyContractSerializer' # Unique ref_name for inventory class TruckSerializer(serializers.ModelSerializer): class Meta: model = Truck - fields = '__all__' \ No newline at end of file + fields = '__all__' + ref_name = 'InventoryTruckSerializer' # Unique ref_name for inventory diff --git a/inventory/urls.py b/inventory/urls.py index 20311a5..ed9c461 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -1,27 +1,29 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from rest_framework.permissions import AllowAny from .views import ( ProductViewSet, EmployeeViewSet, PositionViewSet, StorageLocationViewSet, ContractorViewSet, SupplyContractViewSet, TruckViewSet ) -from drf_yasg.views import get_schema_view -from drf_yasg import openapi -from rest_framework.permissions import AllowAny -router = DefaultRouter() -router.register(r'products', ProductViewSet) -router.register(r'employees', EmployeeViewSet) -router.register(r'positions', PositionViewSet) -router.register(r'storage-locations', StorageLocationViewSet) -router.register(r'contractors', ContractorViewSet) -router.register(r'supply-contracts', SupplyContractViewSet) -router.register(r'trucks', TruckViewSet) +# Create the router for inventory-related views +router_inventory = DefaultRouter() +router_inventory.register(r'products', ProductViewSet) +router_inventory.register(r'employees', EmployeeViewSet) +router_inventory.register(r'positions', PositionViewSet) +router_inventory.register(r'storage-locations', StorageLocationViewSet) +router_inventory.register(r'contractors', ContractorViewSet) +router_inventory.register(r'supply-contracts', SupplyContractViewSet) +router_inventory.register(r'trucks', TruckViewSet) -schema_view = get_schema_view( +# Swagger schema view for inventory +schema_view_inventory = get_schema_view( openapi.Info( - title="Store Management API", + title="Store Management API - Inventory", default_version='v1', - description="API for managing the franchise store", + description="API for managing inventory in the franchise store", terms_of_service="https://www.example.com/terms/", contact=openapi.Contact(email="support@example.com"), license=openapi.License(name="BSD License"), @@ -31,7 +33,7 @@ schema_view = get_schema_view( ) urlpatterns = [ - path('api/', include(router.urls)), - path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), -] \ No newline at end of file + path('', include(router_inventory.urls)), + path('swagger/', schema_view_inventory.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui-inventory'), + path('redoc/', schema_view_inventory.with_ui('redoc', cache_timeout=0), name='schema-redoc-inventory'), +] diff --git a/pricing/__init__.py b/pricing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pricing/admin.py b/pricing/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/pricing/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/pricing/apps.py b/pricing/apps.py new file mode 100644 index 0000000..237e8ef --- /dev/null +++ b/pricing/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PricingConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pricing' diff --git a/pricing/migrations/0001_initial.py b/pricing/migrations/0001_initial.py new file mode 100644 index 0000000..d9a75c6 --- /dev/null +++ b/pricing/migrations/0001_initial.py @@ -0,0 +1,113 @@ +# Generated by Django 5.1.4 on 2025-01-08 14:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Discount', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('discount_percentage', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Процент скидки')), + ('start_date', models.DateField(verbose_name='Дата начала')), + ('end_date', models.DateField(verbose_name='Дата окончания')), + ('description', models.TextField(blank=True, null=True, verbose_name='Описание акции')), + ], + ), + migrations.CreateModel( + name='PriceList', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('entry_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Входная цена')), + ('final_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Итоговая цена')), + ('date_effective', models.DateField(verbose_name='Дата вступления в силу')), + ('constraint_percent_limit', models.DecimalField(decimal_places=2, default=1000, max_digits=5, verbose_name='Лимит на наценку (%)')), + ('constraint_price_change', models.DecimalField(decimal_places=2, default=90, max_digits=5, verbose_name='Лимит на изменение цены (%)')), + ], + ), + migrations.CreateModel( + name='PriceType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(choices=[('regular', 'Регулярная'), ('discount', 'Скидочная'), ('promotional', 'Акционная')], max_length=50, verbose_name='Тип цены')), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Наименование товара')), + ('manufacturer_name', models.CharField(max_length=255, verbose_name='Производитель')), + ('manufacturer_country', models.CharField(max_length=255, verbose_name='Страна производителя')), + ('manufacturer_code', models.CharField(blank=True, max_length=50, null=True, verbose_name='Код производителя')), + ('dimensions', models.CharField(blank=True, max_length=255, null=True, verbose_name='Размеры')), + ('unit_of_measure', models.CharField(max_length=50, verbose_name='Единица измерения')), + ('shelf_life_days', models.IntegerField(verbose_name='Срок годности (дни)')), + ('barcode', models.CharField(max_length=50, unique=True, verbose_name='Штрихкод')), + ], + ), + migrations.CreateModel( + name='DiscountHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('old_discount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Старая скидка')), + ('new_discount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Новая скидка')), + ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Дата изменения скидки')), + ('reason', models.TextField(blank=True, null=True, verbose_name='Причина изменения скидки')), + ('discount', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.discount', verbose_name='Акция/Скидка')), + ], + ), + migrations.CreateModel( + name='PriceListWithDiscount', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('final_price_after_discount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Итоговая цена после скидки')), + ('discount', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.discount', verbose_name='Скидка')), + ('price_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.pricelist', verbose_name='Прайс-лист')), + ], + ), + migrations.AddField( + model_name='pricelist', + name='price_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.pricetype', verbose_name='Тип цены'), + ), + migrations.CreateModel( + name='PriceTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag_image', models.ImageField(blank=True, null=True, upload_to='price_tags/', verbose_name='Изображение ценника')), + ('price_effective_date', models.DateField(verbose_name='Дата вступления в силу ценника')), + ('price_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.pricelist', verbose_name='Прайс-лист')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.product', verbose_name='Товар')), + ], + ), + migrations.AddField( + model_name='pricelist', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.product', verbose_name='Товар'), + ), + migrations.CreateModel( + name='PriceChangeHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('old_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Старая цена')), + ('new_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Новая цена')), + ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Дата изменения цены')), + ('reason', models.TextField(blank=True, null=True, verbose_name='Причина изменения цены')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.product', verbose_name='Товар')), + ], + ), + migrations.AddField( + model_name='discount', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.product', verbose_name='Товар'), + ), + ] diff --git a/pricing/migrations/__init__.py b/pricing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pricing/models.py b/pricing/models.py new file mode 100644 index 0000000..bca45d2 --- /dev/null +++ b/pricing/models.py @@ -0,0 +1,108 @@ +from django.db import models + +# Справочник товаров +class Product(models.Model): + name = models.CharField(max_length=255, verbose_name="Наименование товара") + manufacturer_name = models.CharField(max_length=255, verbose_name="Производитель") + manufacturer_country = models.CharField(max_length=255, verbose_name="Страна производителя") + manufacturer_code = models.CharField(max_length=50, verbose_name="Код производителя", blank=True, null=True) + dimensions = models.CharField(max_length=255, verbose_name="Размеры", blank=True, null=True) + unit_of_measure = models.CharField(max_length=50, verbose_name="Единица измерения") + shelf_life_days = models.IntegerField(verbose_name="Срок годности (дни)") + barcode = models.CharField(max_length=50, unique=True, verbose_name="Штрихкод") + + def __str__(self): + return self.name + + +# Типы цен (например, регулярная, скидочная, акционная) +class PriceType(models.Model): + name = models.CharField(max_length=50, verbose_name="Тип цены", choices=[ + ('regular', 'Регулярная'), + ('discount', 'Скидочная'), + ('promotional', 'Акционная'), + ]) + + def __str__(self): + return self.name + + +# Прайс-листы (со всеми ценами) +class PriceList(models.Model): + product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name="Товар") + price_type = models.ForeignKey(PriceType, on_delete=models.CASCADE, verbose_name="Тип цены") + entry_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Входная цена") + final_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Итоговая цена") + date_effective = models.DateField(verbose_name="Дата вступления в силу") + constraint_percent_limit = models.DecimalField(max_digits=5, decimal_places=2, default=1000, verbose_name="Лимит на наценку (%)") + constraint_price_change = models.DecimalField(max_digits=5, decimal_places=2, default=90, verbose_name="Лимит на изменение цены (%)") + + def save(self, *args, **kwargs): + # Логика проверки на изменение цены + if self.pk: + old_price = PriceList.objects.get(pk=self.pk).final_price + price_change_percent = abs((self.final_price - old_price) / old_price) * 100 + if price_change_percent > self.constraint_price_change: + raise ValueError(f"Цена не может измениться более чем на {self.constraint_price_change}%") + + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.product.name} - {self.price_type.name} - {self.final_price} руб." + + +# Скидки и акции (могут применяться к товарам) +class Discount(models.Model): + product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name="Товар") + discount_percentage = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Процент скидки") + start_date = models.DateField(verbose_name="Дата начала") + end_date = models.DateField(verbose_name="Дата окончания") + description = models.TextField(verbose_name="Описание акции", blank=True, null=True) + + def __str__(self): + return f"Скидка {self.discount_percentage}% на {self.product.name} с {self.start_date} по {self.end_date}" + + +# История изменений цен +class PriceChangeHistory(models.Model): + product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name="Товар") + old_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Старая цена") + new_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Новая цена") + change_date = models.DateTimeField(auto_now_add=True, verbose_name="Дата изменения цены") + reason = models.TextField(verbose_name="Причина изменения цены", blank=True, null=True) + + def __str__(self): + return f"{self.product.name} - изменение цены с {self.old_price} на {self.new_price} - {self.change_date}" + + +# Пример использования ценников для печати +class PriceTag(models.Model): + product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name="Товар") + price_list = models.ForeignKey(PriceList, on_delete=models.CASCADE, verbose_name="Прайс-лист") + tag_image = models.ImageField(upload_to='price_tags/', verbose_name="Изображение ценника", blank=True, null=True) + price_effective_date = models.DateField(verbose_name="Дата вступления в силу ценника") + + def __str__(self): + return f"Ценник для {self.product.name} - {self.price_effective_date}" + + +# Связь с историей акций +class DiscountHistory(models.Model): + discount = models.ForeignKey(Discount, on_delete=models.CASCADE, verbose_name="Акция/Скидка") + old_discount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Старая скидка") + new_discount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Новая скидка") + change_date = models.DateTimeField(auto_now_add=True, verbose_name="Дата изменения скидки") + reason = models.TextField(verbose_name="Причина изменения скидки", blank=True, null=True) + + def __str__(self): + return f"Изменение скидки с {self.old_discount}% на {self.new_discount}% для {self.discount.product.name}" + + +# Пример применения скидки к прайс-листу +class PriceListWithDiscount(models.Model): + price_list = models.ForeignKey(PriceList, on_delete=models.CASCADE, verbose_name="Прайс-лист") + discount = models.ForeignKey(Discount, on_delete=models.CASCADE, verbose_name="Скидка") + final_price_after_discount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Итоговая цена после скидки") + + def __str__(self): + return f"{self.price_list.product.name} - {self.final_price_after_discount} руб. (с учетом скидки)" diff --git a/pricing/serializers.py b/pricing/serializers.py new file mode 100644 index 0000000..1336eab --- /dev/null +++ b/pricing/serializers.py @@ -0,0 +1,80 @@ +from rest_framework import serializers +from .models import Product, PriceType, PriceList, Discount, PriceChangeHistory, PriceTag, DiscountHistory, PriceListWithDiscount + +# Сериализатор для модели Product +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = ['id', 'name', 'manufacturer_name', 'manufacturer_country', 'manufacturer_code', 'dimensions', 'unit_of_measure', 'shelf_life_days', 'barcode'] + ref_name = 'PricingProductSerializer' # Unique ref_name for pricing + + +# Сериализатор для модели PriceType +class PriceTypeSerializer(serializers.ModelSerializer): + class Meta: + model = PriceType + fields = ['id', 'name'] + ref_name = 'PricingPriceTypeSerializer' # Unique ref_name for pricing + + +# Сериализатор для модели PriceList +class PriceListSerializer(serializers.ModelSerializer): + product = ProductSerializer() # Вложенный сериализатор для связи с продуктом + price_type = PriceTypeSerializer() # Вложенный сериализатор для связи с типом цены + + class Meta: + model = PriceList + fields = ['id', 'product', 'price_type', 'entry_price', 'final_price', 'date_effective', 'constraint_percent_limit', 'constraint_price_change'] + ref_name = 'PricingPriceListSerializer' # Unique ref_name for pricing + + +# Сериализатор для модели Discount +class DiscountSerializer(serializers.ModelSerializer): + product = ProductSerializer() # Вложенный сериализатор для связи с продуктом + + class Meta: + model = Discount + fields = ['id', 'product', 'discount_percentage', 'start_date', 'end_date', 'description'] + ref_name = 'PricingDiscountSerializer' # Unique ref_name for pricing + + +# Сериализатор для модели PriceChangeHistory +class PriceChangeHistorySerializer(serializers.ModelSerializer): + product = ProductSerializer() # Вложенный сериализатор для связи с продуктом + + class Meta: + model = PriceChangeHistory + fields = ['id', 'product', 'old_price', 'new_price', 'change_date', 'reason'] + ref_name = 'PricingPriceChangeHistorySerializer' # Unique ref_name for pricing + + +# Сериализатор для модели PriceTag +class PriceTagSerializer(serializers.ModelSerializer): + product = ProductSerializer() # Вложенный сериализатор для связи с продуктом + price_list = PriceListSerializer() # Вложенный сериализатор для связи с прайс-листом + + class Meta: + model = PriceTag + fields = ['id', 'product', 'price_list', 'tag_image', 'price_effective_date'] + ref_name = 'PricingPriceTagSerializer' # Unique ref_name for pricing + + +# Сериализатор для модели DiscountHistory +class DiscountHistorySerializer(serializers.ModelSerializer): + discount = DiscountSerializer() # Вложенный сериализатор для связи с акцией + + class Meta: + model = DiscountHistory + fields = ['id', 'discount', 'old_discount', 'new_discount', 'change_date', 'reason'] + ref_name = 'PricingDiscountHistorySerializer' # Unique ref_name for pricing + + +# Сериализатор для модели PriceListWithDiscount +class PriceListWithDiscountSerializer(serializers.ModelSerializer): + price_list = PriceListSerializer() # Вложенный сериализатор для связи с прайс-листом + discount = DiscountSerializer() # Вложенный сериализатор для связи с скидкой + + class Meta: + model = PriceListWithDiscount + fields = ['id', 'price_list', 'discount', 'final_price_after_discount'] + ref_name = 'PricingPriceListWithDiscountSerializer' # Unique ref_name for pricing diff --git a/pricing/tests.py b/pricing/tests.py new file mode 100644 index 0000000..a8087bb --- /dev/null +++ b/pricing/tests.py @@ -0,0 +1,46 @@ +from rest_framework.test import APITestCase +from rest_framework import status +from .models import Product +from django.urls import reverse + +class ProductAPITest(APITestCase): + + def setUp(self): + # Создание тестового продукта + self.product_data = { + 'name': 'Test Product', + 'manufacturer_name': 'Test Manufacturer', + 'manufacturer_country': 'Test Country', + 'unit_of_measure': 'pcs', + 'shelf_life_days': 365, + 'barcode': '123456789012', + } + self.product = Product.objects.create(**self.product_data) + self.url = reverse('product-list') # Замени на свой URL + + def test_create_product(self): + """Test creating a new product""" + response = self.client.post(self.url, self.product_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Product.objects.count(), 2) + self.assertEqual(Product.objects.latest('id').name, self.product_data['name']) + + def test_get_product(self): + """Test retrieving a product""" + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) # Только один продукт в базе + + def test_update_product(self): + """Test updating a product""" + updated_data = {'name': 'Updated Product'} + response = self.client.put(reverse('product-detail', args=[self.product.id]), updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.product.refresh_from_db() + self.assertEqual(self.product.name, 'Updated Product') + + def test_delete_product(self): + """Test deleting a product""" + response = self.client.delete(reverse('product-detail', args=[self.product.id])) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Product.objects.count(), 0) # Продукт должен быть удален diff --git a/pricing/urls.py b/pricing/urls.py new file mode 100644 index 0000000..1386bd9 --- /dev/null +++ b/pricing/urls.py @@ -0,0 +1,39 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from rest_framework.permissions import AllowAny +from .views import ( + PriceTypeViewSet, PriceListViewSet, DiscountViewSet, + PriceChangeHistoryViewSet, PriceTagViewSet, DiscountHistoryViewSet, PriceListWithDiscountViewSet +) + +# Create the router for pricing-related views +router_pricing = DefaultRouter() +router_pricing.register(r'price-types', PriceTypeViewSet) +router_pricing.register(r'price-lists', PriceListViewSet) +router_pricing.register(r'discounts', DiscountViewSet) +router_pricing.register(r'price-change-history', PriceChangeHistoryViewSet) +router_pricing.register(r'price-tags', PriceTagViewSet) +router_pricing.register(r'discount-history', DiscountHistoryViewSet) +router_pricing.register(r'price-lists-with-discounts', PriceListWithDiscountViewSet) + +# Swagger schema view for pricing +schema_view_pricing = get_schema_view( + openapi.Info( + title="Store Management API - Pricing", + default_version='v1', + description="API for managing pricing in the franchise store", + terms_of_service="https://www.example.com/terms/", + contact=openapi.Contact(email="support@example.com"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(AllowAny,), +) + +urlpatterns = [ + path('', include(router_pricing.urls)), + path('swagger/', schema_view_pricing.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui-pricing'), + path('redoc/', schema_view_pricing.with_ui('redoc', cache_timeout=0), name='schema-redoc-pricing'), +] diff --git a/pricing/views.py b/pricing/views.py new file mode 100644 index 0000000..acbf9d9 --- /dev/null +++ b/pricing/views.py @@ -0,0 +1,46 @@ +from rest_framework import viewsets +from .models import Product, PriceType, PriceList, Discount, PriceChangeHistory, PriceTag, DiscountHistory, PriceListWithDiscount +from .serializers import ( + ProductSerializer, PriceTypeSerializer, PriceListSerializer, DiscountSerializer, + PriceChangeHistorySerializer, PriceTagSerializer, DiscountHistorySerializer, PriceListWithDiscountSerializer +) + +# ViewSet для модели Product +class ProductViewSet(viewsets.ModelViewSet): + queryset = Product.objects.all() + serializer_class = ProductSerializer + +# ViewSet для модели PriceType +class PriceTypeViewSet(viewsets.ModelViewSet): + queryset = PriceType.objects.all() + serializer_class = PriceTypeSerializer + +# ViewSet для модели PriceList +class PriceListViewSet(viewsets.ModelViewSet): + queryset = PriceList.objects.all() + serializer_class = PriceListSerializer + +# ViewSet для модели Discount +class DiscountViewSet(viewsets.ModelViewSet): + queryset = Discount.objects.all() + serializer_class = DiscountSerializer + +# ViewSet для модели PriceChangeHistory +class PriceChangeHistoryViewSet(viewsets.ModelViewSet): + queryset = PriceChangeHistory.objects.all() + serializer_class = PriceChangeHistorySerializer + +# ViewSet для модели PriceTag +class PriceTagViewSet(viewsets.ModelViewSet): + queryset = PriceTag.objects.all() + serializer_class = PriceTagSerializer + +# ViewSet для модели DiscountHistory +class DiscountHistoryViewSet(viewsets.ModelViewSet): + queryset = DiscountHistory.objects.all() + serializer_class = DiscountHistorySerializer + +# ViewSet для модели PriceListWithDiscount +class PriceListWithDiscountViewSet(viewsets.ModelViewSet): + queryset = PriceListWithDiscount.objects.all() + serializer_class = PriceListWithDiscountSerializer diff --git a/settings/__pycache__/__init__.cpython-310.pyc b/settings/__pycache__/__init__.cpython-310.pyc index c0f6ea35189c4f5ccd7ac3531be34a3d4848bb27..d653309f64c6ca3750ec2608389e2b34d1745259 100644 GIT binary patch delta 75 zcmbQqxRjAQpO=@50SK-})=lKLG4asP$j?pHPf0Aw)_2KIF3nBND=F4@&n)pMP0}we b$uCOP%}vZpOiu+#=oVKNm!#%S%+~?{V@DZ% delta 63 zcmZ3=IFpe(pO=@50SFRpYbSEsXxQk77N-^!>!&0ZWtJA}yW}UA=BDPA6ziuICFUh( RWEQ8!7nkH0rB1BY0suHc6te&T diff --git a/settings/__pycache__/base.cpython-310.pyc b/settings/__pycache__/base.cpython-310.pyc index 77d06b74496e62d448701a6c9d26c0c6b87bf333..db58c1b2014eb0c28d9bdf9c6bf0855956254548 100644 GIT binary patch literal 1553 zcma)6OK;mo5GLuhM7?b1(FU#2d)2^H6S~c%D9VU*B1A8UOw;m0P~5dli?8f1tVrM>plzYr8~hmu?ivVoQh&d%&S_M6$+ORlJsXOY%&Bf{LuN^n16dO&sV&Gm&h(-WN@a~*qUI@LuLlT$yB zc<~uN_mf$w;i)$DgE>hs)3TKD871s>1i+VS4^Q33n#~!Z?mic~$%y zimQZ*Pq#gmH+yxV_uw*e@3aIBmu>FW!sHEGdsp z)sJ=C>P{@jdtv=3YNp{BWe{7`9BVM{8xFAbLC-Y%)+=LTiM`$sBJ}%~=|M-^qFF{Q zx9N_CCx1xv5Fjqe2eWgQ#-xLNI`5cikjH?Kb)JR%c|L_#E~Vrs_7k{CrQ(P!7`zo7 zMmUGuS!e1qqGb!AeiNrSkI1(&1ZKxTn$m%S6jiP$=n^4iN0F}w`hYH#Rw=8Ba*6&% Lm+FC9SDXI;n3NBh delta 190 zcmbQp^NoowpO=@50SHnZYSZsAPUMqOQvmW(7*ZHhm{OQiSW;M1*izW%uw^nvF{E&$ zGe$9{a0WAIa!vf)7Z=5unOByYSCU^;>8Htbi_^t1#L>wy*fsbTpQm4Nh@+2>t4q9N zKtS+Hh9Zz9VB(jBerR!OQL%nXVo_#kvA#=wa%nEmkYfF`qQt!9jLhQH_+p@usgrlI WDoBX|wJ|XwLzZ7G|Cxk1egXier!xuw diff --git a/settings/__pycache__/dev.cpython-310.pyc b/settings/__pycache__/dev.cpython-310.pyc index 7cd880a83f81c3a14e467aab070879bb46b63381..ab580c7395867e8c7b6ca8e35860fc20b241e753 100644 GIT binary patch literal 269 zcmYjLO-sW-5Z%oeQAmG-*9dk0fQZ$URw!*D5wRZ1))`~5n}pqskc+>iJ^F*Z?A4Qh zp(iJvd@yh3y*CU~C&?)zd_8`YPt?DA@gF{kYbtX_1Pu6&6%4!;2fySi5B+@r!GUuI zVb0R%htpch9X~I$N27~DzpOiH%6S^PU?UB>FdN_8Ph2vZ%@z;iY&~7%MeYW7^SmI7 zjL~wLyGS+-R`AL63Elalg`kf|Itj>E=NCF^Gc*(X-AMWZB@8KUD61