diff --git a/goods_reception/__init__.py b/goods_reception/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/goods_reception/admin.py b/goods_reception/admin.py
new file mode 100644
index 0000000..ec33eac
--- /dev/null
+++ b/goods_reception/admin.py
@@ -0,0 +1,8 @@
+from django.contrib import admin
+from .models import GoodsReception
+
+@admin.register(GoodsReception)
+class GoodsReceptionAdmin(admin.ModelAdmin):
+ list_display = ('product', 'quantity', 'quality_check', 'shelf_life_valid', 'is_accepted', 'rejection_reason')
+ search_fields = ('product__name',)
+ list_filter = ('is_accepted', 'quality_check', 'shelf_life_valid')
diff --git a/goods_reception/api_views.py b/goods_reception/api_views.py
new file mode 100644
index 0000000..8192e8c
--- /dev/null
+++ b/goods_reception/api_views.py
@@ -0,0 +1,23 @@
+from rest_framework.viewsets import ModelViewSet
+from .models import GoodsReception
+from .serializers import GoodsReceptionSerializer
+from warehouse.models import Stock
+
+class GoodsReceptionViewSet(ModelViewSet):
+ """
+ API для работы с приемкой товаров.
+ """
+ queryset = GoodsReception.objects.all()
+ serializer_class = GoodsReceptionSerializer
+
+ def perform_create(self, serializer):
+ reception = serializer.save()
+ if reception.is_accepted:
+ # Добавление на склад
+ stock, created = Stock.objects.get_or_create(
+ product=reception.product,
+ location="Основной склад",
+ defaults={'quantity': 0},
+ )
+ stock.quantity += reception.quantity
+ stock.save()
diff --git a/goods_reception/apps.py b/goods_reception/apps.py
new file mode 100644
index 0000000..5e7dce9
--- /dev/null
+++ b/goods_reception/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class GoodsReceptionConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'goods_reception'
diff --git a/goods_reception/forms.py b/goods_reception/forms.py
new file mode 100644
index 0000000..468f714
--- /dev/null
+++ b/goods_reception/forms.py
@@ -0,0 +1,13 @@
+from django import forms
+from .models import GoodsReception
+
+class GoodsReceptionForm(forms.ModelForm):
+ class Meta:
+ model = GoodsReception
+ fields = '__all__'
+
+ def clean(self):
+ cleaned_data = super().clean()
+ if not cleaned_data.get('quality_check') or not cleaned_data.get('shelf_life_valid'):
+ cleaned_data['is_accepted'] = False
+ return cleaned_data
\ No newline at end of file
diff --git a/goods_reception/migrations/0001_initial.py b/goods_reception/migrations/0001_initial.py
new file mode 100644
index 0000000..9ee65e7
--- /dev/null
+++ b/goods_reception/migrations/0001_initial.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.1.4 on 2025-01-08 14:37
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('product_directory', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='GoodsReception',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('quantity', models.PositiveIntegerField(verbose_name='Количество')),
+ ('quality_check', models.BooleanField(verbose_name='Проверка качества')),
+ ('shelf_life_valid', models.BooleanField(verbose_name='Срок годности соответствует')),
+ ('is_accepted', models.BooleanField(default=False, verbose_name='Принят')),
+ ('rejection_reason', models.TextField(blank=True, null=True, verbose_name='Причина отклонения')),
+ ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product_directory.product', verbose_name='Товар')),
+ ],
+ ),
+ ]
diff --git a/goods_reception/migrations/__init__.py b/goods_reception/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/goods_reception/models.py b/goods_reception/models.py
new file mode 100644
index 0000000..039602f
--- /dev/null
+++ b/goods_reception/models.py
@@ -0,0 +1,13 @@
+from django.db import models
+from product_directory.models import Product
+
+class GoodsReception(models.Model):
+ product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name="Товар")
+ quantity = models.PositiveIntegerField(verbose_name="Количество")
+ quality_check = models.BooleanField(verbose_name="Проверка качества")
+ shelf_life_valid = models.BooleanField(verbose_name="Срок годности соответствует")
+ is_accepted = models.BooleanField(verbose_name="Принят", default=False)
+ rejection_reason = models.TextField(verbose_name="Причина отклонения", blank=True, null=True)
+
+ def __str__(self):
+ return f"Приемка: {self.product.name}"
\ No newline at end of file
diff --git a/goods_reception/serializers.py b/goods_reception/serializers.py
new file mode 100644
index 0000000..9ba8954
--- /dev/null
+++ b/goods_reception/serializers.py
@@ -0,0 +1,12 @@
+from rest_framework import serializers
+from .models import GoodsReception
+
+class GoodsReceptionSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = GoodsReception
+ fields = '__all__'
+
+ def validate(self, data):
+ if not data['quality_check'] or not data['shelf_life_valid']:
+ raise serializers.ValidationError("Товар не прошёл проверку качества или срок годности истёк.")
+ return data
diff --git a/goods_reception/signals.py b/goods_reception/signals.py
new file mode 100644
index 0000000..9642962
--- /dev/null
+++ b/goods_reception/signals.py
@@ -0,0 +1,24 @@
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from goods_reception.models import GoodsReception
+from warehouse.models import StockOperation, Warehouse, StorageLocation
+
+@receiver(post_save, sender=GoodsReception)
+def handle_goods_reception(sender, instance, created, **kwargs):
+ if created and instance.is_accepted:
+ # операция прихода
+ operation = StockOperation.objects.create(
+ product=instance.product,
+ warehouse=Warehouse.objects.get(name="Основной склад"),
+ storage_location=StorageLocation.objects.filter(warehouse__name="Основной склад").first(),
+ operation_type='Incoming',
+ quantity=instance.quantity,
+ expiration_date=None # Укажите срок годности, если известно
+ )
+
+ # логика распределения по местам хранения
+ if instance.product.storage_temperature == "+4C":
+ cold_storage = StorageLocation.objects.filter(warehouse=operation.warehouse, temperature_control=True).first()
+ if cold_storage:
+ operation.storage_location = cold_storage
+ operation.save()
diff --git a/goods_reception/templates/goods_reception/reception_report.html b/goods_reception/templates/goods_reception/reception_report.html
new file mode 100644
index 0000000..6f1c249
--- /dev/null
+++ b/goods_reception/templates/goods_reception/reception_report.html
@@ -0,0 +1,19 @@
+
Акт приемки
+
+
+ Товар |
+ Количество |
+ Качество |
+ Срок годности |
+ Причина отклонения |
+
+ {% for reception in receptions %}
+
+ {{ reception.product.name }} |
+ {{ reception.quantity }} |
+ {{ reception.quality_check }} |
+ {{ reception.shelf_life_valid }} |
+ {{ reception.rejection_reason }} |
+
+ {% endfor %}
+
\ No newline at end of file
diff --git a/goods_reception/tests.py b/goods_reception/tests.py
new file mode 100644
index 0000000..28d3acb
--- /dev/null
+++ b/goods_reception/tests.py
@@ -0,0 +1,18 @@
+from django.test import TestCase
+from .models import Product
+
+class ProductTestCase(TestCase):
+ def test_product_creation(self):
+ product = Product.objects.create(
+ name="Тестовый товар",
+ barcode="1234567890123",
+ shelf_life_days=365,
+ dimensions="10x10x10",
+ unit_of_measure="шт.",
+ manufacturer="Производитель",
+ category="Food",
+ storage_temperature="+4C",
+ promotion="1+1"
+ )
+ self.assertEqual(Product.objects.count(), 1)
+ self.assertEqual(product.name, "Тестовый товар")
\ No newline at end of file
diff --git a/goods_reception/tests/test_api.py b/goods_reception/tests/test_api.py
new file mode 100644
index 0000000..1493812
--- /dev/null
+++ b/goods_reception/tests/test_api.py
@@ -0,0 +1,36 @@
+from rest_framework.test import APITestCase
+from rest_framework import status
+from .models import GoodsReception
+from product_directory.models import Product
+
+class GoodsReceptionAPITestCase(APITestCase):
+ def setUp(self):
+ self.product = Product.objects.create(
+ name="Тестовый товар",
+ barcode="1234567890123",
+ shelf_life_days=365,
+ dimensions="10x10x10",
+ unit_of_measure="шт.",
+ manufacturer="Производитель",
+ category="Food",
+ storage_temperature="+4C",
+ promotion="1+1"
+ )
+ self.goods_reception_data = {
+ "product": self.product.id,
+ "quantity": 10,
+ "quality_check": True,
+ "shelf_life_valid": True,
+ "is_accepted": True,
+ "rejection_reason": None
+ }
+
+ def test_create_goods_reception(self):
+ response = self.client.post('/goods-reception/api/goods-receptions/', self.goods_reception_data)
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ def test_list_goods_receptions(self):
+ GoodsReception.objects.create(**self.goods_reception_data)
+ response = self.client.get('/goods-reception/api/goods-receptions/')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
diff --git a/goods_reception/urls.py b/goods_reception/urls.py
new file mode 100644
index 0000000..8138dd5
--- /dev/null
+++ b/goods_reception/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path, include
+from rest_framework.routers import DefaultRouter
+from .api_views import GoodsReceptionViewSet
+
+router = DefaultRouter()
+router.register(r'goods-receptions', GoodsReceptionViewSet, basename='goods-reception')
+
+urlpatterns = [
+ path('api/', include(router.urls)),
+]
\ No newline at end of file
diff --git a/goods_reception/views.py b/goods_reception/views.py
new file mode 100644
index 0000000..168f4c1
--- /dev/null
+++ b/goods_reception/views.py
@@ -0,0 +1,17 @@
+from django.shortcuts import render, redirect
+from .forms import GoodsReceptionForm
+from .models import GoodsReception
+
+def goods_reception_list(request):
+ receptions = GoodsReception.objects.all()
+ return render(request, 'goods_reception/goods_reception_list.html', {'receptions': receptions})
+
+def goods_reception_create(request):
+ if request.method == 'POST':
+ form = GoodsReceptionForm(request.POST)
+ if form.is_valid():
+ form.save()
+ return redirect('goods_reception_list')
+ else:
+ form = GoodsReceptionForm()
+ return render(request, 'goods_reception/goods_reception_form.html', {'form': form})
\ No newline at end of file
diff --git a/inventory/models.py b/inventory/models.py
index b6928e0..31563e0 100644
--- a/inventory/models.py
+++ b/inventory/models.py
@@ -1,19 +1,23 @@
from django.db import models
from django.contrib.auth.models import AbstractUser, Permission, Group
-# Справочник товаров
-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="Штрихкод")
+from product_directory.models import Product
- def __str__(self):
- return self.name
+# todo: нужно перенести уникальные поля в новую модель
+
+# Справочник товаров
+# 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 Position(models.Model):
diff --git a/logs.log b/logs.log
new file mode 100644
index 0000000..92ab0b8
--- /dev/null
+++ b/logs.log
@@ -0,0 +1,135 @@
+Watching for file changes with StatReloader
+Watching for file changes with StatReloader
+Watching for file changes with StatReloader
+Not Found: /
+"GET / HTTP/1.1" 404 2456
+Not Found: /goods-reception
+"GET /goods-reception HTTP/1.1" 404 2519
+"GET /goods-reception/api HTTP/1.1" 301 0
+"GET /goods-reception/api/ HTTP/1.1" 200 5454
+"GET /static/rest_framework/css/prettify.css HTTP/1.1" 200 817
+"GET /static/rest_framework/css/bootstrap-tweaks.css HTTP/1.1" 200 3426
+"GET /static/rest_framework/css/default.css HTTP/1.1" 200 1152
+"GET /static/rest_framework/js/ajax-form.js HTTP/1.1" 200 3796
+"GET /static/rest_framework/js/prettify-min.js HTTP/1.1" 200 13632
+"GET /static/rest_framework/css/bootstrap.min.css HTTP/1.1" 200 121457
+"GET /static/rest_framework/js/default.js HTTP/1.1" 200 1268
+"GET /static/rest_framework/js/csrf.js HTTP/1.1" 200 1793
+"GET /static/rest_framework/js/jquery-3.7.1.min.js HTTP/1.1" 200 87533
+"GET /static/rest_framework/js/load-ajax-form.js HTTP/1.1" 200 59
+"GET /static/rest_framework/js/bootstrap.min.js HTTP/1.1" 200 39680
+"GET /static/rest_framework/img/grid.png HTTP/1.1" 200 1458
+"GET /goods-reception/api/ HTTP/1.1" 200 5454
+"OPTIONS /goods-reception/api/ HTTP/1.1" 200 5662
+"GET /goods-reception/api/ HTTP/1.1" 200 5454
+Not Found: /goods-reception/swagger
+"GET /goods-reception/swagger HTTP/1.1" 404 2637
+"GET /api/swagger/ HTTP/1.1" 200 2456
+"GET /api/swagger/?format=openapi HTTP/1.1" 200 21094
+"GET /goods-reception/api/goods-receptions/ HTTP/1.1" 200 2
+"GET /admin/ HTTP/1.1" 200 7189
+/Users/darius/Documents/franchise_store/goods_reception/admin.py changed, reloading.
+Watching for file changes with StatReloader
+"GET /admin/ HTTP/1.1" 200 8161
+"GET /admin/goods_reception/goodsreception/ HTTP/1.1" 200 10520
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /static/admin/img/icon-viewlink.svg HTTP/1.1" 200 581
+"GET /admin/ HTTP/1.1" 200 8161
+"GET /admin/product_directory/product/ HTTP/1.1" 200 11170
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/ HTTP/1.1" 200 8161
+"GET /admin/goods_reception/goodsreception/ HTTP/1.1" 200 10520
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/goods_reception/goodsreception/add/ HTTP/1.1" 200 13670
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"POST /admin/goods_reception/goodsreception/add/ HTTP/1.1" 302 0
+"GET /admin/goods_reception/goodsreception/ HTTP/1.1" 200 13308
+"GET /static/admin/img/icon-no.svg HTTP/1.1" 200 560
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/goods_reception/goodsreception/1/change/ HTTP/1.1" 200 13985
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/goods_reception/goodsreception/ HTTP/1.1" 200 13062
+Watching for file changes with StatReloader
+"GET /admin/ HTTP/1.1" 200 9417
+"GET /admin/ HTTP/1.1" 200 9417
+"GET /admin/warehouse/stock/ HTTP/1.1" 200 10410
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/ HTTP/1.1" 200 9417
+/Users/darius/Documents/franchise_store/warehouse/models.py changed, reloading.
+Watching for file changes with StatReloader
+/Users/darius/Documents/franchise_store/warehouse/models.py changed, reloading.
+Watching for file changes with StatReloader
+/Users/darius/Documents/franchise_store/warehouse/models.py changed, reloading.
+Watching for file changes with StatReloader
+Watching for file changes with StatReloader
+"GET /admin/ HTTP/1.1" 200 10808
+"GET /admin/warehouse/storagelocation/ HTTP/1.1" 200 11604
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/storagelocation/add/ HTTP/1.1" 200 13587
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/storagelocation/ HTTP/1.1" 200 11604
+"GET /admin/ HTTP/1.1" 200 10808
+"GET /admin/warehouse/warehouse/ HTTP/1.1" 200 10710
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/warehouse/add/ HTTP/1.1" 200 12376
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"POST /admin/warehouse/warehouse/add/ HTTP/1.1" 302 0
+"GET /admin/warehouse/warehouse/ HTTP/1.1" 200 12695
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/warehouse/1/change/ HTTP/1.1" 200 12651
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/storagelocation/ HTTP/1.1" 200 11604
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/storagelocation/add/ HTTP/1.1" 200 13632
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"POST /admin/warehouse/storagelocation/add/ HTTP/1.1" 302 0
+"GET /admin/warehouse/storagelocation/ HTTP/1.1" 200 13712
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 12593
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/stockoperation/add/ HTTP/1.1" 200 19071
+"GET /static/admin/js/calendar.js HTTP/1.1" 200 9141
+"GET /static/admin/js/admin/DateTimeShortcuts.js HTTP/1.1" 200 19319
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /static/admin/img/icon-calendar.svg HTTP/1.1" 200 1086
+"GET /static/admin/img/calendar-icons.svg HTTP/1.1" 200 2455
+"POST /admin/warehouse/stockoperation/add/ HTTP/1.1" 302 0
+"GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 15070
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/stockoperation/1/change/ HTTP/1.1" 200 19394
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 14799
+Watching for file changes with StatReloader
+"GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 14886
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+Watching for file changes with StatReloader
+"GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 14914
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+/Users/darius/Documents/franchise_store/warehouse/admin.py changed, reloading.
+Watching for file changes with StatReloader
+/Users/darius/Documents/franchise_store/warehouse/admin.py changed, reloading.
+Watching for file changes with StatReloader
+/Users/darius/Documents/franchise_store/warehouse/admin.py changed, reloading.
+Watching for file changes with StatReloader
+Watching for file changes with StatReloader
+"GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 14886
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/stockoperation/statistics/ HTTP/1.1" 200 24699
+"GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 14886
+"GET /admin/warehouse/stockoperation/add/ HTTP/1.1" 200 19071
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/product_directory/product/1/change/?_to_field=id&_popup=1 HTTP/1.1" 200 10158
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/product_directory/product/1/change/?_to_field=id HTTP/1.1" 200 17244
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"GET /admin/warehouse/stockoperation/add/ HTTP/1.1" 200 19071
+"GET /admin/product_directory/product/add/?_to_field=id&_popup=1 HTTP/1.1" 200 10066
+"GET /admin/jsi18n/ HTTP/1.1" 200 3342
+"POST /admin/warehouse/stockoperation/add/ HTTP/1.1" 302 0
+"GET /admin/warehouse/stockoperation/ HTTP/1.1" 200 15687
+"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
+"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
diff --git a/product_directory.log b/product_directory.log
new file mode 100644
index 0000000..5e7d650
--- /dev/null
+++ b/product_directory.log
@@ -0,0 +1,4 @@
+Watching for file changes with StatReloader
+/Users/darius/Documents/franchise_store/settings/base.py changed, reloading.
+Watching for file changes with StatReloader
+/Users/darius/Documents/franchise_store/settings/base.py changed, reloading.
diff --git a/product_directory/__init__.py b/product_directory/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/product_directory/admin.py b/product_directory/admin.py
new file mode 100644
index 0000000..137462c
--- /dev/null
+++ b/product_directory/admin.py
@@ -0,0 +1,8 @@
+from django.contrib import admin
+
+from .models import Product
+
+@admin.register(Product)
+class ProductAdmin(admin.ModelAdmin):
+ list_display = ('name', 'barcode', 'category', 'shelf_life_days', 'promotion')
+ search_fields = ('name', 'barcode')
diff --git a/product_directory/api.py b/product_directory/api.py
new file mode 100644
index 0000000..e92f206
--- /dev/null
+++ b/product_directory/api.py
@@ -0,0 +1,7 @@
+from rest_framework.viewsets import ModelViewSet
+from .models import Product
+from .serializers import ProductSerializer
+
+class ProductViewSet(ModelViewSet):
+ queryset = Product.objects.all()
+ serializer_class = ProductSerializer
\ No newline at end of file
diff --git a/product_directory/apps.py b/product_directory/apps.py
new file mode 100644
index 0000000..6c478b2
--- /dev/null
+++ b/product_directory/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ProductDirectoryConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'product_directory'
diff --git a/product_directory/forms.py b/product_directory/forms.py
new file mode 100644
index 0000000..1242276
--- /dev/null
+++ b/product_directory/forms.py
@@ -0,0 +1,15 @@
+from django import forms
+from .models import Product
+
+class ProductForm(forms.ModelForm):
+ class Meta:
+ model = Product
+ fields = '__all__'
+
+ def clean_shelf_life_days(self):
+ shelf_life_days = self.cleaned_data.get('shelf_life_days')
+ if shelf_life_days <= 0:
+ raise forms.ValidationError("Срок годности должен быть положительным числом.")
+ if shelf_life_days > 3650: # 10 лет
+ raise forms.ValidationError("Срок годности превышает допустимый предел.")
+ return shelf_life_days
\ No newline at end of file
diff --git a/product_directory/migrations/0001_initial.py b/product_directory/migrations/0001_initial.py
new file mode 100644
index 0000000..7cb2c54
--- /dev/null
+++ b/product_directory/migrations/0001_initial.py
@@ -0,0 +1,29 @@
+# Generated by Django 5.1.4 on 2025-01-08 14:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ 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='Наименование товара')),
+ ('barcode', models.CharField(max_length=50, unique=True, verbose_name='Штрих-код')),
+ ('shelf_life_days', models.PositiveIntegerField(verbose_name='Срок годности (дни)')),
+ ('dimensions', models.CharField(max_length=255, verbose_name='Размеры')),
+ ('unit_of_measure', models.CharField(max_length=50, verbose_name='Единицы измерения')),
+ ('manufacturer', models.CharField(max_length=255, verbose_name='Производитель')),
+ ('category', models.CharField(choices=[('Food', 'Продукты'), ('Electronics', 'Электроника'), ('Clothing', 'Одежда'), ('Other', 'Другое')], max_length=50, verbose_name='Категория')),
+ ('storage_temperature', models.CharField(max_length=50, verbose_name='Температурные условия хранения')),
+ ('promotion', models.CharField(blank=True, choices=[('1+1', '1+1'), ('2+1', '2+1')], max_length=10, null=True, verbose_name='Акция')),
+ ],
+ ),
+ ]
diff --git a/product_directory/migrations/__init__.py b/product_directory/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/product_directory/models.py b/product_directory/models.py
new file mode 100644
index 0000000..4882d40
--- /dev/null
+++ b/product_directory/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+
+
+class Product(models.Model):
+ CATEGORY_CHOICES = [
+ ('Food', 'Продукты'),
+ ('Electronics', 'Электроника'),
+ ('Clothing', 'Одежда'),
+ ('Other', 'Другое'),
+ ]
+ PROMOTION_CHOICES = [
+ ('1+1', '1+1'),
+ ('2+1', '2+1'),
+ ]
+ name = models.CharField(max_length=255, verbose_name="Наименование товара")
+ barcode = models.CharField(max_length=50, unique=True, verbose_name="Штрих-код")
+ shelf_life_days = models.PositiveIntegerField(verbose_name="Срок годности (дни)")
+ dimensions = models.CharField(max_length=255, verbose_name="Размеры")
+ unit_of_measure = models.CharField(max_length=50, verbose_name="Единицы измерения")
+ manufacturer = models.CharField(max_length=255, verbose_name="Производитель")
+ category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, verbose_name="Категория")
+ storage_temperature = models.CharField(max_length=50, verbose_name="Температурные условия хранения")
+ promotion = models.CharField(max_length=10, choices=PROMOTION_CHOICES, verbose_name="Акция", blank=True, null=True)
+
+ def __str__(self):
+ return self.name
\ No newline at end of file
diff --git a/product_directory/serializers.py b/product_directory/serializers.py
new file mode 100644
index 0000000..c4c09f1
--- /dev/null
+++ b/product_directory/serializers.py
@@ -0,0 +1,7 @@
+from rest_framework import serializers
+from .models import Product
+
+class ProductSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Product
+ fields = '__all__'
\ No newline at end of file
diff --git a/product_directory/templates/product_directory/product_list.html b/product_directory/templates/product_directory/product_list.html
new file mode 100644
index 0000000..12b8a38
--- /dev/null
+++ b/product_directory/templates/product_directory/product_list.html
@@ -0,0 +1,13 @@
+{% for product in products %}
+
+ {{ product.name }} |
+ {{ product.barcode }} |
+ {{ product.category }} |
+ {{ product.shelf_life_days }} |
+ {{ product.promotion }} |
+
+ Изменить
+ Удалить
+ |
+
+{% endfor %}
\ No newline at end of file
diff --git a/product_directory/tests.py b/product_directory/tests.py
new file mode 100644
index 0000000..28d3acb
--- /dev/null
+++ b/product_directory/tests.py
@@ -0,0 +1,18 @@
+from django.test import TestCase
+from .models import Product
+
+class ProductTestCase(TestCase):
+ def test_product_creation(self):
+ product = Product.objects.create(
+ name="Тестовый товар",
+ barcode="1234567890123",
+ shelf_life_days=365,
+ dimensions="10x10x10",
+ unit_of_measure="шт.",
+ manufacturer="Производитель",
+ category="Food",
+ storage_temperature="+4C",
+ promotion="1+1"
+ )
+ self.assertEqual(Product.objects.count(), 1)
+ self.assertEqual(product.name, "Тестовый товар")
\ No newline at end of file
diff --git a/product_directory/views.py b/product_directory/views.py
new file mode 100644
index 0000000..828d22c
--- /dev/null
+++ b/product_directory/views.py
@@ -0,0 +1,42 @@
+import logging # Импортируем библиотеку для логирования
+from django.shortcuts import render, get_object_or_404, redirect
+from .models import Product
+from .forms import ProductForm
+
+# Настраиваем логгер
+logger = logging.getLogger(__name__)
+
+def product_list(request):
+ products = Product.objects.all()
+ return render(request, 'product_directory/product_list.html', {'products': products})
+
+def product_create(request):
+ if request.method == 'POST':
+ form = ProductForm(request.POST)
+ if form.is_valid():
+ product = form.save()
+ logger.info(f"Пользователь {request.user} создал товар '{product.name}'")
+ return redirect('product_list')
+ else:
+ form = ProductForm()
+ return render(request, 'product_directory/product_form.html', {'form': form})
+
+def product_update(request, pk):
+ product = get_object_or_404(Product, pk=pk)
+ if request.method == 'POST':
+ form = ProductForm(request.POST, instance=product)
+ if form.is_valid():
+ product = form.save()
+ logger.info(f"Пользователь {request.user} обновил товар '{product.name}'")
+ return redirect('product_list')
+ else:
+ form = ProductForm(instance=product)
+ return render(request, 'product_directory/product_form.html', {'form': form})
+
+def product_delete(request, pk):
+ product = get_object_or_404(Product, pk=pk)
+ if request.method == 'POST':
+ logger.warning(f"Пользователь {request.user} удалил товар '{product.name}'")
+ product.delete()
+ return redirect('product_list')
+ return render(request, 'product_directory/product_confirm_delete.html', {'product': product})
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..ec863bf
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,12 @@
+asgiref==3.8.1
+Django==5.1.4
+djangorestframework==3.15.2
+drf-yasg==1.21.8
+inflection==0.5.1
+packaging==24.2
+psycopg2-binary==2.9.10
+pytz==2024.2
+PyYAML==6.0.2
+sqlparse==0.5.3
+typing_extensions==4.12.2
+uritemplate==4.1.1
diff --git a/settings/base.py b/settings/base.py
index 3bb3108..21adf95 100644
--- a/settings/base.py
+++ b/settings/base.py
@@ -56,7 +56,34 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
'inventory',
+ 'warehouse',
+ 'product_directory',
+ 'goods_reception',
'pricing',
]
ROOT_URLCONF = 'urls'
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'handlers': {
+ 'file': {
+ 'level': 'INFO',
+ 'class': 'logging.FileHandler',
+ 'filename': 'logs.log', # путь к файлу для логов
+ },
+ },
+ 'loggers': {
+ 'django': {
+ 'handlers': ['file'],
+ 'level': 'INFO',
+ 'propagate': True,
+ },
+ 'product_directory': {
+ 'handlers': ['file'],
+ 'level': 'INFO',
+ 'propagate': True,
+ },
+ },
+}
diff --git a/urls.py b/urls.py
index 08dbe5f..49a048e 100644
--- a/urls.py
+++ b/urls.py
@@ -5,4 +5,6 @@ urlpatterns = [
path('admin/', admin.site.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')),
]
diff --git a/warehouse/__init__.py b/warehouse/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/warehouse/admin.py b/warehouse/admin.py
new file mode 100644
index 0000000..260a64a
--- /dev/null
+++ b/warehouse/admin.py
@@ -0,0 +1,69 @@
+from django.contrib import admin
+from .models import Warehouse, StorageLocation, StockOperation
+from django.urls import path
+from django.shortcuts import render
+import matplotlib.pyplot as plt
+from io import BytesIO
+import base64
+
+import matplotlib
+matplotlib.use('Agg')
+
+@admin.register(Warehouse)
+class WarehouseAdmin(admin.ModelAdmin):
+ list_display = ('name', 'location', 'is_active')
+ search_fields = ('name', 'location')
+
+
+@admin.register(StorageLocation)
+class StorageLocationAdmin(admin.ModelAdmin):
+ list_display = ('name', 'warehouse', 'temperature_control')
+ search_fields = ('name', 'warehouse__name')
+ list_filter = ('temperature_control',)
+
+
+@admin.register(StockOperation)
+class StockOperationAdmin(admin.ModelAdmin):
+ list_display = ('product', 'warehouse', 'operation_type', 'quantity', 'operation_date')
+ search_fields = ('product__name', 'warehouse__name', 'operation_type')
+ list_filter = ('operation_type', 'operation_date')
+
+ def changelist_view(self, request, extra_context=None):
+ extra_context = extra_context or {}
+ extra_context['custom_button'] = {
+ 'url': 'statistics/',
+ 'label': 'Просмотреть статистику'
+ }
+ return super().changelist_view(request, extra_context)
+
+ def get_urls(self):
+ urls = super().get_urls()
+ custom_urls = [
+ path('statistics/', self.admin_site.admin_view(self.statistics_view), name='stock_operation_statistics'),
+ ]
+ return custom_urls + urls
+
+ def statistics_view(self, request):
+ operations = StockOperation.objects.all()
+ incoming_count = operations.filter(operation_type='Incoming').count()
+ outgoing_count = operations.filter(operation_type='Outgoing').count()
+ transfer_count = operations.filter(operation_type='Transfer').count()
+ writeoff_count = operations.filter(operation_type='WriteOff').count()
+
+ labels = ['Incoming', 'Outgoing', 'Transfer', 'WriteOff']
+ data = [incoming_count, outgoing_count, transfer_count, writeoff_count]
+
+ plt.figure(figsize=(6, 6))
+ plt.pie(data, labels=labels, autopct='%1.1f%%', startangle=90)
+ plt.axis('equal')
+
+ buffer = BytesIO()
+ plt.savefig(buffer, format='png')
+ buffer.seek(0)
+ image_png = buffer.getvalue()
+ buffer.close()
+
+ chart = base64.b64encode(image_png).decode('utf-8')
+
+ context = {'chart': chart}
+ return render(request, 'admin/warehouse/stock_statistics.html', context)
\ No newline at end of file
diff --git a/warehouse/api_views.py b/warehouse/api_views.py
new file mode 100644
index 0000000..c437fcd
--- /dev/null
+++ b/warehouse/api_views.py
@@ -0,0 +1,11 @@
+from rest_framework.viewsets import ModelViewSet
+from .models import StockOperation
+from .serializers import StockOperationSerializer
+
+class StockOperationViewSet(ModelViewSet):
+ queryset = StockOperation.objects.all()
+ serializer_class = StockOperationSerializer
+
+def handle_goods_reception(sender, instance, created, **kwargs):
+ if created and instance.is_accepted:
+ from warehouse.models import Stock
\ No newline at end of file
diff --git a/warehouse/apps.py b/warehouse/apps.py
new file mode 100644
index 0000000..40fc6fa
--- /dev/null
+++ b/warehouse/apps.py
@@ -0,0 +1,9 @@
+from django.apps import AppConfig
+
+
+class WarehouseConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'warehouse'
+
+ def ready(self):
+ import goods_reception.signals
diff --git a/warehouse/migrations/0001_initial.py b/warehouse/migrations/0001_initial.py
new file mode 100644
index 0000000..3df60a9
--- /dev/null
+++ b/warehouse/migrations/0001_initial.py
@@ -0,0 +1,30 @@
+# Generated by Django 5.1.4 on 2025-01-08 15:13
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('product_directory', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Stock',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('quantity', models.PositiveIntegerField(default=0, verbose_name='Количество')),
+ ('location', models.CharField(max_length=255, verbose_name='Место хранения')),
+ ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='product_directory.product', verbose_name='Товар')),
+ ],
+ options={
+ 'verbose_name': 'Складская запись',
+ 'verbose_name_plural': 'Складские записи',
+ 'unique_together': {('product', 'location')},
+ },
+ ),
+ ]
diff --git a/warehouse/migrations/0002_warehouse_alter_stock_options_and_more.py b/warehouse/migrations/0002_warehouse_alter_stock_options_and_more.py
new file mode 100644
index 0000000..f28308e
--- /dev/null
+++ b/warehouse/migrations/0002_warehouse_alter_stock_options_and_more.py
@@ -0,0 +1,67 @@
+# Generated by Django 5.1.4 on 2025-01-08 15:29
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('product_directory', '0001_initial'),
+ ('warehouse', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Warehouse',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, verbose_name='Название склада')),
+ ('location', models.CharField(max_length=255, verbose_name='Расположение склада')),
+ ('is_active', models.BooleanField(default=True, verbose_name='Активен')),
+ ],
+ options={
+ 'verbose_name': 'Склад',
+ 'verbose_name_plural': 'Склады',
+ },
+ ),
+ migrations.AlterModelOptions(
+ name='stock',
+ options={},
+ ),
+ migrations.AlterUniqueTogether(
+ name='stock',
+ unique_together=set(),
+ ),
+ migrations.CreateModel(
+ name='StorageLocation',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, verbose_name='Место хранения')),
+ ('temperature_control', models.BooleanField(default=False, verbose_name='Требуется контроль температуры')),
+ ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='storage_locations', to='warehouse.warehouse', verbose_name='Склад')),
+ ],
+ options={
+ 'verbose_name': 'Место хранения',
+ 'verbose_name_plural': 'Места хранения',
+ },
+ ),
+ migrations.CreateModel(
+ name='StockOperation',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('operation_type', models.CharField(choices=[('Incoming', 'Приход'), ('Outgoing', 'Расход'), ('Transfer', 'Перемещение'), ('WriteOff', 'Списание')], max_length=50, verbose_name='Тип операции')),
+ ('quantity', models.PositiveIntegerField(verbose_name='Количество')),
+ ('operation_date', models.DateTimeField(auto_now_add=True, verbose_name='Дата операции')),
+ ('expiration_date', models.DateField(blank=True, null=True, verbose_name='Срок годности')),
+ ('reason', models.TextField(blank=True, null=True, verbose_name='Причина (для списания)')),
+ ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='warehouse_stock_operations', to='product_directory.product', verbose_name='Товар')),
+ ('storage_location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='warehouse.storagelocation', verbose_name='Место хранения')),
+ ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='warehouse.warehouse', verbose_name='Склад')),
+ ],
+ options={
+ 'verbose_name': 'Операция с товаром',
+ 'verbose_name_plural': 'Операции с товарами',
+ },
+ ),
+ ]
diff --git a/warehouse/migrations/__init__.py b/warehouse/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/warehouse/models.py b/warehouse/models.py
new file mode 100644
index 0000000..03d8ae5
--- /dev/null
+++ b/warehouse/models.py
@@ -0,0 +1,64 @@
+from django.db import models
+from product_directory.models import Product
+
+class Warehouse(models.Model):
+ name = models.CharField(max_length=255, verbose_name="Название склада")
+ location = models.CharField(max_length=255, verbose_name="Расположение склада")
+ is_active = models.BooleanField(default=True, verbose_name="Активен")
+
+ class Meta:
+ verbose_name = "Склад"
+ verbose_name_plural = "Склады"
+
+ def __str__(self):
+ return self.name
+
+
+class StorageLocation(models.Model):
+ warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE, related_name="storage_locations", verbose_name="Склад")
+ name = models.CharField(max_length=255, verbose_name="Место хранения")
+ temperature_control = models.BooleanField(default=False, verbose_name="Требуется контроль температуры")
+
+ class Meta:
+ verbose_name = "Место хранения"
+ verbose_name_plural = "Места хранения"
+
+ def __str__(self):
+ return f"{self.name} ({self.warehouse.name})"
+
+class Stock(models.Model):
+ product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name="Товар")
+ quantity = models.PositiveIntegerField(default=0, verbose_name="Количество")
+ location = models.CharField(max_length=255, verbose_name="Место хранения")
+
+ def __str__(self):
+ return f"{self.product.name} - {self.quantity} шт."
+
+class StockOperation(models.Model):
+ OPERATION_TYPE_CHOICES = [
+ ('Incoming', 'Приход'),
+ ('Outgoing', 'Расход'),
+ ('Transfer', 'Перемещение'),
+ ('WriteOff', 'Списание'),
+ ]
+
+ product = models.ForeignKey(
+ Product,
+ on_delete=models.CASCADE,
+ verbose_name="Товар",
+ related_name="warehouse_stock_operations"
+ )
+ warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE, verbose_name="Склад")
+ storage_location = models.ForeignKey(StorageLocation, on_delete=models.CASCADE, verbose_name="Место хранения")
+ operation_type = models.CharField(max_length=50, choices=OPERATION_TYPE_CHOICES, verbose_name="Тип операции")
+ quantity = models.PositiveIntegerField(verbose_name="Количество")
+ operation_date = models.DateTimeField(auto_now_add=True, verbose_name="Дата операции")
+ expiration_date = models.DateField(blank=True, null=True, verbose_name="Срок годности")
+ reason = models.TextField(blank=True, null=True, verbose_name="Причина (для списания)")
+
+ class Meta:
+ verbose_name = "Операция с товаром"
+ verbose_name_plural = "Операции с товарами"
+
+ def __str__(self):
+ return f"{self.operation_type} - {self.product.name} ({self.warehouse.name})"
diff --git a/warehouse/serializers.py b/warehouse/serializers.py
new file mode 100644
index 0000000..0a34141
--- /dev/null
+++ b/warehouse/serializers.py
@@ -0,0 +1,7 @@
+from rest_framework import serializers
+from .models import StockOperation
+
+class StockOperationSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = StockOperation
+ fields = '__all__'
diff --git a/warehouse/services.py b/warehouse/services.py
new file mode 100644
index 0000000..13efd27
--- /dev/null
+++ b/warehouse/services.py
@@ -0,0 +1,53 @@
+from . import models
+from .models import StockOperation
+
+def register_incoming_stock(product, quantity, warehouse, expiration_date=None):
+ operation = StockOperation.objects.create(
+ product=product,
+ quantity=quantity,
+ warehouse=warehouse,
+ operation_type='Incoming',
+ expiration_date=expiration_date
+ )
+
+ # Распределение по местам хранения
+ if product.storage_temperature:
+ storage_location = "Холодильник" if product.storage_temperature == "+4C" else "Полка"
+ operation.storage_location = storage_location
+ operation.save()
+ return operation
+
+def transfer_stock(product, quantity, from_warehouse, to_location):
+ total_stock = StockOperation.objects.filter(
+ product=product, warehouse=from_warehouse, operation_type='Incoming'
+ ).aggregate(total=models.Sum('quantity'))['total'] or 0
+
+ if total_stock < quantity:
+ raise ValueError("Недостаточно товара на складе для перемещения.")
+
+ transfer_operation = StockOperation.objects.create(
+ product=product,
+ quantity=quantity,
+ warehouse=from_warehouse,
+ storage_location=to_location,
+ operation_type='Transfer'
+ )
+ return transfer_operation
+
+
+def write_off_stock(product, quantity, warehouse, reason):
+ total_stock = StockOperation.objects.filter(
+ product=product, warehouse=warehouse, operation_type='Incoming'
+ ).aggregate(total=models.Sum('quantity'))['total'] or 0
+
+ if total_stock < quantity:
+ raise ValueError("Недостаточно товара на складе для списания.")
+
+ write_off_operation = StockOperation.objects.create(
+ product=product,
+ quantity=quantity,
+ warehouse=warehouse,
+ operation_type='WriteOff',
+ reason=reason
+ )
+ return write_off_operation
diff --git a/warehouse/templates/admin/warehouse/stock_statistics.html b/warehouse/templates/admin/warehouse/stock_statistics.html
new file mode 100644
index 0000000..1a7f2c4
--- /dev/null
+++ b/warehouse/templates/admin/warehouse/stock_statistics.html
@@ -0,0 +1,7 @@
+{% extends "admin/base_site.html" %}
+
+{% block content %}
+Статистика операций на складе
+
+Назад к списку операций
+{% endblock %}
diff --git a/warehouse/templates/admin/warehouse/stockoperation/change_list.html b/warehouse/templates/admin/warehouse/stockoperation/change_list.html
new file mode 100644
index 0000000..abf58f4
--- /dev/null
+++ b/warehouse/templates/admin/warehouse/stockoperation/change_list.html
@@ -0,0 +1,6 @@
+{% extends "admin/change_list.html" %}
+
+{% block object-tools %}
+{{ block.super }}
+{{ custom_button.label }}
+{% endblock %}
diff --git a/warehouse/tests.py b/warehouse/tests.py
new file mode 100644
index 0000000..06c7455
--- /dev/null
+++ b/warehouse/tests.py
@@ -0,0 +1,57 @@
+from django.test import TestCase
+from product_directory.models import Product
+from .models import Stock, Warehouse, StorageLocation, StockOperation
+
+
+class StockTestCase(TestCase):
+ def setUp(self):
+ self.product = Product.objects.create(
+ name="Тестовый товар",
+ barcode="1234567890123",
+ shelf_life_days=365,
+ dimensions="10x10x10",
+ unit_of_measure="шт.",
+ manufacturer="Производитель",
+ category="Food",
+ storage_temperature="+4C",
+ promotion="1+1"
+ )
+ self.stock = Stock.objects.create(
+ product=self.product,
+ quantity=100,
+ location="Основной склад"
+ )
+
+ def test_stock_creation(self):
+ self.assertEqual(self.stock.quantity, 100)
+
+ def test_stock_update(self):
+ self.stock.quantity += 50
+ self.stock.save()
+ self.assertEqual(self.stock.quantity, 150)
+
+class StockOperationTestCase(TestCase):
+ def setUp(self):
+ self.warehouse = Warehouse.objects.create(name="Основной склад", location="Москва")
+ self.storage = StorageLocation.objects.create(warehouse=self.warehouse, name="Холодильник", temperature_control=True)
+ self.product = Product.objects.create(
+ name="Тестовый товар",
+ barcode="1234567890123",
+ shelf_life_days=365,
+ dimensions="10x10x10",
+ unit_of_measure="шт.",
+ manufacturer="Производитель",
+ category="Food",
+ storage_temperature="+4C",
+ promotion="1+1"
+ )
+
+ def test_incoming_operation(self):
+ operation = StockOperation.objects.create(
+ product=self.product,
+ warehouse=self.warehouse,
+ storage_location=self.storage,
+ operation_type="Incoming",
+ quantity=100
+ )
+ self.assertEqual(operation.quantity, 100)
\ No newline at end of file
diff --git a/warehouse/urls.py b/warehouse/urls.py
new file mode 100644
index 0000000..fc68b30
--- /dev/null
+++ b/warehouse/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path, include
+from rest_framework.routers import DefaultRouter
+from .api_views import StockOperationViewSet
+
+router = DefaultRouter()
+router.register(r'stock-operations', StockOperationViewSet, basename='stock-operation')
+
+urlpatterns = [
+ path('api/', include(router.urls)),
+]
diff --git a/warehouse/views.py b/warehouse/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/warehouse/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.