Merge branch 'main' of https://git.djft.ru/darius-atlas/store-management-system
commit
f529b056b0
@ -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,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})
|
@ -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
|
@ -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.
|
@ -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')
|
@ -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
|
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProductDirectoryConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'product_directory'
|
@ -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
|
@ -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='Акция')),
|
||||
],
|
||||
),
|
||||
]
|
@ -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
|
@ -0,0 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Product
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = '__all__'
|
@ -0,0 +1,13 @@
|
||||
{% for product in products %}
|
||||
<tr>
|
||||
<td>{{ product.name }}</td>
|
||||
<td>{{ product.barcode }}</td>
|
||||
<td>{{ product.category }}</td>
|
||||
<td>{{ product.shelf_life_days }}</td>
|
||||
<td>{{ product.promotion }}</td>
|
||||
<td>
|
||||
<a href="{% url 'product_update' product.id %}">Изменить</a>
|
||||
<a href="{% url 'product_delete' product.id %}">Удалить</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
@ -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,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})
|
@ -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
|
@ -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)
|
@ -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,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,7 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Статистика операций на складе</h1>
|
||||
<img src="data:image/png;base64,{{ chart }}" alt="График статистики" />
|
||||
<a href="{% url 'admin:warehouse_stockoperation_changelist' %}" class="button">Назад к списку операций</a>
|
||||
{% endblock %}
|
@ -0,0 +1,6 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
|
||||
{% block object-tools %}
|
||||
{{ block.super }}
|
||||
<a href="{{ custom_button.url }}" class="button">{{ custom_button.label }}</a>
|
||||
{% endblock %}
|
@ -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…
Reference in new issue