feat: Модуль "Приемка товаров"

## Задачи:

1. Реализовать функционал для приемки товара:

- Проверка товара по количеству, качеству и сроку годности.

- Автоматическое добавление товара на склад, если все параметры соответствуют.

- Создание акта приемки с возможностью указания причин отклонений.

2. Реализовать логику учета состояния товаров (повреждения, не соответствие).

3. Реестор складов хранения товаров.
main
Artem-Darius Weber 15 hours ago
parent 87dae7b461
commit c6dfbc90dd

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

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

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

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

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

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

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

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

@ -0,0 +1,19 @@
<h1>Акт приемки</h1>
<table>
<tr>
<th>Товар</th>
<th>Количество</th>
<th>Качество</th>
<th>Срок годности</th>
<th>Причина отклонения</th>
</tr>
{% for reception in receptions %}
<tr>
<td>{{ reception.product.name }}</td>
<td>{{ reception.quantity }}</td>
<td>{{ reception.quality_check }}</td>
<td>{{ reception.shelf_life_valid }}</td>
<td>{{ reception.rejection_reason }}</td>
</tr>
{% endfor %}
</table>

@ -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, "Тестовый товар")

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

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

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

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

@ -56,7 +56,9 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'rest_framework',
'inventory',
'warehouse',
'product_directory',
'goods_reception',
]
ROOT_URLCONF = 'urls'

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

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

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

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

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

@ -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': 'Операции с товарами',
},
),
]

@ -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})"

@ -0,0 +1,7 @@
from rest_framework import serializers
from .models import StockOperation
class StockOperationSerializer(serializers.ModelSerializer):
class Meta:
model = StockOperation
fields = '__all__'

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

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

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

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
Loading…
Cancel
Save