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

## Задачи:

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

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

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

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

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

3. Реестор складов хранения товаров.
main
Artem-Darius Weber 1 day 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 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', 'django.contrib.staticfiles',
'rest_framework', 'rest_framework',
'inventory', 'inventory',
'warehouse',
'product_directory', 'product_directory',
'goods_reception',
] ]
ROOT_URLCONF = 'urls' ROOT_URLCONF = 'urls'

@ -4,4 +4,6 @@ from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/', include('inventory.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