Docs: начальная структура проекта
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# База данных (для docker-compose)
|
||||||
|
POSTGRES_USER=watersurf_erp
|
||||||
|
POSTGRES_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD
|
||||||
|
POSTGRES_DB=watersurf_erp
|
||||||
|
|
||||||
|
# Django
|
||||||
|
SECRET_KEY=CHANGE_ME_SECRET_KEY_MIN_50_CHARS
|
||||||
|
DEBUG=false
|
||||||
|
ALLOWED_HOSTS=erp.gen7x.ru,localhost,127.0.0.1,app
|
||||||
|
|
||||||
|
# Подключение к БД (приложение)
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=watersurf_erp
|
||||||
|
DB_USER=watersurf_erp
|
||||||
|
DB_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD
|
||||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.env
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
*.sqlite3
|
||||||
|
.git/
|
||||||
|
logs/*.log
|
||||||
|
app/logs/*.log
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONPATH=/app/app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libpq5 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN mkdir -p logs app/logs
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "config.wsgi:application"]
|
||||||
24
HISTORY.md
Normal file
24
HISTORY.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# История изменений ERP WaterSurf
|
||||||
|
|
||||||
|
## 2025-02-25 15:00 UTC – Начальная структура проекта (MVP)
|
||||||
|
|
||||||
|
**Проблема**: необходимо развернуть систему класса ERP для WaterSurf с веб-доступом, авторизацией и хранением данных в БД.
|
||||||
|
|
||||||
|
**Решение**:
|
||||||
|
- Создан проект в `/opt/watersurf-erp`: Django 5, PostgreSQL 16, Docker Compose.
|
||||||
|
- Реализованы справочники: Валюты, Виды заказов, Клиенты, Организации, Поставщики, Сотрудники, Счета денежных средств, Товары (CRUD через веб и админку).
|
||||||
|
- Реализованы документы: Заказ клиента, Заказ поставщику (с табличными частями товаров), Поступление/Перемещение/Расход денежных средств (автогенерация номера).
|
||||||
|
- Связь пользователь → сотрудник (профиль) для автоматической подстановки автора в документах.
|
||||||
|
- Настроены Nginx (erp.gen7x.ru), добавлен volume в список бэкапов платформы.
|
||||||
|
|
||||||
|
**Изменения**:
|
||||||
|
- Структура приложения: config, references, documents, users; шаблоны, формы, представления; миграции.
|
||||||
|
- Файлы: README.md, HISTORY.md, .env.example, docker-compose.yml, Dockerfile, manage.sh, requirements.txt.
|
||||||
|
|
||||||
|
**Проверка**:
|
||||||
|
```bash
|
||||||
|
cd /opt/watersurf-erp
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f app
|
||||||
|
# Открыть https://erp.gen7x.ru/ (после перезагрузки Nginx)
|
||||||
|
```
|
||||||
81
README.md
Normal file
81
README.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# ERP WaterSurf
|
||||||
|
|
||||||
|
Веб-приложение класса ERP для компании WaterSurf: справочники, документы (заказы клиентов и поставщикам, поступления/перемещения/расход денежных средств), авторизация пользователей.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Docker и Docker Compose
|
||||||
|
- Доступ к порту 8010 на localhost (для reverse proxy)
|
||||||
|
|
||||||
|
## Установка и настройка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/watersurf-erp
|
||||||
|
cp .env.example .env
|
||||||
|
# Отредактировать .env: пароли, SECRET_KEY, ALLOWED_HOSTS
|
||||||
|
docker compose up -d db
|
||||||
|
docker compose run --rm app python manage.py migrate --noinput
|
||||||
|
docker compose run --rm app python manage.py createsuperuser # первый пользователь
|
||||||
|
docker compose up -d app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
Переменные в `.env` (см. `.env.example`):
|
||||||
|
|
||||||
|
- `POSTGRES_*` — учётные данные PostgreSQL для контейнера db
|
||||||
|
- `DB_*` — подключение приложения к БД
|
||||||
|
- `SECRET_KEY` — секрет Django (не менее 50 символов)
|
||||||
|
- `ALLOWED_HOSTS` — через запятую (например `erp.gen7x.ru,localhost`)
|
||||||
|
- `DEBUG` — true/false
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
- Веб-интерфейс: после настройки Nginx — https://erp.gen7x.ru/
|
||||||
|
- Вход по логину/паролю (пользователи Django).
|
||||||
|
- Меню: Справочники (валюты, виды заказов, клиенты, организации, поставщики, сотрудники, счета, товары), Документы (заказы клиентов, заказы поставщику, поступления, перемещения, расходы).
|
||||||
|
- Поле «Автор» в документах подставляется из профиля пользователя (связь User → Сотрудник в разделе «Пользователи» / админка).
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
- `app/config/` — настройки Django
|
||||||
|
- `app/references/` — справочники (модели, админка, CRUD)
|
||||||
|
- `app/documents/` — документы (модели, формы, представления, автогенерация номера)
|
||||||
|
- `app/users/` — профиль пользователя (связь с сотрудником)
|
||||||
|
- `app/templates/` — шаблоны
|
||||||
|
- `manage.py` — точка входа для команд Django
|
||||||
|
|
||||||
|
## Docker Compose
|
||||||
|
|
||||||
|
- Сервис `db`: PostgreSQL 16, volume `watersurf_erp_data`
|
||||||
|
- Сервис `app`: Django + Gunicorn, порт 8010:8000
|
||||||
|
|
||||||
|
Запуск: `./manage.sh start` или `docker compose up -d`
|
||||||
|
Остановка: `./manage.sh stop`
|
||||||
|
Логи: `./manage.sh logs app`
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
- `watersurf_erp_data` — данные PostgreSQL. Добавлен в `/opt/gen7x/backup/include-volumes.txt` для бэкапа.
|
||||||
|
|
||||||
|
## Логирование
|
||||||
|
|
||||||
|
- Логи приложения: stdout контейнера; при наличии каталога `app/logs` — также в `app/logs/app.log`.
|
||||||
|
- Просмотр: `docker compose logs -f app`
|
||||||
|
|
||||||
|
## Резервное копирование и восстановление
|
||||||
|
|
||||||
|
- Volume БД включён в список бэкапов платформы Gen7x. Восстановление — по инструкции платформы (restore.sh и т.д.).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- Ошибка подключения к БД: проверить, что контейнер `db` запущен и переменные `DB_*` в `.env` совпадают с `POSTGRES_*`.
|
||||||
|
- 502 от Nginx: проверить, что приложение слушает на 8010 (`docker compose ps`, `docker compose logs app`).
|
||||||
|
- После добавления полей в модели: `docker compose run --rm app python manage.py makemigrations && docker compose run --rm app python manage.py migrate`.
|
||||||
|
|
||||||
|
## Команды управления
|
||||||
|
|
||||||
|
- Запуск: `./manage.sh start`
|
||||||
|
- Остановка: `./manage.sh stop`
|
||||||
|
- Перезапуск: `./manage.sh restart`
|
||||||
|
- Логи: `./manage.sh logs [service]`
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
0
app/config/__init__.py
Normal file
0
app/config/__init__.py
Normal file
117
app/config/settings.py
Normal file
117
app/config/settings.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Настройки Django для ERP WaterSurf.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-change-me")
|
||||||
|
DEBUG = os.environ.get("DEBUG", "false").lower() in ("1", "true", "yes")
|
||||||
|
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"references",
|
||||||
|
"documents",
|
||||||
|
"users",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "config.urls"
|
||||||
|
WSGI_APPLICATION = "config.wsgi.application"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [BASE_DIR / "templates"],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": os.environ.get("DB_NAME", "watersurf_erp"),
|
||||||
|
"USER": os.environ.get("DB_USER", "watersurf_erp"),
|
||||||
|
"PASSWORD": os.environ.get("DB_PASSWORD", ""),
|
||||||
|
"HOST": os.environ.get("DB_HOST", "localhost"),
|
||||||
|
"PORT": os.environ.get("DB_PORT", "5432"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "ru-ru"
|
||||||
|
TIME_ZONE = "Europe/Moscow"
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / "static"] if (BASE_DIR / "static").exists() else []
|
||||||
|
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
LOGIN_URL = "users:login"
|
||||||
|
LOGIN_REDIRECT_URL = "users:home"
|
||||||
|
LOGOUT_REDIRECT_URL = "users:login"
|
||||||
|
|
||||||
|
# Логирование
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"verbose": {
|
||||||
|
"format": "{levelname} {asctime} {module} {message}",
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"class": "logging.FileHandler",
|
||||||
|
"filename": BASE_DIR / "logs" / "app.log",
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "INFO",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_log_file = BASE_DIR / "logs" / "app.log"
|
||||||
|
if _log_file.parent.exists():
|
||||||
|
LOGGING["root"]["handlers"].append("file")
|
||||||
12
app/config/urls.py
Normal file
12
app/config/urls.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""
|
||||||
|
Главный маршрутизатор URL.
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("", include("users.urls")),
|
||||||
|
path("references/", include("references.urls")),
|
||||||
|
path("documents/", include("documents.urls")),
|
||||||
|
]
|
||||||
8
app/config/wsgi.py
Normal file
8
app/config/wsgi.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
WSGI-конфигурация для ERP WaterSurf.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
application = get_wsgi_application()
|
||||||
0
app/documents/__init__.py
Normal file
0
app/documents/__init__.py
Normal file
47
app/documents/admin.py
Normal file
47
app/documents/admin.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import (
|
||||||
|
CustomerOrder,
|
||||||
|
CustomerOrderItem,
|
||||||
|
SupplierOrder,
|
||||||
|
SupplierOrderItem,
|
||||||
|
CashInflow,
|
||||||
|
CashTransfer,
|
||||||
|
CashExpense,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerOrderItemInline(admin.TabularInline):
|
||||||
|
model = CustomerOrderItem
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CustomerOrder)
|
||||||
|
class CustomerOrderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("number", "date", "organization", "client", "total_amount")
|
||||||
|
inlines = [CustomerOrderItemInline]
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierOrderItemInline(admin.TabularInline):
|
||||||
|
model = SupplierOrderItem
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SupplierOrder)
|
||||||
|
class SupplierOrderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("number", "date", "organization", "supplier", "total_in_currency", "total_amount")
|
||||||
|
inlines = [SupplierOrderItemInline]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CashInflow)
|
||||||
|
class CashInflowAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("number", "date", "recipient", "amount", "customer_order")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CashTransfer)
|
||||||
|
class CashTransferAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("number", "date", "sender", "recipient", "amount")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CashExpense)
|
||||||
|
class CashExpenseAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("number", "date", "sender", "amount", "supplier_order")
|
||||||
7
app/documents/apps.py
Normal file
7
app/documents/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "documents"
|
||||||
|
verbose_name = "Документы"
|
||||||
58
app/documents/forms.py
Normal file
58
app/documents/forms.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Формы документов."""
|
||||||
|
from django import forms
|
||||||
|
from .models import (
|
||||||
|
CustomerOrder,
|
||||||
|
CustomerOrderItem,
|
||||||
|
SupplierOrder,
|
||||||
|
SupplierOrderItem,
|
||||||
|
CashInflow,
|
||||||
|
CashTransfer,
|
||||||
|
CashExpense,
|
||||||
|
)
|
||||||
|
from django.forms import inlineformset_factory
|
||||||
|
|
||||||
|
CustomerOrderItemFormSet = inlineformset_factory(
|
||||||
|
CustomerOrder,
|
||||||
|
CustomerOrderItem,
|
||||||
|
fields=("product", "price", "currency", "quantity"),
|
||||||
|
extra=1,
|
||||||
|
can_delete=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
SupplierOrderItemFormSet = inlineformset_factory(
|
||||||
|
SupplierOrder,
|
||||||
|
SupplierOrderItem,
|
||||||
|
fields=("product", "price", "currency", "quantity"),
|
||||||
|
extra=1,
|
||||||
|
can_delete=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerOrderForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CustomerOrder
|
||||||
|
fields = ("date", "number", "order_kind", "organization", "client", "author")
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierOrderForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = SupplierOrder
|
||||||
|
fields = ("date", "number", "organization", "supplier", "currency", "rate", "author")
|
||||||
|
|
||||||
|
|
||||||
|
class CashInflowForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CashInflow
|
||||||
|
fields = ("date", "number", "recipient", "amount", "customer_order", "comment", "author")
|
||||||
|
|
||||||
|
|
||||||
|
class CashTransferForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CashTransfer
|
||||||
|
fields = ("date", "number", "sender", "recipient", "amount", "comment", "author")
|
||||||
|
|
||||||
|
|
||||||
|
class CashExpenseForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CashExpense
|
||||||
|
fields = ("date", "number", "sender", "amount", "supplier_order", "comment", "author")
|
||||||
136
app/documents/migrations/0001_initial.py
Normal file
136
app/documents/migrations/0001_initial.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-02-25 14:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('references', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CashTransfer',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(verbose_name='Дата')),
|
||||||
|
('number', models.CharField(max_length=50, verbose_name='Номер')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=18, verbose_name='Сумма')),
|
||||||
|
('comment', models.CharField(blank=True, max_length=500, verbose_name='Комментарий')),
|
||||||
|
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cash_transfers', to='references.employee', verbose_name='Автор')),
|
||||||
|
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transfers_received', to='references.cashaccount', verbose_name='Получатель')),
|
||||||
|
('sender', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transfers_sent', to='references.cashaccount', verbose_name='Отправитель')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Перемещение денежных средств',
|
||||||
|
'verbose_name_plural': 'Перемещения денежных средств',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomerOrder',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(verbose_name='Дата')),
|
||||||
|
('number', models.CharField(max_length=50, verbose_name='Номер')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, default=Decimal('0'), editable=False, max_digits=18, verbose_name='Стоимость заказа')),
|
||||||
|
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer_orders', to='references.employee', verbose_name='Автор')),
|
||||||
|
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='references.client', verbose_name='Клиент')),
|
||||||
|
('order_kind', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='references.orderkind', verbose_name='Вид заказа')),
|
||||||
|
('organization', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='references.organization', verbose_name='Организация')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Заказ клиента',
|
||||||
|
'verbose_name_plural': 'Заказы клиентов',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CashInflow',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(verbose_name='Дата')),
|
||||||
|
('number', models.CharField(max_length=50, verbose_name='Номер')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=18, verbose_name='Сумма')),
|
||||||
|
('comment', models.CharField(blank=True, max_length=500, verbose_name='Комментарий')),
|
||||||
|
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cash_inflows', to='references.employee', verbose_name='Автор')),
|
||||||
|
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inflows', to='references.cashaccount', verbose_name='Получатель')),
|
||||||
|
('customer_order', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cash_inflows', to='documents.customerorder', verbose_name='Заказ клиента')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Поступление денежных средств',
|
||||||
|
'verbose_name_plural': 'Поступления денежных средств',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomerOrderItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('price', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=18, verbose_name='Цена')),
|
||||||
|
('quantity', models.DecimalField(decimal_places=4, default=Decimal('1'), max_digits=18, verbose_name='Количество')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, default=Decimal('0'), editable=False, max_digits=18, verbose_name='Стоимость')),
|
||||||
|
('currency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='references.currency', verbose_name='Валюта')),
|
||||||
|
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='documents.customerorder', verbose_name='Заказ')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='references.product', verbose_name='Товар')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Строка заказа клиента',
|
||||||
|
'verbose_name_plural': 'Строки заказа клиента',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SupplierOrder',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(verbose_name='Дата')),
|
||||||
|
('number', models.CharField(max_length=50, verbose_name='Номер')),
|
||||||
|
('rate', models.DecimalField(decimal_places=6, default=Decimal('1'), max_digits=18, verbose_name='Курс валюты')),
|
||||||
|
('total_in_currency', models.DecimalField(decimal_places=2, default=Decimal('0'), editable=False, max_digits=18, verbose_name='Стоимость в валюте')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, default=Decimal('0'), editable=False, max_digits=18, verbose_name='Стоимость заказа')),
|
||||||
|
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='supplier_orders', to='references.employee', verbose_name='Автор')),
|
||||||
|
('currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='references.currency', verbose_name='Валюта')),
|
||||||
|
('organization', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='references.organization', verbose_name='Организация')),
|
||||||
|
('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='references.supplier', verbose_name='Поставщик')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Заказ поставщику',
|
||||||
|
'verbose_name_plural': 'Заказы поставщику',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CashExpense',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(verbose_name='Дата')),
|
||||||
|
('number', models.CharField(max_length=50, verbose_name='Номер')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=18, verbose_name='Сумма')),
|
||||||
|
('comment', models.CharField(blank=True, max_length=500, verbose_name='Комментарий')),
|
||||||
|
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cash_expenses', to='references.employee', verbose_name='Автор')),
|
||||||
|
('sender', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to='references.cashaccount', verbose_name='Отправитель')),
|
||||||
|
('supplier_order', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cash_expenses', to='documents.supplierorder', verbose_name='Заказ поставщику')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Расход денежных средств',
|
||||||
|
'verbose_name_plural': 'Расходы денежных средств',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SupplierOrderItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('price', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=18, verbose_name='Цена')),
|
||||||
|
('quantity', models.DecimalField(decimal_places=4, default=Decimal('1'), max_digits=18, verbose_name='Количество')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, default=Decimal('0'), editable=False, max_digits=18, verbose_name='Стоимость')),
|
||||||
|
('currency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='references.currency', verbose_name='Валюта')),
|
||||||
|
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='documents.supplierorder', verbose_name='Заказ поставщику')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='references.product', verbose_name='Товар')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Строка заказа поставщику',
|
||||||
|
'verbose_name_plural': 'Строки заказа поставщику',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
app/documents/migrations/__init__.py
Normal file
0
app/documents/migrations/__init__.py
Normal file
105
app/documents/models.py
Normal file
105
app/documents/models.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
"""Документы ERP WaterSurf."""
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db import models
|
||||||
|
from references.models import Currency, OrderKind, Client, Organization, Supplier, Employee, CashAccount, Product
|
||||||
|
|
||||||
|
class CustomerOrder(models.Model):
|
||||||
|
date = models.DateField("Дата")
|
||||||
|
number = models.CharField("Номер", max_length=50)
|
||||||
|
order_kind = models.ForeignKey(OrderKind, on_delete=models.PROTECT, verbose_name="Вид заказа")
|
||||||
|
organization = models.ForeignKey(Organization, on_delete=models.PROTECT, verbose_name="Организация")
|
||||||
|
client = models.ForeignKey(Client, on_delete=models.PROTECT, verbose_name="Клиент")
|
||||||
|
total_amount = models.DecimalField("Стоимость заказа", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False)
|
||||||
|
author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="customer_orders")
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Заказ клиента"
|
||||||
|
verbose_name_plural = "Заказы клиентов"
|
||||||
|
def __str__(self): return f"{self.number} от {self.date}"
|
||||||
|
def recalc_total(self): self.total_amount = sum((i.price * i.quantity for i in self.items.all()), Decimal("0"))
|
||||||
|
|
||||||
|
class CustomerOrderItem(models.Model):
|
||||||
|
document = models.ForeignKey(CustomerOrder, on_delete=models.CASCADE, related_name="items", verbose_name="Заказ")
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар")
|
||||||
|
price = models.DecimalField("Цена", max_digits=18, decimal_places=2, default=Decimal("0"))
|
||||||
|
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, verbose_name="Валюта", null=True, blank=True)
|
||||||
|
quantity = models.DecimalField("Количество", max_digits=18, decimal_places=4, default=Decimal("1"))
|
||||||
|
amount = models.DecimalField("Стоимость", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False)
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Строка заказа клиента"
|
||||||
|
verbose_name_plural = "Строки заказа клиента"
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.amount = self.price * self.quantity
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierOrder(models.Model):
|
||||||
|
date = models.DateField("Дата")
|
||||||
|
number = models.CharField("Номер", max_length=50)
|
||||||
|
organization = models.ForeignKey(Organization, on_delete=models.PROTECT, verbose_name="Организация")
|
||||||
|
supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT, verbose_name="Поставщик")
|
||||||
|
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, verbose_name="Валюта")
|
||||||
|
rate = models.DecimalField("Курс валюты", max_digits=18, decimal_places=6, default=Decimal("1"))
|
||||||
|
total_in_currency = models.DecimalField("Стоимость в валюте", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False)
|
||||||
|
total_amount = models.DecimalField("Стоимость заказа", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False)
|
||||||
|
author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="supplier_orders")
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Заказ поставщику"
|
||||||
|
verbose_name_plural = "Заказы поставщику"
|
||||||
|
def __str__(self): return f"{self.number} от {self.date}"
|
||||||
|
def recalc_totals(self):
|
||||||
|
self.total_in_currency = sum((i.price * i.quantity for i in self.items.all()), Decimal("0"))
|
||||||
|
self.total_amount = self.total_in_currency * self.rate
|
||||||
|
|
||||||
|
class SupplierOrderItem(models.Model):
|
||||||
|
document = models.ForeignKey(SupplierOrder, on_delete=models.CASCADE, related_name="items", verbose_name="Заказ поставщику")
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар")
|
||||||
|
price = models.DecimalField("Цена", max_digits=18, decimal_places=2, default=Decimal("0"))
|
||||||
|
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, verbose_name="Валюта", null=True, blank=True)
|
||||||
|
quantity = models.DecimalField("Количество", max_digits=18, decimal_places=4, default=Decimal("1"))
|
||||||
|
amount = models.DecimalField("Стоимость", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False)
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Строка заказа поставщику"
|
||||||
|
verbose_name_plural = "Строки заказа поставщику"
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.amount = self.price * self.quantity
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
class CashInflow(models.Model):
|
||||||
|
date = models.DateField("Дата")
|
||||||
|
number = models.CharField("Номер", max_length=50)
|
||||||
|
recipient = models.ForeignKey(CashAccount, on_delete=models.PROTECT, verbose_name="Получатель", related_name="inflows")
|
||||||
|
amount = models.DecimalField("Сумма", max_digits=18, decimal_places=2, default=Decimal("0"))
|
||||||
|
customer_order = models.ForeignKey(CustomerOrder, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Заказ клиента", related_name="cash_inflows")
|
||||||
|
comment = models.CharField("Комментарий", max_length=500, blank=True)
|
||||||
|
author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="cash_inflows")
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Поступление денежных средств"
|
||||||
|
verbose_name_plural = "Поступления денежных средств"
|
||||||
|
def __str__(self): return f"{self.number} от {self.date}"
|
||||||
|
|
||||||
|
class CashTransfer(models.Model):
|
||||||
|
date = models.DateField("Дата")
|
||||||
|
number = models.CharField("Номер", max_length=50)
|
||||||
|
sender = models.ForeignKey(CashAccount, on_delete=models.PROTECT, verbose_name="Отправитель", related_name="transfers_sent")
|
||||||
|
recipient = models.ForeignKey(CashAccount, on_delete=models.PROTECT, verbose_name="Получатель", related_name="transfers_received")
|
||||||
|
amount = models.DecimalField("Сумма", max_digits=18, decimal_places=2, default=Decimal("0"))
|
||||||
|
comment = models.CharField("Комментарий", max_length=500, blank=True)
|
||||||
|
author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="cash_transfers")
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Перемещение денежных средств"
|
||||||
|
verbose_name_plural = "Перемещения денежных средств"
|
||||||
|
def __str__(self): return f"{self.number} от {self.date}"
|
||||||
|
|
||||||
|
class CashExpense(models.Model):
|
||||||
|
date = models.DateField("Дата")
|
||||||
|
number = models.CharField("Номер", max_length=50)
|
||||||
|
sender = models.ForeignKey(CashAccount, on_delete=models.PROTECT, verbose_name="Отправитель", related_name="expenses")
|
||||||
|
amount = models.DecimalField("Сумма", max_digits=18, decimal_places=2, default=Decimal("0"))
|
||||||
|
supplier_order = models.ForeignKey(SupplierOrder, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Заказ поставщику", related_name="cash_expenses")
|
||||||
|
comment = models.CharField("Комментарий", max_length=500, blank=True)
|
||||||
|
author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="cash_expenses")
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Расход денежных средств"
|
||||||
|
verbose_name_plural = "Расходы денежных средств"
|
||||||
|
def __str__(self): return f"{self.number} от {self.date}"
|
||||||
12
app/documents/services.py
Normal file
12
app/documents/services.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.db.models import Max
|
||||||
|
|
||||||
|
def next_number(model_class, field_name="number"):
|
||||||
|
"""Следующий номер по порядку (максимум + 1). Для числоподобных строк."""
|
||||||
|
agg = model_class.objects.aggregate(m=Max(field_name))
|
||||||
|
current = agg["m"]
|
||||||
|
if current is None:
|
||||||
|
return "1"
|
||||||
|
try:
|
||||||
|
return str(int(current) + 1)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return "1"
|
||||||
27
app/documents/urls.py
Normal file
27
app/documents/urls.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "documents"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("customer-orders/", views.CustomerOrderList.as_view(), name="customer_order_list"),
|
||||||
|
path("customer-orders/create/", views.CustomerOrderCreate.as_view(), name="customer_order_create"),
|
||||||
|
path("customer-orders/<int:pk>/edit/", views.CustomerOrderUpdate.as_view(), name="customer_order_edit"),
|
||||||
|
path("customer-orders/<int:pk>/delete/", views.CustomerOrderDelete.as_view(), name="customer_order_delete"),
|
||||||
|
path("supplier-orders/", views.SupplierOrderList.as_view(), name="supplier_order_list"),
|
||||||
|
path("supplier-orders/create/", views.SupplierOrderCreate.as_view(), name="supplier_order_create"),
|
||||||
|
path("supplier-orders/<int:pk>/edit/", views.SupplierOrderUpdate.as_view(), name="supplier_order_edit"),
|
||||||
|
path("supplier-orders/<int:pk>/delete/", views.SupplierOrderDelete.as_view(), name="supplier_order_delete"),
|
||||||
|
path("cash-inflows/", views.CashInflowList.as_view(), name="cash_inflow_list"),
|
||||||
|
path("cash-inflows/create/", views.CashInflowCreate.as_view(), name="cash_inflow_create"),
|
||||||
|
path("cash-inflows/<int:pk>/edit/", views.CashInflowUpdate.as_view(), name="cash_inflow_edit"),
|
||||||
|
path("cash-inflows/<int:pk>/delete/", views.CashInflowDelete.as_view(), name="cash_inflow_delete"),
|
||||||
|
path("cash-transfers/", views.CashTransferList.as_view(), name="cash_transfer_list"),
|
||||||
|
path("cash-transfers/create/", views.CashTransferCreate.as_view(), name="cash_transfer_create"),
|
||||||
|
path("cash-transfers/<int:pk>/edit/", views.CashTransferUpdate.as_view(), name="cash_transfer_edit"),
|
||||||
|
path("cash-transfers/<int:pk>/delete/", views.CashTransferDelete.as_view(), name="cash_transfer_delete"),
|
||||||
|
path("cash-expenses/", views.CashExpenseList.as_view(), name="cash_expense_list"),
|
||||||
|
path("cash-expenses/create/", views.CashExpenseCreate.as_view(), name="cash_expense_create"),
|
||||||
|
path("cash-expenses/<int:pk>/edit/", views.CashExpenseUpdate.as_view(), name="cash_expense_edit"),
|
||||||
|
path("cash-expenses/<int:pk>/delete/", views.CashExpenseDelete.as_view(), name="cash_expense_delete"),
|
||||||
|
]
|
||||||
348
app/documents/views.py
Normal file
348
app/documents/views.py
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
"""Представления документов: списки и формы создания/редактирования."""
|
||||||
|
import logging
|
||||||
|
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.shortcuts import redirect, get_object_or_404
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
|
from users.utils import get_author_employee
|
||||||
|
from .models import (
|
||||||
|
CustomerOrder,
|
||||||
|
SupplierOrder,
|
||||||
|
CashInflow,
|
||||||
|
CashTransfer,
|
||||||
|
CashExpense,
|
||||||
|
)
|
||||||
|
from .forms import (
|
||||||
|
CustomerOrderForm,
|
||||||
|
CustomerOrderItemFormSet,
|
||||||
|
SupplierOrderForm,
|
||||||
|
SupplierOrderItemFormSet,
|
||||||
|
CashInflowForm,
|
||||||
|
CashTransferForm,
|
||||||
|
CashExpenseForm,
|
||||||
|
)
|
||||||
|
from .services import next_number
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def set_author(form, request):
|
||||||
|
"""Подставить автора из профиля пользователя."""
|
||||||
|
author = get_author_employee(request.user)
|
||||||
|
if author and "author" in form.fields:
|
||||||
|
form.instance.author = author
|
||||||
|
|
||||||
|
|
||||||
|
# --- Заказы клиентов ---
|
||||||
|
class CustomerOrderList(LoginRequiredMixin, ListView):
|
||||||
|
model = CustomerOrder
|
||||||
|
template_name = "documents/customer_order_list.html"
|
||||||
|
context_object_name = "object_list"
|
||||||
|
|
||||||
|
class CustomerOrderCreate(LoginRequiredMixin, CreateView):
|
||||||
|
model = CustomerOrder
|
||||||
|
form_class = CustomerOrderForm
|
||||||
|
template_name = "documents/order_form.html"
|
||||||
|
success_url = reverse_lazy("documents:customer_order_list")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["formset"] = CustomerOrderItemFormSet(instance=self.object) if self.object.pk else CustomerOrderItemFormSet()
|
||||||
|
ctx["title"] = "Заказ клиента"
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
set_author(form, self.request)
|
||||||
|
self.object = form.save()
|
||||||
|
formset = CustomerOrderItemFormSet(self.request.POST, instance=self.object)
|
||||||
|
if formset.is_valid():
|
||||||
|
formset.save()
|
||||||
|
self.object.recalc_total()
|
||||||
|
self.object.save()
|
||||||
|
logger.info("Создан заказ клиента %s", self.object)
|
||||||
|
messages.success(self.request, "Заказ создан.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
return self.render_to_response(self.get_context_data(form=form))
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = None
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerOrderUpdate(LoginRequiredMixin, UpdateView):
|
||||||
|
model = CustomerOrder
|
||||||
|
form_class = CustomerOrderForm
|
||||||
|
template_name = "documents/order_form.html"
|
||||||
|
context_object_name = "object"
|
||||||
|
success_url = reverse_lazy("documents:customer_order_list")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["formset"] = CustomerOrderItemFormSet(instance=self.object)
|
||||||
|
ctx["title"] = "Заказ клиента"
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
formset = CustomerOrderItemFormSet(self.request.POST, instance=self.object)
|
||||||
|
if formset.is_valid():
|
||||||
|
form.save()
|
||||||
|
formset.save()
|
||||||
|
self.object.recalc_total()
|
||||||
|
self.object.save()
|
||||||
|
logger.info("Обновлён заказ клиента %s", self.object)
|
||||||
|
messages.success(self.request, "Заказ сохранён.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
return self.render_to_response(self.get_context_data(form=form))
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerOrderDelete(LoginRequiredMixin, DeleteView):
|
||||||
|
model = CustomerOrder
|
||||||
|
template_name = "documents/confirm_delete.html"
|
||||||
|
success_url = reverse_lazy("documents:customer_order_list")
|
||||||
|
context_object_name = "object"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Заказы поставщику ---
|
||||||
|
class SupplierOrderList(LoginRequiredMixin, ListView):
|
||||||
|
model = SupplierOrder
|
||||||
|
template_name = "documents/supplier_order_list.html"
|
||||||
|
context_object_name = "object_list"
|
||||||
|
|
||||||
|
class SupplierOrderCreate(LoginRequiredMixin, CreateView):
|
||||||
|
model = SupplierOrder
|
||||||
|
form_class = SupplierOrderForm
|
||||||
|
template_name = "documents/supplier_order_form.html"
|
||||||
|
success_url = reverse_lazy("documents:supplier_order_list")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["formset"] = SupplierOrderItemFormSet(instance=self.object) if self.object and self.object.pk else SupplierOrderItemFormSet()
|
||||||
|
ctx["title"] = "Заказ поставщику"
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
set_author(form, self.request)
|
||||||
|
self.object = form.save()
|
||||||
|
formset = SupplierOrderItemFormSet(self.request.POST, instance=self.object)
|
||||||
|
if formset.is_valid():
|
||||||
|
formset.save()
|
||||||
|
self.object.recalc_totals()
|
||||||
|
self.object.save()
|
||||||
|
logger.info("Создан заказ поставщику %s", self.object)
|
||||||
|
messages.success(self.request, "Заказ создан.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
return self.render_to_response(self.get_context_data(form=form))
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = None
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierOrderUpdate(LoginRequiredMixin, UpdateView):
|
||||||
|
model = SupplierOrder
|
||||||
|
form_class = SupplierOrderForm
|
||||||
|
template_name = "documents/supplier_order_form.html"
|
||||||
|
context_object_name = "object"
|
||||||
|
success_url = reverse_lazy("documents:supplier_order_list")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["formset"] = SupplierOrderItemFormSet(instance=self.object)
|
||||||
|
ctx["title"] = "Заказ поставщику"
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
formset = SupplierOrderItemFormSet(self.request.POST, instance=self.object)
|
||||||
|
if formset.is_valid():
|
||||||
|
form.save()
|
||||||
|
formset.save()
|
||||||
|
self.object.recalc_totals()
|
||||||
|
self.object.save()
|
||||||
|
logger.info("Обновлён заказ поставщику %s", self.object)
|
||||||
|
messages.success(self.request, "Заказ сохранён.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
return self.render_to_response(self.get_context_data(form=form))
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierOrderDelete(LoginRequiredMixin, DeleteView):
|
||||||
|
model = SupplierOrder
|
||||||
|
template_name = "documents/confirm_delete.html"
|
||||||
|
success_url = reverse_lazy("documents:supplier_order_list")
|
||||||
|
context_object_name = "object"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Поступление денежных средств ---
|
||||||
|
class CashInflowList(LoginRequiredMixin, ListView):
|
||||||
|
model = CashInflow
|
||||||
|
template_name = "documents/cash_inflow_list.html"
|
||||||
|
context_object_name = "object_list"
|
||||||
|
|
||||||
|
class CashInflowCreate(LoginRequiredMixin, CreateView):
|
||||||
|
model = CashInflow
|
||||||
|
form_class = CashInflowForm
|
||||||
|
template_name = "documents/cash_doc_form.html"
|
||||||
|
success_url = reverse_lazy("documents:cash_inflow_list")
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
initial["number"] = next_number(CashInflow)
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
set_author(form, self.request)
|
||||||
|
form.save()
|
||||||
|
logger.info("Создано поступление %s", form.instance)
|
||||||
|
messages.success(self.request, "Поступление создано.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["title"] = "Поступление денежных средств"
|
||||||
|
ctx["cancel_url"] = reverse_lazy("documents:cash_inflow_list")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class CashInflowUpdate(LoginRequiredMixin, UpdateView):
|
||||||
|
model = CashInflow
|
||||||
|
form_class = CashInflowForm
|
||||||
|
template_name = "documents/cash_doc_form.html"
|
||||||
|
context_object_name = "object"
|
||||||
|
success_url = reverse_lazy("documents:cash_inflow_list")
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
logger.info("Обновлено поступление %s", self.object)
|
||||||
|
messages.success(self.request, "Поступление сохранено.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["title"] = "Поступление денежных средств"
|
||||||
|
ctx["cancel_url"] = reverse_lazy("documents:cash_inflow_list")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class CashInflowDelete(LoginRequiredMixin, DeleteView):
|
||||||
|
model = CashInflow
|
||||||
|
template_name = "documents/confirm_delete.html"
|
||||||
|
success_url = reverse_lazy("documents:cash_inflow_list")
|
||||||
|
context_object_name = "object"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Перемещение денежных средств ---
|
||||||
|
class CashTransferList(LoginRequiredMixin, ListView):
|
||||||
|
model = CashTransfer
|
||||||
|
template_name = "documents/cash_transfer_list.html"
|
||||||
|
context_object_name = "object_list"
|
||||||
|
|
||||||
|
class CashTransferCreate(LoginRequiredMixin, CreateView):
|
||||||
|
model = CashTransfer
|
||||||
|
form_class = CashTransferForm
|
||||||
|
template_name = "documents/cash_doc_form.html"
|
||||||
|
success_url = reverse_lazy("documents:cash_transfer_list")
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
initial["number"] = next_number(CashTransfer)
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
set_author(form, self.request)
|
||||||
|
form.save()
|
||||||
|
logger.info("Создано перемещение %s", form.instance)
|
||||||
|
messages.success(self.request, "Перемещение создано.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["title"] = "Перемещение денежных средств"
|
||||||
|
ctx["cancel_url"] = reverse_lazy("documents:cash_transfer_list")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class CashTransferUpdate(LoginRequiredMixin, UpdateView):
|
||||||
|
model = CashTransfer
|
||||||
|
form_class = CashTransferForm
|
||||||
|
template_name = "documents/cash_doc_form.html"
|
||||||
|
context_object_name = "object"
|
||||||
|
success_url = reverse_lazy("documents:cash_transfer_list")
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
logger.info("Обновлено перемещение %s", self.object)
|
||||||
|
messages.success(self.request, "Перемещение сохранено.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["title"] = "Перемещение денежных средств"
|
||||||
|
ctx["cancel_url"] = reverse_lazy("documents:cash_transfer_list")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class CashTransferDelete(LoginRequiredMixin, DeleteView):
|
||||||
|
model = CashTransfer
|
||||||
|
template_name = "documents/confirm_delete.html"
|
||||||
|
success_url = reverse_lazy("documents:cash_transfer_list")
|
||||||
|
context_object_name = "object"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Расход денежных средств ---
|
||||||
|
class CashExpenseList(LoginRequiredMixin, ListView):
|
||||||
|
model = CashExpense
|
||||||
|
template_name = "documents/cash_expense_list.html"
|
||||||
|
context_object_name = "object_list"
|
||||||
|
|
||||||
|
class CashExpenseCreate(LoginRequiredMixin, CreateView):
|
||||||
|
model = CashExpense
|
||||||
|
form_class = CashExpenseForm
|
||||||
|
template_name = "documents/cash_doc_form.html"
|
||||||
|
success_url = reverse_lazy("documents:cash_expense_list")
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
initial["number"] = next_number(CashExpense)
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
set_author(form, self.request)
|
||||||
|
form.save()
|
||||||
|
logger.info("Создан расход %s", form.instance)
|
||||||
|
messages.success(self.request, "Расход создан.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["title"] = "Расход денежных средств"
|
||||||
|
ctx["cancel_url"] = reverse_lazy("documents:cash_expense_list")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class CashExpenseUpdate(LoginRequiredMixin, UpdateView):
|
||||||
|
model = CashExpense
|
||||||
|
form_class = CashExpenseForm
|
||||||
|
template_name = "documents/cash_doc_form.html"
|
||||||
|
context_object_name = "object"
|
||||||
|
success_url = reverse_lazy("documents:cash_expense_list")
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
logger.info("Обновлён расход %s", self.object)
|
||||||
|
messages.success(self.request, "Расход сохранён.")
|
||||||
|
return redirect(self.success_url)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["title"] = "Расход денежных средств"
|
||||||
|
ctx["cancel_url"] = reverse_lazy("documents:cash_expense_list")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class CashExpenseDelete(LoginRequiredMixin, DeleteView):
|
||||||
|
model = CashExpense
|
||||||
|
template_name = "documents/confirm_delete.html"
|
||||||
|
success_url = reverse_lazy("documents:cash_expense_list")
|
||||||
|
context_object_name = "object"
|
||||||
20
app/references/admin.py
Normal file
20
app/references/admin.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import (
|
||||||
|
Currency,
|
||||||
|
OrderKind,
|
||||||
|
Client,
|
||||||
|
Organization,
|
||||||
|
Supplier,
|
||||||
|
Employee,
|
||||||
|
CashAccount,
|
||||||
|
Product,
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.site.register(Currency)
|
||||||
|
admin.site.register(OrderKind)
|
||||||
|
admin.site.register(Client)
|
||||||
|
admin.site.register(Organization)
|
||||||
|
admin.site.register(Supplier)
|
||||||
|
admin.site.register(Employee)
|
||||||
|
admin.site.register(CashAccount)
|
||||||
|
admin.site.register(Product)
|
||||||
7
app/references/apps.py
Normal file
7
app/references/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ReferencesConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "references"
|
||||||
|
verbose_name = "Справочники"
|
||||||
103
app/references/migrations/0001_initial.py
Normal file
103
app/references/migrations/0001_initial.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-02-25 14:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CashAccount',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=300, verbose_name='Название')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Счёт денежных средств',
|
||||||
|
'verbose_name_plural': 'Счета денежных средств',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Client',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=300, verbose_name='Название')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Клиент',
|
||||||
|
'verbose_name_plural': 'Клиенты',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Currency',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='Название')),
|
||||||
|
('code', models.CharField(blank=True, max_length=10, verbose_name='Код')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Валюта',
|
||||||
|
'verbose_name_plural': 'Валюты',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Employee',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=300, verbose_name='ФИО')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Сотрудник',
|
||||||
|
'verbose_name_plural': 'Сотрудники',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrderKind',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='Название')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Вид заказа',
|
||||||
|
'verbose_name_plural': 'Виды заказов',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Organization',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=300, verbose_name='Название')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Организация',
|
||||||
|
'verbose_name_plural': 'Организации',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=300, verbose_name='Название')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Товар',
|
||||||
|
'verbose_name_plural': 'Товары',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Supplier',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=300, verbose_name='Название')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Поставщик',
|
||||||
|
'verbose_name_plural': 'Поставщики',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
app/references/migrations/__init__.py
Normal file
0
app/references/migrations/__init__.py
Normal file
101
app/references/models.py
Normal file
101
app/references/models.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Справочники ERP WaterSurf.
|
||||||
|
"""
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Currency(models.Model):
|
||||||
|
"""Валюты."""
|
||||||
|
name = models.CharField("Название", max_length=100)
|
||||||
|
code = models.CharField("Код", max_length=10, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Валюта"
|
||||||
|
verbose_name_plural = "Валюты"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name or self.code or str(self.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderKind(models.Model):
|
||||||
|
"""Виды заказов."""
|
||||||
|
name = models.CharField("Название", max_length=200)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Вид заказа"
|
||||||
|
verbose_name_plural = "Виды заказов"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Client(models.Model):
|
||||||
|
"""Клиенты."""
|
||||||
|
name = models.CharField("Название", max_length=300)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Клиент"
|
||||||
|
verbose_name_plural = "Клиенты"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(models.Model):
|
||||||
|
"""Организации."""
|
||||||
|
name = models.CharField("Название", max_length=300)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Организация"
|
||||||
|
verbose_name_plural = "Организации"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Supplier(models.Model):
|
||||||
|
"""Поставщики."""
|
||||||
|
name = models.CharField("Название", max_length=300)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Поставщик"
|
||||||
|
verbose_name_plural = "Поставщики"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Employee(models.Model):
|
||||||
|
"""Сотрудники (для поля Автор в документах)."""
|
||||||
|
name = models.CharField("ФИО", max_length=300)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Сотрудник"
|
||||||
|
verbose_name_plural = "Сотрудники"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class CashAccount(models.Model):
|
||||||
|
"""Счета денежных средств."""
|
||||||
|
name = models.CharField("Название", max_length=300)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Счёт денежных средств"
|
||||||
|
verbose_name_plural = "Счета денежных средств"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
"""Товары."""
|
||||||
|
name = models.CharField("Название", max_length=300)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Товар"
|
||||||
|
verbose_name_plural = "Товары"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
39
app/references/urls.py
Normal file
39
app/references/urls.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "references"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("currencies/", views.CurrencyList.as_view(), name="currency_list"),
|
||||||
|
path("currencies/create/", views.CurrencyCreate.as_view(), name="currency_create"),
|
||||||
|
path("currencies/<int:pk>/edit/", views.CurrencyUpdate.as_view(), name="currency_update"),
|
||||||
|
path("currencies/<int:pk>/delete/", views.CurrencyDelete.as_view(), name="currency_delete"),
|
||||||
|
path("order-kinds/", views.OrderKindList.as_view(), name="orderkind_list"),
|
||||||
|
path("order-kinds/create/", views.OrderKindCreate.as_view(), name="orderkind_create"),
|
||||||
|
path("order-kinds/<int:pk>/edit/", views.OrderKindUpdate.as_view(), name="orderkind_update"),
|
||||||
|
path("order-kinds/<int:pk>/delete/", views.OrderKindDelete.as_view(), name="orderkind_delete"),
|
||||||
|
path("clients/", views.ClientList.as_view(), name="client_list"),
|
||||||
|
path("clients/create/", views.ClientCreate.as_view(), name="client_create"),
|
||||||
|
path("clients/<int:pk>/edit/", views.ClientUpdate.as_view(), name="client_update"),
|
||||||
|
path("clients/<int:pk>/delete/", views.ClientDelete.as_view(), name="client_delete"),
|
||||||
|
path("organizations/", views.OrganizationList.as_view(), name="organization_list"),
|
||||||
|
path("organizations/create/", views.OrganizationCreate.as_view(), name="organization_create"),
|
||||||
|
path("organizations/<int:pk>/edit/", views.OrganizationUpdate.as_view(), name="organization_update"),
|
||||||
|
path("organizations/<int:pk>/delete/", views.OrganizationDelete.as_view(), name="organization_delete"),
|
||||||
|
path("suppliers/", views.SupplierList.as_view(), name="supplier_list"),
|
||||||
|
path("suppliers/create/", views.SupplierCreate.as_view(), name="supplier_create"),
|
||||||
|
path("suppliers/<int:pk>/edit/", views.SupplierUpdate.as_view(), name="supplier_update"),
|
||||||
|
path("suppliers/<int:pk>/delete/", views.SupplierDelete.as_view(), name="supplier_delete"),
|
||||||
|
path("employees/", views.EmployeeList.as_view(), name="employee_list"),
|
||||||
|
path("employees/create/", views.EmployeeCreate.as_view(), name="employee_create"),
|
||||||
|
path("employees/<int:pk>/edit/", views.EmployeeUpdate.as_view(), name="employee_update"),
|
||||||
|
path("employees/<int:pk>/delete/", views.EmployeeDelete.as_view(), name="employee_delete"),
|
||||||
|
path("cash-accounts/", views.CashAccountList.as_view(), name="cashaccount_list"),
|
||||||
|
path("cash-accounts/create/", views.CashAccountCreate.as_view(), name="cashaccount_create"),
|
||||||
|
path("cash-accounts/<int:pk>/edit/", views.CashAccountUpdate.as_view(), name="cashaccount_update"),
|
||||||
|
path("cash-accounts/<int:pk>/delete/", views.CashAccountDelete.as_view(), name="cashaccount_delete"),
|
||||||
|
path("products/", views.ProductList.as_view(), name="product_list"),
|
||||||
|
path("products/create/", views.ProductCreate.as_view(), name="product_create"),
|
||||||
|
path("products/<int:pk>/edit/", views.ProductUpdate.as_view(), name="product_update"),
|
||||||
|
path("products/<int:pk>/delete/", views.ProductDelete.as_view(), name="product_delete"),
|
||||||
|
]
|
||||||
131
app/references/views.py
Normal file
131
app/references/views.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"""
|
||||||
|
CRUD для справочников (списки и формы).
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
Currency,
|
||||||
|
OrderKind,
|
||||||
|
Client,
|
||||||
|
Organization,
|
||||||
|
Supplier,
|
||||||
|
Employee,
|
||||||
|
CashAccount,
|
||||||
|
Product,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _ref_view(model_class, list_url_name, create_url_name, update_url_name, delete_url_name, list_title, form_title):
|
||||||
|
"""Фабрика представлений для одного справочника."""
|
||||||
|
class List(LoginRequiredMixin, ListView):
|
||||||
|
model = model_class
|
||||||
|
template_name = "references/reference_list.html"
|
||||||
|
context_object_name = "items"
|
||||||
|
extra_context = {
|
||||||
|
"title": list_title,
|
||||||
|
"create_url_name": create_url_name,
|
||||||
|
"update_url_name": update_url_name,
|
||||||
|
"delete_url_name": delete_url_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Create(LoginRequiredMixin, CreateView):
|
||||||
|
model = model_class
|
||||||
|
fields = "__all__"
|
||||||
|
template_name = "references/reference_form.html"
|
||||||
|
success_url = reverse_lazy(list_url_name)
|
||||||
|
extra_context = {"title": form_title, "cancel_url": reverse_lazy(list_url_name)}
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
messages.success(self.request, _("Запись создана."))
|
||||||
|
logger.info("Справочник %s: создана запись %s", model_class.__name__, form.instance)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
class Update(LoginRequiredMixin, UpdateView):
|
||||||
|
model = model_class
|
||||||
|
fields = "__all__"
|
||||||
|
template_name = "references/reference_form.html"
|
||||||
|
success_url = reverse_lazy(list_url_name)
|
||||||
|
context_object_name = "object"
|
||||||
|
extra_context = {"title": form_title, "cancel_url": reverse_lazy(list_url_name)}
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
messages.success(self.request, _("Запись сохранена."))
|
||||||
|
logger.info("Справочник %s: обновлена запись %s", model_class.__name__, form.instance)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
class Delete(LoginRequiredMixin, DeleteView):
|
||||||
|
model = model_class
|
||||||
|
template_name = "references/reference_confirm_delete.html"
|
||||||
|
success_url = reverse_lazy(list_url_name)
|
||||||
|
context_object_name = "object"
|
||||||
|
extra_context = {"cancel_url": reverse_lazy(list_url_name)}
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
messages.success(self.request, _("Запись удалена."))
|
||||||
|
logger.info("Справочник %s: удалена запись %s", model_class.__name__, self.object)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
return List, Create, Update, Delete
|
||||||
|
|
||||||
|
# Валюты
|
||||||
|
CurrencyList, CurrencyCreate, CurrencyUpdate, CurrencyDelete = _ref_view(
|
||||||
|
Currency, "references:currency_list", "references:currency_create",
|
||||||
|
"references:currency_update", "references:currency_delete",
|
||||||
|
"Валюты", "Валюта",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Виды заказов
|
||||||
|
OrderKindList, OrderKindCreate, OrderKindUpdate, OrderKindDelete = _ref_view(
|
||||||
|
OrderKind, "references:orderkind_list", "references:orderkind_create",
|
||||||
|
"references:orderkind_update", "references:orderkind_delete",
|
||||||
|
"Виды заказов", "Вид заказа",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Клиенты
|
||||||
|
ClientList, ClientCreate, ClientUpdate, ClientDelete = _ref_view(
|
||||||
|
Client, "references:client_list", "references:client_create",
|
||||||
|
"references:client_update", "references:client_delete",
|
||||||
|
"Клиенты", "Клиент",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Организации
|
||||||
|
OrganizationList, OrganizationCreate, OrganizationUpdate, OrganizationDelete = _ref_view(
|
||||||
|
Organization, "references:organization_list", "references:organization_create",
|
||||||
|
"references:organization_update", "references:organization_delete",
|
||||||
|
"Организации", "Организация",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Поставщики
|
||||||
|
SupplierList, SupplierCreate, SupplierUpdate, SupplierDelete = _ref_view(
|
||||||
|
Supplier, "references:supplier_list", "references:supplier_create",
|
||||||
|
"references:supplier_update", "references:supplier_delete",
|
||||||
|
"Поставщики", "Поставщик",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сотрудники
|
||||||
|
EmployeeList, EmployeeCreate, EmployeeUpdate, EmployeeDelete = _ref_view(
|
||||||
|
Employee, "references:employee_list", "references:employee_create",
|
||||||
|
"references:employee_update", "references:employee_delete",
|
||||||
|
"Сотрудники", "Сотрудник",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Счета денежных средств
|
||||||
|
CashAccountList, CashAccountCreate, CashAccountUpdate, CashAccountDelete = _ref_view(
|
||||||
|
CashAccount, "references:cashaccount_list", "references:cashaccount_create",
|
||||||
|
"references:cashaccount_update", "references:cashaccount_delete",
|
||||||
|
"Счета денежных средств", "Счёт денежных средств",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Товары
|
||||||
|
ProductList, ProductCreate, ProductUpdate, ProductDelete = _ref_view(
|
||||||
|
Product, "references:product_list", "references:product_create",
|
||||||
|
"references:product_update", "references:product_delete",
|
||||||
|
"Товары", "Товар",
|
||||||
|
)
|
||||||
71
app/templates/base.html
Normal file
71
app/templates/base.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}ERP WaterSurf{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{% url 'users:home' %}">ERP WaterSurf</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">Справочники</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{% url 'references:currency_list' %}">Валюты</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'references:orderkind_list' %}">Виды заказов</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'references:client_list' %}">Клиенты</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'references:organization_list' %}">Организации</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'references:supplier_list' %}">Поставщики</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'references:employee_list' %}">Сотрудники</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'references:cashaccount_list' %}">Счета денежных средств</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'references:product_list' %}">Товары</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">Документы</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{% url 'documents:customer_order_list' %}">Заказы клиентов</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'documents:supplier_order_list' %}">Заказы поставщику</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'documents:cash_inflow_list' %}">Поступление денежных средств</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'documents:cash_transfer_list' %}">Перемещение денежных средств</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'documents:cash_expense_list' %}">Расход денежных средств</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item"><span class="navbar-text me-2">{{ user.username }}</span></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'users:logout' %}">Выход</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'users:login' %}">Вход</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="container my-4">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
app/templates/documents/cash_doc_form.html
Normal file
11
app/templates/documents/cash_doc_form.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ title }} — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% if object %}Редактировать{% else %}Создать{% endif %} {{ title }}</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||||
|
<a href="{{ cancel_url|default:'#' }}" class="btn btn-secondary">Отмена</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
35
app/templates/documents/cash_expense_list.html
Normal file
35
app/templates/documents/cash_expense_list.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Расходы денежных средств — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Расходы денежных средств</h2>
|
||||||
|
<p><a href="{% url 'documents:cash_expense_create' %}" class="btn btn-primary">Создать</a></p>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Номер</th>
|
||||||
|
<th>Отправитель</th>
|
||||||
|
<th>Сумма</th>
|
||||||
|
<th>Заказ поставщику</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for obj in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ obj.date }}</td>
|
||||||
|
<td>{{ obj.number }}</td>
|
||||||
|
<td>{{ obj.sender }}</td>
|
||||||
|
<td>{{ obj.amount }}</td>
|
||||||
|
<td>{{ obj.supplier_order|default:"—" }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'documents:cash_expense_edit' obj.pk %}">Изменить</a>
|
||||||
|
| <a href="{% url 'documents:cash_expense_delete' obj.pk %}" class="text-danger">Удалить</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6">Нет расходов.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
35
app/templates/documents/cash_inflow_list.html
Normal file
35
app/templates/documents/cash_inflow_list.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Поступления денежных средств — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Поступления денежных средств</h2>
|
||||||
|
<p><a href="{% url 'documents:cash_inflow_create' %}" class="btn btn-primary">Создать</a></p>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Номер</th>
|
||||||
|
<th>Получатель</th>
|
||||||
|
<th>Сумма</th>
|
||||||
|
<th>Заказ клиента</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for obj in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ obj.date }}</td>
|
||||||
|
<td>{{ obj.number }}</td>
|
||||||
|
<td>{{ obj.recipient }}</td>
|
||||||
|
<td>{{ obj.amount }}</td>
|
||||||
|
<td>{{ obj.customer_order|default:"—" }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'documents:cash_inflow_edit' obj.pk %}">Изменить</a>
|
||||||
|
| <a href="{% url 'documents:cash_inflow_delete' obj.pk %}" class="text-danger">Удалить</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6">Нет поступлений.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
35
app/templates/documents/cash_transfer_list.html
Normal file
35
app/templates/documents/cash_transfer_list.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Перемещения денежных средств — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Перемещения денежных средств</h2>
|
||||||
|
<p><a href="{% url 'documents:cash_transfer_create' %}" class="btn btn-primary">Создать</a></p>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Номер</th>
|
||||||
|
<th>Отправитель</th>
|
||||||
|
<th>Получатель</th>
|
||||||
|
<th>Сумма</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for obj in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ obj.date }}</td>
|
||||||
|
<td>{{ obj.number }}</td>
|
||||||
|
<td>{{ obj.sender }}</td>
|
||||||
|
<td>{{ obj.recipient }}</td>
|
||||||
|
<td>{{ obj.amount }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'documents:cash_transfer_edit' obj.pk %}">Изменить</a>
|
||||||
|
| <a href="{% url 'documents:cash_transfer_delete' obj.pk %}" class="text-danger">Удалить</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6">Нет перемещений.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
11
app/templates/documents/confirm_delete.html
Normal file
11
app/templates/documents/confirm_delete.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Удалить — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Удалить запись?</h2>
|
||||||
|
<p>{{ object }}</p>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger">Удалить</button>
|
||||||
|
<a href="{{ request.META.HTTP_REFERER|default:'/' }}" class="btn btn-secondary">Отмена</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
37
app/templates/documents/customer_order_list.html
Normal file
37
app/templates/documents/customer_order_list.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Заказы клиентов — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Заказы клиентов</h2>
|
||||||
|
<p><a href="{% url 'documents:customer_order_create' %}" class="btn btn-primary">Создать заказ</a></p>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Номер</th>
|
||||||
|
<th>Вид заказа</th>
|
||||||
|
<th>Организация</th>
|
||||||
|
<th>Клиент</th>
|
||||||
|
<th>Стоимость заказа</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for obj in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ obj.date }}</td>
|
||||||
|
<td>{{ obj.number }}</td>
|
||||||
|
<td>{{ obj.order_kind }}</td>
|
||||||
|
<td>{{ obj.organization }}</td>
|
||||||
|
<td>{{ obj.client }}</td>
|
||||||
|
<td>{{ obj.total_amount }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'documents:customer_order_edit' obj.pk %}">Изменить</a>
|
||||||
|
| <a href="{% url 'documents:customer_order_delete' obj.pk %}" class="text-danger">Удалить</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="7">Нет заказов.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
48
app/templates/documents/order_form.html
Normal file
48
app/templates/documents/order_form.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ title }} — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% if object %}Редактировать{% else %}Создать{% endif %} {{ title }}</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<h3>Товары</h3>
|
||||||
|
{{ formset.management_form }}
|
||||||
|
<table class="table" id="order-items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Товар</th>
|
||||||
|
<th>Цена</th>
|
||||||
|
<th>Валюта</th>
|
||||||
|
<th>Количество</th>
|
||||||
|
<th>Стоимость</th>
|
||||||
|
<th>Удалить</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for f in formset %}
|
||||||
|
<tr class="item-row">
|
||||||
|
<td>{{ f.id }}{{ f.product }}</td>
|
||||||
|
<td>{{ f.price }}</td>
|
||||||
|
<td>{{ f.currency }}</td>
|
||||||
|
<td>{{ f.quantity }}</td>
|
||||||
|
<td class="row-amount">—</td>
|
||||||
|
<td>{% if f.DELETE %}{{ f.DELETE }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||||
|
<a href="{% url 'documents:customer_order_list' %}" class="btn btn-secondary">Отмена</a>
|
||||||
|
</form>
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.querySelector('form').addEventListener('input', function() {
|
||||||
|
document.querySelectorAll('#order-items .item-row').forEach(function(row) {
|
||||||
|
var price = parseFloat(row.querySelector('input[name$="-price"]')?.value) || 0;
|
||||||
|
var qty = parseFloat(row.querySelector('input[name$="-quantity"]')?.value) || 0;
|
||||||
|
row.querySelector('.row-amount').textContent = (price * qty).toFixed(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
46
app/templates/documents/supplier_order_form.html
Normal file
46
app/templates/documents/supplier_order_form.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ title }} — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% if object %}Редактировать{% else %}Создать{% endif %} {{ title }}</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<h3>Товары</h3>
|
||||||
|
{{ formset.management_form }}
|
||||||
|
<table class="table" id="supplier-order-items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Товар</th>
|
||||||
|
<th>Цена</th>
|
||||||
|
<th>Валюта</th>
|
||||||
|
<th>Количество</th>
|
||||||
|
<th>Стоимость</th>
|
||||||
|
<th>Удалить</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for f in formset %}
|
||||||
|
<tr class="item-row">
|
||||||
|
<td>{{ f.id }}{{ f.product }}</td>
|
||||||
|
<td>{{ f.price }}</td>
|
||||||
|
<td>{{ f.currency }}</td>
|
||||||
|
<td>{{ f.quantity }}</td>
|
||||||
|
<td class="row-amount">—</td>
|
||||||
|
<td>{% if f.DELETE %}{{ f.DELETE }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||||
|
<a href="{% url 'documents:supplier_order_list' %}" class="btn btn-secondary">Отмена</a>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
document.querySelector('form').addEventListener('input', function() {
|
||||||
|
document.querySelectorAll('#supplier-order-items .item-row').forEach(function(row) {
|
||||||
|
var price = parseFloat(row.querySelector('input[name$=\"-price\"]')?.value) || 0;
|
||||||
|
var qty = parseFloat(row.querySelector('input[name$=\"-quantity\"]')?.value) || 0;
|
||||||
|
row.querySelector('.row-amount').textContent = (price * qty).toFixed(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
37
app/templates/documents/supplier_order_list.html
Normal file
37
app/templates/documents/supplier_order_list.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Заказы поставщику — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Заказы поставщику</h2>
|
||||||
|
<p><a href="{% url 'documents:supplier_order_create' %}" class="btn btn-primary">Создать заказ</a></p>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Номер</th>
|
||||||
|
<th>Организация</th>
|
||||||
|
<th>Поставщик</th>
|
||||||
|
<th>Стоимость в валюте</th>
|
||||||
|
<th>Стоимость заказа</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for obj in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ obj.date }}</td>
|
||||||
|
<td>{{ obj.number }}</td>
|
||||||
|
<td>{{ obj.organization }}</td>
|
||||||
|
<td>{{ obj.supplier }}</td>
|
||||||
|
<td>{{ obj.total_in_currency }}</td>
|
||||||
|
<td>{{ obj.total_amount }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'documents:supplier_order_edit' obj.pk %}">Изменить</a>
|
||||||
|
| <a href="{% url 'documents:supplier_order_delete' obj.pk %}" class="text-danger">Удалить</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="7">Нет заказов.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
6
app/templates/home.html
Normal file
6
app/templates/home.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Главная — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>WaterSurf ERP</h1>
|
||||||
|
<p class="lead">Выберите раздел в меню: Справочники или Документы.</p>
|
||||||
|
{% endblock %}
|
||||||
11
app/templates/references/reference_confirm_delete.html
Normal file
11
app/templates/references/reference_confirm_delete.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Удалить {{ object }} — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Удалить запись?</h2>
|
||||||
|
<p>{{ object }}</p>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger">Удалить</button>
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-secondary">Отмена</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
11
app/templates/references/reference_form.html
Normal file
11
app/templates/references/reference_form.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ title }} — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% if object %}Редактировать{% else %}Создать{% endif %} {{ title }}</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-secondary">Отмена</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
30
app/templates/references/reference_list.html
Normal file
30
app/templates/references/reference_list.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ title }} — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
<p><a href="{% url create_url_name %}" class="btn btn-primary">Добавить</a></p>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
{% block table_headers %}<th>Название</th>{% endblock %}
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.pk }}</td>
|
||||||
|
{% block table_cells %}<td>{{ item }}</td>{% endblock %}
|
||||||
|
<td>
|
||||||
|
<a href="{% block edit_url %}{% url update_url_name item.pk %}{% endblock %}">Изменить</a>
|
||||||
|
|
|
||||||
|
<a href="{% block delete_url %}{% url delete_url_name item.pk %}{% endblock %}" class="text-danger">Удалить</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="3">Нет записей.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
14
app/templates/registration/login.html
Normal file
14
app/templates/registration/login.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Вход — ERP WaterSurf{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h2>Вход</h2>
|
||||||
|
<form method="post" action="{% url 'users:login' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">Войти</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
0
app/users/__init__.py
Normal file
0
app/users/__init__.py
Normal file
7
app/users/admin.py
Normal file
7
app/users/admin.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserProfile)
|
||||||
|
class UserProfileAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("user", "employee")
|
||||||
7
app/users/apps.py
Normal file
7
app/users/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "users"
|
||||||
|
verbose_name = "Пользователи"
|
||||||
30
app/users/migrations/0001_initial.py
Normal file
30
app/users/migrations/0001_initial.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-02-25 14:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('references', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('employee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_profiles', to='references.employee', verbose_name='Сотрудник (автор в документах)')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Профиль пользователя',
|
||||||
|
'verbose_name_plural': 'Профили пользователей',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
app/users/migrations/__init__.py
Normal file
0
app/users/migrations/__init__.py
Normal file
31
app/users/models.py
Normal file
31
app/users/models.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
Профиль пользователя: связь User → Сотрудник для поля «Автор» в документах.
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from references.models import Employee
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(models.Model):
|
||||||
|
"""Профиль пользователя: привязка к справочнику Сотрудники."""
|
||||||
|
user = models.OneToOneField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="profile",
|
||||||
|
verbose_name="Пользователь",
|
||||||
|
)
|
||||||
|
employee = models.ForeignKey(
|
||||||
|
Employee,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="user_profiles",
|
||||||
|
verbose_name="Сотрудник (автор в документах)",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Профиль пользователя"
|
||||||
|
verbose_name_plural = "Профили пользователей"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} → {self.employee or '—'}"
|
||||||
10
app/users/urls.py
Normal file
10
app/users/urls.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "users"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.HomeView.as_view(), name="home"),
|
||||||
|
path("login/", views.ErpLoginView.as_view(), name="login"),
|
||||||
|
path("logout/", views.ErpLogoutView.as_view(), name="logout"),
|
||||||
|
]
|
||||||
10
app/users/utils.py
Normal file
10
app/users/utils.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""Хелперы для пользователей."""
|
||||||
|
def get_author_employee(user):
|
||||||
|
"""Возвращает сотрудника (автора) для текущего пользователя, если привязан профиль."""
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
profile = user.profile
|
||||||
|
return profile.employee if profile else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
35
app/users/views.py
Normal file
35
app/users/views.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Вход, выход и главная страница.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from django.contrib.auth.views import LoginView, LogoutView
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HomeView(LoginRequiredMixin, TemplateView):
|
||||||
|
"""Главная страница после входа: меню справочников и документов."""
|
||||||
|
template_name = "home.html"
|
||||||
|
login_url = "users:login"
|
||||||
|
|
||||||
|
|
||||||
|
class ErpLoginView(LoginView):
|
||||||
|
"""Страница входа."""
|
||||||
|
template_name = "registration/login.html"
|
||||||
|
redirect_authenticated_user = True
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
logger.info("Вход пользователя: %s", form.get_user().username)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class ErpLogoutView(LogoutView):
|
||||||
|
"""Выход."""
|
||||||
|
next_page = "users:login"
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
logger.info("Выход пользователя: %s", request.user.username)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
54
docker-compose.yml
Normal file
54
docker-compose.yml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: watersurf_erp_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
volumes:
|
||||||
|
- watersurf_erp_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- watersurf_erp_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: watersurf_erp_app
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${POSTGRES_DB}
|
||||||
|
DB_USER: ${POSTGRES_USER}
|
||||||
|
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
DEBUG: ${DEBUG:-false}
|
||||||
|
ALLOWED_HOSTS: ${ALLOWED_HOSTS}
|
||||||
|
volumes:
|
||||||
|
- ./logs:/app/logs
|
||||||
|
networks:
|
||||||
|
- watersurf_erp_network
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8010:8000"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
watersurf_erp_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
watersurf_erp_network:
|
||||||
|
driver: bridge
|
||||||
0
logs/.gitkeep
Normal file
0
logs/.gitkeep
Normal file
16
manage.py
Normal file
16
manage.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Точка входа для управляющих команд Django."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
base = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
app_dir = os.path.join(base, "app")
|
||||||
|
if app_dir not in sys.path:
|
||||||
|
sys.path.insert(0, app_dir)
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError("Не удалось импортировать Django.") from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
11
manage.sh
Normal file
11
manage.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Управление контейнерами ERP WaterSurf
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
case "${1:-}" in
|
||||||
|
start) docker compose up -d ;;
|
||||||
|
stop) docker compose stop ;;
|
||||||
|
restart) docker compose restart ;;
|
||||||
|
logs) docker compose logs -f "${2:-app}" ;;
|
||||||
|
*) echo "Использование: $0 start|stop|restart|logs [service]"; exit 1 ;;
|
||||||
|
esac
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Django>=5.0,<6
|
||||||
|
psycopg2-binary>=2.9
|
||||||
|
gunicorn>=21.0
|
||||||
|
whitenoise>=6.6
|
||||||
Reference in New Issue
Block a user