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/logs.log b/logs.log
index 7cec389..e35bd10 100644
--- a/logs.log
+++ b/logs.log
@@ -1,2 +1,101 @@
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
diff --git a/settings/base.py b/settings/base.py
index f9e4780..dd0f478 100644
--- a/settings/base.py
+++ b/settings/base.py
@@ -56,7 +56,9 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
'inventory',
+ 'warehouse',
'product_directory',
+ 'goods_reception',
]
ROOT_URLCONF = 'urls'
diff --git a/urls.py b/urls.py
index 4c7bb56..f7a79b1 100644
--- a/urls.py
+++ b/urls.py
@@ -4,4 +4,6 @@ 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
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..2914f5a
--- /dev/null
+++ b/warehouse/admin.py
@@ -0,0 +1,21 @@
+from django.contrib import admin
+from .models import Warehouse, StorageLocation, StockOperation
+
+@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')
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/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.