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 1/4] 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 Date: Wed, 8 Jan 2025 19:19:40 +0300 Subject: [PATCH 2/4] feat addded fully task 3 --- .gitignore | 1 + inventory/__pycache__/models.cpython-310.pyc | Bin 7147 -> 6357 bytes ...roduct_alter_inventory_product_and_more.py | 33 ++++++++++++++++++ logs.log | 25 +++++++++++++ pricing/models.py | 22 +++++++++++- settings/__pycache__/base.cpython-310.pyc | Bin 1553 -> 1866 bytes 6 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 inventory/migrations/0003_alter_stockoperation_product_alter_inventory_product_and_more.py diff --git a/.gitignore b/.gitignore index ebd7ddf..bb04232 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc __pycache__/* inventory/migrations/__pycache__/0001_initial.cpython-310.pyc +*.log \ No newline at end of file diff --git a/inventory/__pycache__/models.cpython-310.pyc b/inventory/__pycache__/models.cpython-310.pyc index a87835cac4db81ee7ffb9b0b17f835dd30010887..b9b8b024547fedc9d59ad440949bdb1857b95391 100644 GIT binary patch delta 2018 zcma)7O>7%g5cYFo@7lZTpS2x3v6I+I8i+y?5i3m)0!ou6l$6#aEouysal9MH#6Rit z`j7|_0u>zSq1C7tBq|U>h#M$WDu)UoDskON<>C{^2E>UAw=lCdO@g!)mUi^sH#5&O z-+cS_kLh2Z_0<%`>%iYjpS@T7B7V!4VCJpS!5z(^b(Gx|AuBM)f6&8_JaYAQe#5u~B_#pZI#MhK&~YW1KnnhHqH(kBK&DnN8KqivJeR8*JeRvTB7x93 zwG%;qQ=ZccrAod$U((B3R$8w$czG+w^M>A(&sPn-sPmRfsnn{*I{GN2&U-MK$4;7V z+6ri*_|$0*qQl1mOlGpv!V3sJ{=w%F1q9(IrP*0+l*?Q~4;SH~FvvXcw=kyC0ZrFx zIbYr2L*$fTN?i0d-HmGLT0_U}$Kq-CaLe88|_o|9+PQ~*u8M}hdk~5BGJGyMt zc)qA#s1@=jC+p$I8iFx7dbXdktWkFwTbsK5;L;fQ96s&AiOlE1(K&eQ{)|FfkFlxMZ1&Z4|{zJQG#=~O=6 z4TaKpc&j6Lr1U>MPWuTmwzHv@=3&COmmH)xQduK_#=K+xXx`i3HNQ3QoA=DSJP1zz z%aWx+OH)H|(SNm%n6sV88}PtC9681~1zRKUAxy&1djUz(6_Jq!1Qqxtb zX?mF_X)PA=oB2X1Wo+4#JBxvD?j_X~#-?C9^dbww51};POMixjq!x=9fih2oSDsAe zCO~*tIz}Zt2&cwJDceov44DiW(rHsHx24}w!zFU0AQYLk=X?QU3VTLmiE90&tM^}~ zfA2~n(^52p#lIDu4*TgWQb<$K^tF7WY+(IgN9#|n{{xtb^)DQ&9}mc?nWgiy=W>@< zGUsx$uUx|Qa$6qsAOGpglJyEXl5i)sX5Yvaj4@!K`#UW7VfXDpOQK7ZXm@kmfEzuj z^vUAv3n7BJbzTa7>*=#EB#VI`K_VV!D$K;6pUlQKTi>djR^mFn%3I@>pz2nyP1kFd ry66kR>B_{b<&*e8;OK6)R^4zv-V>IcYAE1UW2&NhRY?`qV3+z230;@{ delta 2694 zcma)7-ES0C6yLiaGqbZZv$NA}cctxaKcKeChkQ%WMk!Ffgr<~WCzxf~nYIht-FkN> zraVxJXf)a&UWkb%B8iEb_)skk6{W=Z2QX`b59^chiO7S|#3u}%GhOZ0bQ?O^lY4*X z+;e~D!6+{MPN5s$o z(khSwP@NG4Bw9hjfYciefHYJP)rcMSbj9B%o7hd?Q|zo9o*;(b@J)Gj&h(x38wB{{ zj(2w)a)&QC<=26~s~7)6f)$&PdqRXA#9_!&M3*WW!BC9wluwrp)reg5=!&7i^>t;w zj}{C&W8Ht`@q0Y>8ule=l1dkxdFN|qx^&)|W@XYeVXuOeJJ(=8@60(jK{xN*aOS{n z*7>&duKgscua_?10q3?e-QwJ-VSDNRCa|?z@xX`ZWd+lLEHL|p zKX2!9wt@~mf!l66-#N3T^QCtdd|pLfKw+-*CelE%t{#XYc*$E5iFGxTtA#LNw)a@ip> zWu(W8I)ADsP00$FwA(LKOw_tVM z*xy2I*Fw$kTE8_uwnF!90h_JxtJFv!O2F;8El{H$w(Owz7xvHqwTFsYFQDi{ft^|w za+T&^m`X=bkzl_@dTJ-V7i>~9yS0Ag`axauzp1`e*8-{jxvme$uZ?bxwxh4rcnNQq zLuor_!ApHDT3ng`IVRL6d#m_^16}Lu-`}$*H89w>C)Km>2&|7T^zd?VxleTvFJHs* z^+UA-IR-Xk?2r1dfbgdcSKHkl?Z<<)hW%UYM67l5QgXG4V1lXZFTti`iP}VTFucGX z#2QJ23Gtora^NMmecXcK(5E;X=@w?k+q#$emsb{^gMPDfW;X%5BEDhC6D68YQ&drl OYqBP4Ax+RC4cb5Bjf!*t diff --git a/inventory/migrations/0003_alter_stockoperation_product_alter_inventory_product_and_more.py b/inventory/migrations/0003_alter_stockoperation_product_alter_inventory_product_and_more.py new file mode 100644 index 0000000..a8ce40c --- /dev/null +++ b/inventory/migrations/0003_alter_stockoperation_product_alter_inventory_product_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.4 on 2025-01-08 16:17 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0002_contractor_position_truck_employee_supplycontract'), + ('product_directory', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='stockoperation', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product_directory.product', verbose_name='Товар'), + ), + migrations.AlterField( + model_name='inventory', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product_directory.product', verbose_name='Товар'), + ), + migrations.AlterField( + model_name='pricelist', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product_directory.product', verbose_name='Товар'), + ), + migrations.DeleteModel( + name='Product', + ), + ] diff --git a/logs.log b/logs.log index 92ab0b8..1fcc9e7 100644 --- a/logs.log +++ b/logs.log @@ -133,3 +133,28 @@ Watching for file changes with StatReloader "GET /admin/jsi18n/ HTTP/1.1" 200 3342 "GET /admin/warehouse/stockoperation/statistics/ HTTP/1.1" 200 28103 "GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 15416 +Watching for file changes with StatReloader +Not Found: / +"GET / HTTP/1.1" 404 2782 +"GET /api/pricing/ HTTP/1.1" 200 11324 +"GET /static/rest_framework/css/bootstrap.min.css HTTP/1.1" 304 0 +"GET /static/rest_framework/css/prettify.css HTTP/1.1" 304 0 +"GET /static/rest_framework/css/bootstrap-tweaks.css HTTP/1.1" 304 0 +"GET /static/rest_framework/js/csrf.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/jquery-3.7.1.min.js HTTP/1.1" 304 0 +"GET /static/rest_framework/css/default.css HTTP/1.1" 304 0 +"GET /static/rest_framework/js/bootstrap.min.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/ajax-form.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/prettify-min.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/default.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/load-ajax-form.js HTTP/1.1" 304 0 +"GET /static/rest_framework/img/grid.png HTTP/1.1" 304 0 +"GET /api/pricing/swagger/ HTTP/1.1" 200 2245 +"GET /static/drf-yasg/style.css HTTP/1.1" 304 0 +"GET /static/drf-yasg/swagger-ui-dist/swagger-ui.css HTTP/1.1" 304 0 +"GET /static/drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/immutable.min.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/swagger-ui-dist/swagger-ui-bundle.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/insQ.min.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/swagger-ui-init.js HTTP/1.1" 304 0 +"GET /api/pricing/swagger/?format=openapi HTTP/1.1" 200 44272 diff --git a/pricing/models.py b/pricing/models.py index bca45d2..246d9ae 100644 --- a/pricing/models.py +++ b/pricing/models.py @@ -1,4 +1,7 @@ from django.db import models +from io import BytesIO +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas # Справочник товаров class Product(models.Model): @@ -38,7 +41,6 @@ class PriceList(models.Model): 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 @@ -81,6 +83,19 @@ class PriceTag(models.Model): 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 generate_pdf(self): + """Генерация ценника в формате PDF.""" + buffer = BytesIO() + c = canvas.Canvas(buffer, pagesize=letter) + c.drawString(100, 750, f"Продукт: {self.product.name}") + c.drawString(100, 730, f"Тип цены: {self.price_list.price_type.name}") + c.drawString(100, 710, f"Входная цена: {self.price_list.entry_price} руб.") + c.drawString(100, 690, f"Итоговая цена: {self.price_list.final_price} руб.") + c.showPage() + c.save() + buffer.seek(0) + return buffer def __str__(self): return f"Ценник для {self.product.name} - {self.price_effective_date}" @@ -104,5 +119,10 @@ class PriceListWithDiscount(models.Model): 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 save(self, *args, **kwargs): + # Расчёт итоговой цены с учётом скидки + self.final_price_after_discount = self.price_list.final_price - (self.price_list.final_price * self.discount.discount_percentage / 100) + super().save(*args, **kwargs) + def __str__(self): return f"{self.price_list.product.name} - {self.final_price_after_discount} руб. (с учетом скидки)" diff --git a/settings/__pycache__/base.cpython-310.pyc b/settings/__pycache__/base.cpython-310.pyc index db58c1b2014eb0c28d9bdf9c6bf0855956254548..041679a8fac4b0ef5f677d7791f157e80fb5a261 100644 GIT binary patch delta 401 zcmY*T%}&BV5T@;x76hsoBP#w~B#j9-6Q2MiF>vrgdZ4M@wUpFuvs+L})c6Rb*ZKe& z59(uh@B#J>ym<6rVsr>x+{y0DH{Z82-=y$XD$!ywpMlf6zwtixNvSG5Pxh}5@hYz1 zHN1{Da5YtY30FJ`uz@#II?lhaNcJm>_ mY9L^>t#MY&mpvh)0%|M~-lSbn(VRPCi2NJ)=bo{WeH}`6yNwDj+v2jvOUWtLH2^8%;e0x p^jj>YMLETOn*5V*v997=$xtK>)LJAxc^X>;qsip=Y^F>cJOC_^7`Xrd From 38ddec84cecf87bc33a354398586dff45f456554 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 19:59:38 +0300 Subject: [PATCH 3/4] feat: added HR --- hr/__init__.py | 0 hr/admin.py | 3 ++ hr/apps.py | 6 ++++ hr/models.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++ hr/serializers.py | 40 +++++++++++++++++++++ hr/tests.py | 0 hr/urls.py | 37 +++++++++++++++++++ hr/views.py | 31 ++++++++++++++++ urls.py | 1 + 9 files changed, 210 insertions(+) create mode 100644 hr/__init__.py create mode 100644 hr/admin.py create mode 100644 hr/apps.py create mode 100644 hr/models.py create mode 100644 hr/serializers.py create mode 100644 hr/tests.py create mode 100644 hr/urls.py create mode 100644 hr/views.py diff --git a/hr/__init__.py b/hr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hr/admin.py b/hr/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/hr/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/hr/apps.py b/hr/apps.py new file mode 100644 index 0000000..339a3b6 --- /dev/null +++ b/hr/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HRConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'hr' diff --git a/hr/models.py b/hr/models.py new file mode 100644 index 0000000..cbfbb1e --- /dev/null +++ b/hr/models.py @@ -0,0 +1,92 @@ +import datetime +from django.db import models +from io import BytesIO +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas + +# Employee Directory +class Employee(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + position = models.CharField(max_length=100) + email = models.EmailField(unique=True) + hired_date = models.DateField() + work_schedule = models.JSONField(default=dict) + + def __str__(self): + return f"{self.first_name} {self.last_name}" + +# Time Tracking System +class WorkTimeLog(models.Model): + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + date = models.DateField() + check_in = models.TimeField(null=True, blank=True) + check_out = models.TimeField(null=True, blank=True) + worked_hours = models.FloatField(null=True, blank=True) + + def calculate_worked_hours(self): + if self.check_in and self.check_out: + delta = datetime.combine(datetime.date.min, self.check_out) - datetime.combine(datetime.date.min, self.check_in) + self.worked_hours = delta.total_seconds() / 3600 + else: + self.worked_hours = 0 + + def save(self, *args, **kwargs): + self.calculate_worked_hours() + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.employee}: {self.date}" + +# Leave Management +class Leave(models.Model): + LEAVE_TYPE_CHOICES = [ + ("vacation", "Vacation"), + ("sick", "Sick Leave"), + ] + + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + leave_type = models.CharField(max_length=10, choices=LEAVE_TYPE_CHOICES) + start_date = models.DateField() + end_date = models.DateField() + reason = models.TextField(blank=True) + + def __str__(self): + return f"{self.employee}: {self.leave_type} ({self.start_date} - {self.end_date})" + + def duration(self): + return (self.end_date - self.start_date).days + 1 + +# Overtime Reporting +class OvertimeReport(models.Model): + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + date = models.DateField() + worked_hours = models.FloatField() + required_hours = models.FloatField(default=8.0) + overtime = models.FloatField() + comment = models.TextField(blank=True) + + def calculate_overtime(self): + self.overtime = self.worked_hours - self.required_hours + + def save(self, *args, **kwargs): + self.calculate_overtime() + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.employee}: {self.overtime} hours on {self.date}" + +# PDF Generation for HR Reports +class Report(models.Model): + employee = models.ForeignKey(Employee, on_delete=models.CASCADE) + report_date = models.DateField(auto_now_add=True) + + def generate_pdf(self): + buffer = BytesIO() + c = canvas.Canvas(buffer, pagesize=letter) + c.drawString(100, 750, f"Employee: {self.employee.first_name} {self.employee.last_name}") + c.drawString(100, 730, f"Date: {self.report_date}") + c.showPage() + c.save() + buffer.seek(0) + return buffer diff --git a/hr/serializers.py b/hr/serializers.py new file mode 100644 index 0000000..564c5b2 --- /dev/null +++ b/hr/serializers.py @@ -0,0 +1,40 @@ +from rest_framework import serializers +from .models import Employee, WorkTimeLog, Leave, OvertimeReport, Report + +# Сериализатор для модели Employee +class EmployeeSerializer(serializers.ModelSerializer): + class Meta: + model = Employee + fields = ['id', 'first_name', 'last_name', 'position', 'email', 'hired_date', 'work_schedule'] + +# Сериализатор для модели WorkTimeLog +class WorkTimeLogSerializer(serializers.ModelSerializer): + employee = EmployeeSerializer() # Вложенный сериализатор для сотрудника + + class Meta: + model = WorkTimeLog + fields = ['id', 'employee', 'date', 'check_in', 'check_out', 'worked_hours'] + +# Сериализатор для модели Leave +class LeaveSerializer(serializers.ModelSerializer): + employee = EmployeeSerializer() # Вложенный сериализатор для сотрудника + + class Meta: + model = Leave + fields = ['id', 'employee', 'leave_type', 'start_date', 'end_date', 'reason'] + +# Сериализатор для модели OvertimeReport +class OvertimeReportSerializer(serializers.ModelSerializer): + employee = EmployeeSerializer() # Вложенный сериализатор для сотрудника + + class Meta: + model = OvertimeReport + fields = ['id', 'employee', 'date', 'worked_hours', 'required_hours', 'overtime', 'comment'] + +# Сериализатор для модели Report +class ReportSerializer(serializers.ModelSerializer): + employee = EmployeeSerializer() # Вложенный сериализатор для сотрудника + + class Meta: + model = Report + fields = ['id', 'employee', 'report_date'] diff --git a/hr/tests.py b/hr/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/hr/urls.py b/hr/urls.py new file mode 100644 index 0000000..6d88889 --- /dev/null +++ b/hr/urls.py @@ -0,0 +1,37 @@ +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 ( + EmployeeViewSet, WorkTimeLogViewSet, LeaveViewSet, + OvertimeReportViewSet, ReportViewSet +) + +# Create the router for HR-related views +router_hr = DefaultRouter() +router_hr.register(r'employees', EmployeeViewSet) +router_hr.register(r'work-time-logs', WorkTimeLogViewSet) +router_hr.register(r'leaves', LeaveViewSet) +router_hr.register(r'overtime-reports', OvertimeReportViewSet) +router_hr.register(r'reports', ReportViewSet) + +# Swagger schema view for HR +schema_view_hr = get_schema_view( + openapi.Info( + title="Human Resources API", + default_version='v1', + description="API for managing HR-related data and operations", + 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_hr.urls)), + path('swagger/', schema_view_hr.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui-hr'), + path('redoc/', schema_view_hr.with_ui('redoc', cache_timeout=0), name='schema-redoc-hr'), +] diff --git a/hr/views.py b/hr/views.py new file mode 100644 index 0000000..f958047 --- /dev/null +++ b/hr/views.py @@ -0,0 +1,31 @@ +from rest_framework import viewsets +from .models import Employee, WorkTimeLog, Leave, OvertimeReport, Report +from .serializers import ( + EmployeeSerializer, WorkTimeLogSerializer, LeaveSerializer, + OvertimeReportSerializer, ReportSerializer +) + +# ViewSet для модели Employee +class EmployeeViewSet(viewsets.ModelViewSet): + queryset = Employee.objects.all() + serializer_class = EmployeeSerializer + +# ViewSet для модели WorkTimeLog +class WorkTimeLogViewSet(viewsets.ModelViewSet): + queryset = WorkTimeLog.objects.all() + serializer_class = WorkTimeLogSerializer + +# ViewSet для модели Leave +class LeaveViewSet(viewsets.ModelViewSet): + queryset = Leave.objects.all() + serializer_class = LeaveSerializer + +# ViewSet для модели OvertimeReport +class OvertimeReportViewSet(viewsets.ModelViewSet): + queryset = OvertimeReport.objects.all() + serializer_class = OvertimeReportSerializer + +# ViewSet для модели Report +class ReportViewSet(viewsets.ModelViewSet): + queryset = Report.objects.all() + serializer_class = ReportSerializer diff --git a/urls.py b/urls.py index 49a048e..022f400 100644 --- a/urls.py +++ b/urls.py @@ -7,4 +7,5 @@ urlpatterns = [ path('api/pricing/', include('pricing.urls')), # Pricing URLs path('api/goods-reception/', include('goods_reception.urls')), path('api/warehouse/', include('warehouse.urls')), + path('api/hr/', include('hr.urls')), ] From fae08514607270546d1654be8834144409b5fe25 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 20:01:41 +0300 Subject: [PATCH 4/4] feat fixed HR --- logs.log | 28 ++++++++++++++++++++++ settings/__pycache__/base.cpython-310.pyc | Bin 1866 -> 1870 bytes settings/base.py | 1 + 3 files changed, 29 insertions(+) diff --git a/logs.log b/logs.log index 1fcc9e7..11ff944 100644 --- a/logs.log +++ b/logs.log @@ -158,3 +158,31 @@ Not Found: / "GET /static/drf-yasg/insQ.min.js HTTP/1.1" 304 0 "GET /static/drf-yasg/swagger-ui-init.js HTTP/1.1" 304 0 "GET /api/pricing/swagger/?format=openapi HTTP/1.1" 200 44272 +Watching for file changes with StatReloader +Watching for file changes with StatReloader +Not Found: / +"GET / HTTP/1.1" 404 2932 +Not Found: /favicon.ico +"GET /api/hr/ HTTP/1.1" 200 10827 +"GET /static/rest_framework/css/bootstrap.min.css HTTP/1.1" 304 0 +"GET /static/rest_framework/css/bootstrap-tweaks.css HTTP/1.1" 304 0 +"GET /static/rest_framework/js/csrf.js HTTP/1.1" 304 0 +"GET /static/rest_framework/css/prettify.css HTTP/1.1" 304 0 +"GET /static/rest_framework/js/jquery-3.7.1.min.js HTTP/1.1" 304 0 +"GET /static/rest_framework/css/default.css HTTP/1.1" 304 0 +"GET /static/rest_framework/js/prettify-min.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/ajax-form.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/bootstrap.min.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/default.js HTTP/1.1" 304 0 +"GET /static/rest_framework/js/load-ajax-form.js HTTP/1.1" 304 0 +"GET /static/rest_framework/img/grid.png HTTP/1.1" 304 0 +"GET /api/hr/swagger HTTP/1.1" 301 0 +"GET /api/hr/swagger/ HTTP/1.1" 200 2229 +"GET /static/drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/insQ.min.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/swagger-ui-dist/swagger-ui-bundle.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/swagger-ui-init.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/style.css HTTP/1.1" 200 1047 +"GET /static/drf-yasg/immutable.min.js HTTP/1.1" 304 0 +"GET /static/drf-yasg/swagger-ui-dist/swagger-ui.css HTTP/1.1" 200 145206 +"GET /api/hr/swagger/?format=openapi HTTP/1.1" 200 54931 diff --git a/settings/__pycache__/base.cpython-310.pyc b/settings/__pycache__/base.cpython-310.pyc index 041679a8fac4b0ef5f677d7791f157e80fb5a261..e43ac90accc892b74a2165967fd13e78d01d7020 100644 GIT binary patch delta 91 zcmX@bcaD!YpO=@50SJQD*QNj1$ZN>L$UE7dWiK;RM$zQoEZx$=K)G9-1x5Jb%7 delta 66 zcmX@dcZ!cUpO=@50SMye)}_DO$ZN>L$TQiVW$z@`E=Hlr-K>&~ypxx*YBGvWKF@lT UO&zGPNPY4wwthzb$y)3x095=DTmS$7 diff --git a/settings/base.py b/settings/base.py index 21adf95..04bdeb4 100644 --- a/settings/base.py +++ b/settings/base.py @@ -57,6 +57,7 @@ INSTALLED_APPS = [ 'rest_framework', 'inventory', 'warehouse', + 'hr', 'product_directory', 'goods_reception', 'pricing',