Обновил модели: добавил Компании и Габариты
All checks were successful
Deploy MES Core / deploy (push) Successful in 9s
All checks were successful
Deploy MES Core / deploy (push) Successful in 9s
This commit is contained in:
23
Dockerfile
23
Dockerfile
@@ -1,24 +1,39 @@
|
|||||||
|
# Используем "легкую" версию Python на базе Debian Bookworm.
|
||||||
|
# slim — это баланс между размером образа и наличием нужных утилит.
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Указываем рабочую папку внутри контейнера. Все последующие команды будут выполняться в ней.
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Окружение
|
# Настройки окружения:
|
||||||
|
# 1. Запрещаем Python писать файлы .pyc (байткод) на диск, чтобы не мусорить.
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
# 2. Отключаем буферизацию логов. Так ты сразу увидишь ошибки в `docker logs`, а не будешь их ждать.
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Ставим системные либы для Postgres
|
# Ставим системные зависимости:
|
||||||
|
# apt-get update — обновляем списки пакетов.
|
||||||
|
# gcc и libpq-dev — необходимы для сборки библиотеки psycopg2 (драйвер для Postgres).
|
||||||
|
# rm -rf /var/lib/apt/lists/* — удаляем кэш установщика, чтобы уменьшить размер образа.
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
gcc libpq-dev && rm -rf /var/lib/apt/lists/*
|
gcc libpq-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Сначала копируем только список зависимостей.
|
||||||
|
# Это нужно для "кэширования слоев": если ты не менял библиотеки, Docker не будет переустанавливать их заново при сборке.
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Теперь копируем весь остальной код проекта в контейнер.
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Обязательно даем права скрипту
|
# Даем права на выполнение нашему скрипту запуска.
|
||||||
|
# Без этого контейнер может упасть с ошибкой "Permission denied".
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
# Используем путь из рабочей папки
|
# ENTRYPOINT — это команда, которая выполняется ВСЕГДА при старте.
|
||||||
|
# Наш скрипт подготовит базу (миграции) и соберет статику.
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
|
||||||
|
# CMD — это основная команда процесса.
|
||||||
|
# Запускаем Gunicorn, привязываем его к порту 8000 и ставим 3 рабочих процесса для скорости.
|
||||||
CMD ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
|
CMD ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
|
||||||
63
README.md
63
README.md
@@ -1,3 +1,64 @@
|
|||||||
# mes_core
|
# mes_core
|
||||||
|
|
||||||
Система управления производством
|
Система управления производством (ShiftFlow MES)
|
||||||
|
|
||||||
|
## 📥 Как пушить код (Шпаргалка)
|
||||||
|
|
||||||
|
Если внес изменения в проект и готов отправить их на сервер `ProdServ`:
|
||||||
|
|
||||||
|
1. **Подготовь файлы**:
|
||||||
|
`git add .`
|
||||||
|
2. **Запечатай изменения**:
|
||||||
|
`git commit -m "Подправил докерфайл сборки контейнера"`
|
||||||
|
3. **Отправляй в полет**:
|
||||||
|
`git push origin main`
|
||||||
|
|
||||||
|
### 💊 Таблетка: Если не пушится (сброс авторизации)
|
||||||
|
Если Git "забыл" пароль или выдает ошибку Permission Denied:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Очищаем старые привязки
|
||||||
|
git config --global --unset credential.helper
|
||||||
|
|
||||||
|
# Устанавливаем менеджер заново (для Windows)
|
||||||
|
git config --global credential.helper manager
|
||||||
|
|
||||||
|
# Снова пушим и вводим логин/пароль в окне
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# 🚀 ShiftFlow MES - Инструкция по деплою (Production)
|
||||||
|
|
||||||
|
### 🧩 Общая архитектура
|
||||||
|
Система работает в связке из 3-х контейнеров:
|
||||||
|
1. **db**: PostgreSQL 15 (хранит все данные).
|
||||||
|
2. **web**: Django + Gunicorn (логика приложения).
|
||||||
|
3. **nginx**: Веб-сервер (раздает статику и проксирует запросы на Django).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🛠 Как это работает (Автоматизация)
|
||||||
|
Мы используем **CI/CD через Gitea Actions**. Тебе не нужно заходить на сервер руками.
|
||||||
|
|
||||||
|
1. **Push в main**: Как только ты пушишь код, раннер на `ProdServ` просыпается.
|
||||||
|
2. **Сборка**: Docker пересобирает образ `web`, если изменились `requirements.txt` или код.
|
||||||
|
3. **Запуск**:
|
||||||
|
- `entrypoint.sh` автоматически накатывает миграции (`migrate`).
|
||||||
|
- `collectstatic` собирает все стили в общую папку для Nginx.
|
||||||
|
- Создается суперпользователь (если его еще нет) из данных `.env`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📁 Важные папки на сервере `ProdServ`
|
||||||
|
Проект живет здесь: `/home/ack/projects/mes_core/`
|
||||||
|
|
||||||
|
* **staticfiles/** — здесь лежат стили и скрипты. Если админка стала "белой", проверь права этой папки (`chmod 755`).
|
||||||
|
* **media/** — здесь будут лежать загруженные фото и файлы.
|
||||||
|
* **.env** — здесь лежат все пароли. **Никогда не удаляй его!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🆘 Что делать, если "все упало"?
|
||||||
|
Если сайт по адресу `192.168.1.108` не открывается:
|
||||||
|
1. Проверь логи контейнеров: `docker compose logs -f`.
|
||||||
|
2. Убедись, что порты в `docker-compose.yml` стоят `80:80`.
|
||||||
|
3. Перезапусти всё одной командой: `docker compose up -d --build`.
|
||||||
@@ -1,42 +1,52 @@
|
|||||||
name: prodman
|
name: prodman # Имя проекта, которое будет префиксом для всех контейнеров и сетей
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
# --- БАЗА ДАННЫХ ---
|
||||||
db:
|
db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine # Легкий образ Postgres на базе Alpine Linux
|
||||||
restart: unless-stopped
|
restart: unless-stopped # Перезапускать всегда, кроме случаев, когда ты сам его выключил
|
||||||
environment:
|
environment:
|
||||||
|
# Данные тянутся из твоего файла .env
|
||||||
- POSTGRES_DB=${DB_NAME}
|
- POSTGRES_DB=${DB_NAME}
|
||||||
- POSTGRES_USER=${DB_USER}
|
- POSTGRES_USER=${DB_USER}
|
||||||
- POSTGRES_PASSWORD=${DB_PASS}
|
- POSTGRES_PASSWORD=${DB_PASS}
|
||||||
volumes:
|
volumes:
|
||||||
|
# Храним базу в именованном томе, чтобы данные не пропали при удалении контейнера
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
# --- ПРИЛОЖЕНИЕ (DJANGO) ---
|
||||||
web:
|
web:
|
||||||
build: .
|
build: . # Собирает образ из Dockerfile в текущей папке
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env # Прокидывает все секреты и настройки внутрь Python
|
||||||
volumes:
|
volumes:
|
||||||
|
# Общие папки для статики и картинок. Сюда Django их складывает.
|
||||||
- staticfiles:/app/staticfiles
|
- staticfiles:/app/staticfiles
|
||||||
- mediafiles:/app/media
|
- mediafiles:/app/media
|
||||||
expose:
|
expose:
|
||||||
- "8000"
|
- "8000" # Открывает порт ТОЛЬКО внутри сети Docker для Nginx
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db # Сначала запустится база, потом приложение
|
||||||
|
|
||||||
|
# --- ВЕБ-СЕРВЕР (ФАСАД) ---
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.25-alpine
|
image: nginx:1.25-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
# Подключаем конфиг и статику с медиа в режиме только для чтения (т.к. nginx смотрит в интернет и может быть подвергнута атаке)
|
# Основной конфиг маршрутизации
|
||||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
# Читаем статику и медиа, которые подготовил контейнер 'web'
|
||||||
|
# :ro (read-only) — защита: даже если Nginx взломают, файлы не удалят
|
||||||
- staticfiles:/app/staticfiles:ro
|
- staticfiles:/app/staticfiles:ro
|
||||||
- mediafiles:/app/media:ro
|
- mediafiles:/app/media:ro
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80" # Единственная "дырка" в мир: порт 80 сервера -> порт 80 контейнера
|
||||||
depends_on:
|
depends_on:
|
||||||
- web
|
- web # Nginx запустится только после Django
|
||||||
|
|
||||||
|
# Описание "жестких дисков" (Volumes), которые живут дольше контейнеров
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data: # Для данных БД
|
||||||
staticfiles:
|
staticfiles: # Для CSS, JS и картинок интерфейса (collectstatic)
|
||||||
mediafiles:
|
mediafiles: # Для загруженных тобой чертежей и фото
|
||||||
@@ -1,39 +1,54 @@
|
|||||||
|
# Описываем группу серверов, куда Nginx будет перекидывать запросы.
|
||||||
|
# 'web' — это имя сервиса из нашего docker-compose.yml.
|
||||||
upstream django_app {
|
upstream django_app {
|
||||||
server web:8000;
|
server web:8000;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
# Слушаем стандартный 80-й порт (HTTP).
|
||||||
listen 80;
|
listen 80;
|
||||||
# Добавляем конкретный домен и IP сервера
|
|
||||||
|
# Список имен, на которые будет откликаться сервер.
|
||||||
|
# Если зайти по другому IP, Nginx может выдать ошибку.
|
||||||
server_name shiftflow.tertelius.space 192.168.1.108 localhost;
|
server_name shiftflow.tertelius.space 192.168.1.108 localhost;
|
||||||
|
|
||||||
# Максимальный размер загружаемого файла
|
# Увеличиваем лимит загрузки (по умолчанию в Nginx всего 1МБ).
|
||||||
|
# 100М — чтобы ты мог спокойно грузить тяжелые чертежи или фото станков.
|
||||||
client_max_body_size 100M;
|
client_max_body_size 100M;
|
||||||
|
|
||||||
# Сжатие (Gzip) — ускорит загрузку интерфейса
|
# Включаем Gzip-сжатие. Nginx будет сжимать текстовые файлы перед отправкой,
|
||||||
|
# что ускорит загрузку интерфейса, особенно на слабом интернете.
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml;
|
gzip_types text/plain text/css application/json application/javascript text/xml;
|
||||||
|
|
||||||
|
# Главный блок: всё, что не статика, летит в Django.
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://django_app;
|
proxy_pass http://django_app;
|
||||||
|
|
||||||
# Передаем оригинальный хост (важно для ALLOWED_HOSTS)
|
# Передаем оригинальный домен/IP (нужно для ALLOWED_HOSTS в Django).
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
# Передаем реальный IP пользователя (чтобы в логах видеть, кто зашел).
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
# КРИТИЧНО для CSRF защиты в Django
|
# Передаем протокол (http или https).
|
||||||
|
# КРИТИЧНО для CSRF защиты, чтобы Django не ругался при входе в админку.
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Раздача статики (CSS, JS, картинки интерфейса).
|
||||||
|
# Nginx сам лезет в папку, не беспокоя Django — это очень быстро.
|
||||||
location /static/ {
|
location /static/ {
|
||||||
|
# Путь ВНУТРИ контейнера Nginx (куда мы примонтировали волюм).
|
||||||
alias /app/staticfiles/;
|
alias /app/staticfiles/;
|
||||||
|
# Заставляем браузер кэшировать стили на 30 дней, чтобы не качать их каждый раз.
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, no-transform";
|
add_header Cache-Control "public, no-transform";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Раздача медиа-файлов (чертежи, фото продукции).
|
||||||
location /media/ {
|
location /media/ {
|
||||||
alias /app/media/;
|
alias /app/media/;
|
||||||
expires 30d;
|
expires 30d;
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-28 08:48
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shiftflow', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Company',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True, verbose_name='Название компании')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Краткое описание / Примечание')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Компания',
|
||||||
|
'verbose_name_plural': 'Компании',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Material',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True, verbose_name='Наименование')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Материал',
|
||||||
|
'verbose_name_plural': 'Материалы',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='item',
|
||||||
|
options={'ordering': ['-date', 'deal'], 'verbose_name': 'Позиция', 'verbose_name_plural': 'Сменное задание'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='item',
|
||||||
|
name='dim_value',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='item',
|
||||||
|
name='priority',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='drawing_file',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='drawings/%Y/%m/', verbose_name='Исходник (DXF/STEP)'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='extra_drawing',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='extra_drawings/%Y/%m/', verbose_name='Доп. чертеж (PDF)'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='is_bend',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Гибка'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='is_synced_1c',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Учтено в 1С'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='material_taken',
|
||||||
|
field=models.TextField(blank=True, help_text='Напр: 3 трубы по 12м', verbose_name='Взятый материал'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='operator',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Исполнитель'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='scrap_weight',
|
||||||
|
field=models.FloatField(default=0.0, verbose_name='Лом (кг)'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='size_value',
|
||||||
|
field=models.FloatField(default=0, help_text='Длина (мм) или Толщина (мм)', verbose_name='Размер детали'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='usable_waste',
|
||||||
|
field=models.TextField(blank=True, help_text='Напр: кусок 1500мм', verbose_name='Деловой отход'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='item',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(default=django.utils.timezone.now, verbose_name='Дата задания'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='item',
|
||||||
|
name='drawing_name',
|
||||||
|
field=models.CharField(blank=True, default='Б/ч', max_length=255, verbose_name='Название детали'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='item',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('work', 'В работе'), ('done', 'Выполнено'), ('partial', 'Частично'), ('leftover', 'Недодел')], default='work', max_length=10, verbose_name='Статус'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='machine',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=100, verbose_name='Название станка'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Deal',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('number', models.CharField(max_length=100, unique=True, verbose_name='№ Сделки')),
|
||||||
|
('description', models.TextField(blank=True, help_text='Общая информация по заказу', verbose_name='Описание сделки')),
|
||||||
|
('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='shiftflow.company', verbose_name='Заказчик')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Сделка',
|
||||||
|
'verbose_name_plural': 'Сделки',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='item',
|
||||||
|
name='deal',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shiftflow.deal', verbose_name='Сделка'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='item',
|
||||||
|
name='material',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='shiftflow.material', verbose_name='Материал'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,40 +1,97 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
class Company(models.Model):
|
||||||
|
"""
|
||||||
|
Справочник контрагентов/заказчиков.
|
||||||
|
Позволяет группировать сделки по компаниям и избегать дублей в названиях.
|
||||||
|
"""
|
||||||
|
name = models.CharField("Название компании", max_length=255, unique=True)
|
||||||
|
description = models.TextField("Краткое описание / Примечание", blank=True)
|
||||||
|
|
||||||
|
def __str__(self): return self.name
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Компания"; verbose_name_plural = "Компании"
|
||||||
|
|
||||||
class Machine(models.Model):
|
class Machine(models.Model):
|
||||||
name = models.CharField("Станок", max_length=100) # Лентопил, Труборез, Лазер
|
"""
|
||||||
|
Список производственных участков (станков).
|
||||||
|
Используется для фильтрации сменных заданий для конкретных операторов.
|
||||||
|
"""
|
||||||
|
name = models.CharField("Название станка", max_length=100)
|
||||||
|
|
||||||
def __str__(self): return self.name
|
def __str__(self): return self.name
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Станок"; verbose_name_plural = "Станки"
|
verbose_name = "Станок"; verbose_name_plural = "Станки"
|
||||||
|
|
||||||
|
class Deal(models.Model):
|
||||||
|
"""
|
||||||
|
Заказ или проект. Номер парсится из пути к файлам.
|
||||||
|
Служит контейнером для группы деталей (позиций).
|
||||||
|
"""
|
||||||
|
number = models.CharField("№ Сделки", max_length=100, unique=True)
|
||||||
|
company = models.ForeignKey(Company, on_delete=models.PROTECT, verbose_name="Заказчик", null=True, blank=True)
|
||||||
|
description = models.TextField("Описание сделки", blank=True, help_text="Общая информация по заказу")
|
||||||
|
|
||||||
|
def __str__(self): return f"Сделка №{self.number} ({self.company})"
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Сделка"; verbose_name_plural = "Сделки"
|
||||||
|
|
||||||
|
class Material(models.Model):
|
||||||
|
"""
|
||||||
|
Справочник ТМЦ (Трубы, листы, профили).
|
||||||
|
Необходим для точного списания остатков и синхронизации с 1С.
|
||||||
|
"""
|
||||||
|
name = models.CharField("Наименование", max_length=255, unique=True)
|
||||||
|
|
||||||
|
def __str__(self): return self.name
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Материал"; verbose_name_plural = "Материалы"
|
||||||
|
|
||||||
class Item(models.Model):
|
class Item(models.Model):
|
||||||
|
"""
|
||||||
|
Единица сменного задания. Основная рабочая сущность.
|
||||||
|
Статус по умолчанию 'work' (В работе).
|
||||||
|
"""
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('new', 'В задании'),
|
|
||||||
('work', 'В работе'),
|
('work', 'В работе'),
|
||||||
('done', 'Выполнено'),
|
('done', 'Выполнено'),
|
||||||
|
('partial', 'Частично'),
|
||||||
|
('leftover', 'Недодел'),
|
||||||
]
|
]
|
||||||
|
|
||||||
date = models.DateField("Дата", default=timezone.now)
|
# --- База (заполняет начальник) ---
|
||||||
|
date = models.DateField("Дата задания", default=timezone.now)
|
||||||
machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок")
|
machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок")
|
||||||
deal = models.CharField("№ Сделки", max_length=100) # Твои "Переходники" или заказы
|
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name="Сделка")
|
||||||
drawing_name = models.CharField("Чертеж / Деталь", max_length=255)
|
|
||||||
|
|
||||||
# Характеристики из твоих файлов
|
drawing_name = models.CharField("Название детали", max_length=255, blank=True, default="Б/ч")
|
||||||
material = models.CharField("Материал", max_length=255) # Труба 180х32, MS 12.00mm и т.д.
|
size_value = models.FloatField("Размер детали", help_text="Длина (мм) или Толщина (мм)")
|
||||||
dim_value = models.FloatField("Размер (мм)", help_text="Длина реза или толщина листа")
|
|
||||||
|
|
||||||
|
drawing_file = models.FileField("Исходник (DXF/STEP)", upload_to="drawings/%Y/%m/", blank=True, null=True)
|
||||||
|
extra_drawing = models.FileField("Доп. чертеж (PDF)", upload_to="extra_drawings/%Y/%m/", blank=True, null=True)
|
||||||
|
|
||||||
|
material = models.ForeignKey(Material, on_delete=models.PROTECT, verbose_name="Материал")
|
||||||
quantity_plan = models.PositiveIntegerField("План, шт")
|
quantity_plan = models.PositiveIntegerField("План, шт")
|
||||||
|
is_bend = models.BooleanField("Гибка", default=False)
|
||||||
|
|
||||||
|
# --- Исполнение (заполняет оператор/мастер) ---
|
||||||
|
operator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Исполнитель")
|
||||||
quantity_fact = models.PositiveIntegerField("Факт, шт", default=0)
|
quantity_fact = models.PositiveIntegerField("Факт, шт", default=0)
|
||||||
|
|
||||||
priority = models.PositiveIntegerField("Приоритет", default=10)
|
material_taken = models.TextField("Взятый материал", blank=True, help_text="Напр: 3 трубы по 12м")
|
||||||
status = models.CharField("Статус", max_length=10, choices=STATUS_CHOICES, default='new')
|
usable_waste = models.TextField("Деловой отход", blank=True, help_text="Напр: кусок 1500мм")
|
||||||
|
scrap_weight = models.FloatField("Лом (кг)", default=0.0)
|
||||||
|
|
||||||
|
# --- Статусы и учет ---
|
||||||
|
status = models.CharField("Статус", max_length=10, choices=STATUS_CHOICES, default='work')
|
||||||
|
is_synced_1c = models.BooleanField("Учтено в 1С", default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Позиция"; verbose_name_plural = "Сменное задание"
|
verbose_name = "Позиция"; verbose_name_plural = "Сменное задание"
|
||||||
ordering = ['-date', 'priority']
|
ordering = ['-date', 'deal']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.drawing_name} ({self.deal})"
|
return f"{self.drawing_name} ({self.quantity_plan} шт.)"
|
||||||
|
|
||||||
Reference in New Issue
Block a user