diff --git a/.gitignore b/.gitignore index eb42055..f103890 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ **/__pycache__/ + *.pyc *.pyo + +*.log + +inventory/migrations/__pycache__/0001_initial.cpython-310.pyc \ No newline at end of file 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/inventory/__pycache__/__init__.cpython-310.pyc b/inventory/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..cbdfa45 Binary files /dev/null and b/inventory/__pycache__/__init__.cpython-310.pyc differ diff --git a/inventory/__pycache__/apps.cpython-310.pyc b/inventory/__pycache__/apps.cpython-310.pyc new file mode 100644 index 0000000..978f291 Binary files /dev/null and b/inventory/__pycache__/apps.cpython-310.pyc differ diff --git a/inventory/__pycache__/models.cpython-310.pyc b/inventory/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..b9b8b02 Binary files /dev/null and b/inventory/__pycache__/models.cpython-310.pyc differ 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/inventory/migrations/__pycache__/0001_initial.cpython-310.pyc b/inventory/migrations/__pycache__/0001_initial.cpython-310.pyc new file mode 100644 index 0000000..c4ea00a Binary files /dev/null and b/inventory/migrations/__pycache__/0001_initial.cpython-310.pyc differ diff --git a/inventory/migrations/__pycache__/__init__.cpython-310.pyc b/inventory/migrations/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..0a89eba Binary files /dev/null and b/inventory/migrations/__pycache__/__init__.cpython-310.pyc differ 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/logs.log b/logs.log index bd1c44a..690227c 100644 --- a/logs.log +++ b/logs.log @@ -134,6 +134,7 @@ Watching for file changes with StatReloader "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 +<<<<<<< HEAD "GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 15416 "GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 15416 "GET /admin/jsi18n/ HTTP/1.1" 200 3342 @@ -297,3 +298,57 @@ Not Found: /favicon.ico "GET /api/swagger/?format=openapi HTTP/1.1" 200 23941 "GET /static/drf-yasg/swagger-ui-dist/favicon-32x32.png HTTP/1.1" 200 628 "GET /static/drf-yasg/swagger-ui-dist/favicon-32x32.png HTTP/1.1" 200 628 +======= +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 +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 +>>>>>>> fae08514607270546d1654be8834144409b5fe25 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..246d9ae --- /dev/null +++ b/pricing/models.py @@ -0,0 +1,128 @@ +from django.db import models +from io import BytesIO +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas + +# Справочник товаров +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 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}" + + +# Связь с историей акций +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 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/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 new file mode 100644 index 0000000..d653309 Binary files /dev/null and b/settings/__pycache__/__init__.cpython-310.pyc differ diff --git a/settings/__pycache__/base.cpython-310.pyc b/settings/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..e43ac90 Binary files /dev/null and b/settings/__pycache__/base.cpython-310.pyc differ diff --git a/settings/__pycache__/dev.cpython-310.pyc b/settings/__pycache__/dev.cpython-310.pyc new file mode 100644 index 0000000..ab580c7 Binary files /dev/null and b/settings/__pycache__/dev.cpython-310.pyc differ diff --git a/settings/base.py b/settings/base.py index dd0f478..04bdeb4 100644 --- a/settings/base.py +++ b/settings/base.py @@ -57,8 +57,10 @@ INSTALLED_APPS = [ 'rest_framework', 'inventory', 'warehouse', + 'hr', 'product_directory', 'goods_reception', + 'pricing', ] ROOT_URLCONF = 'urls' diff --git a/urls.py b/urls.py index f7a79b1..022f400 100644 --- a/urls.py +++ b/urls.py @@ -3,7 +3,9 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), - path('api/', include('inventory.urls')), - path('goods-reception/', include('goods_reception.urls')), - path('warehouse/', include('warehouse.urls')), -] \ No newline at end of file + path('api/inventory/', include('inventory.urls')), # Inventory URLs + 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')), +]