Artem-Darius Weber 1 day ago
commit 32880a2178

5
.gitignore vendored

@ -1,3 +1,8 @@
**/__pycache__/
*.pyc
*.pyo
*.log
inventory/migrations/__pycache__/0001_initial.cpython-310.pyc

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,6 @@
from django.apps import AppConfig
class HRConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hr'

@ -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

@ -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']

@ -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'),
]

@ -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

@ -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',
),
]

@ -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__'
fields = '__all__'
ref_name = 'InventoryTruckSerializer' # Unique ref_name for inventory

@ -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'),
]
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'),
]

@ -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

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PricingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'pricing'

@ -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='Товар'),
),
]

@ -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} руб. (с учетом скидки)"

@ -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

@ -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) # Продукт должен быть удален

@ -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'),
]

@ -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

@ -57,8 +57,10 @@ INSTALLED_APPS = [
'rest_framework',
'inventory',
'warehouse',
'hr',
'product_directory',
'goods_reception',
'pricing',
]
ROOT_URLCONF = 'urls'

@ -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')),
]
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')),
]

Loading…
Cancel
Save