Files
smartsoltech_site/smartsoltech/web/models.py
Andrey K. Choi ec01a2ae10 👥 Добавлено управление персоналом и карьерой
 Новые функции:
- 🧑‍💻 Team модель для управления сотрудниками
  • Полная информация о персонале (имя, должность, отдел)
  • Фотографии и контактные данные
  • Социальные сети (LinkedIn, GitHub, Telegram)
  • Навыки и опыт работы
  • Гибкие настройки отображения

- 💼 Career модель для вакансий
  • Детальное описание позиций
  • Требования и обязанности
  • Зарплатные вилки
  • Типы занятости и уровни опыта
  • Статусы вакансий и дедлайны

🔧 Админ-панель:
- Удобные интерфейсы для HR-менеджмента
- Группировка полей и фильтрация
- Быстрые действия для массовых операций
- Сортировка по приоритету

📊 База данных:
- Миграция 0013_career_team.py
- Оптимизированные индексы и связи
2025-11-25 15:44:57 +09:00

386 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.db import models
from django.contrib.auth.models import AbstractUser, User
import uuid
from django.urls import reverse
class ContactInfo(models.Model):
"""Модель для контактной информации компании"""
company_name = models.CharField(max_length=200, default="SmartSolTech", verbose_name="Название компании")
email = models.EmailField(default="info@smartsoltech.kr", verbose_name="Email")
phone = models.CharField(max_length=20, default="+82-10-5693-6103", verbose_name="Телефон")
telegram = models.CharField(max_length=100, default="@smartsoltech", verbose_name="Telegram")
address = models.TextField(default="Чолланамдо, Кванджу", verbose_name="Адрес")
working_hours = models.CharField(max_length=100, default="Пн-Пт 9:00-18:00", verbose_name="Часы работы")
description = models.TextField(default="Мы - команда профессионалов в сфере IT-решений", verbose_name="Описание")
call_to_action = models.CharField(max_length=200, default="Начнем сотрудничество?", verbose_name="Призыв к действию")
subtitle = models.CharField(max_length=200, default="Свяжитесь с нами для обсуждения вашего проекта", verbose_name="Подзаголовок")
is_active = models.BooleanField(default=True, verbose_name="Активно")
class Meta:
verbose_name = 'Контактная информация'
verbose_name_plural = 'Контактная информация'
def __str__(self):
return f"Контакты - {self.company_name}"
@classmethod
def get_active(cls):
"""Получить активную контактную информацию"""
return cls.objects.filter(is_active=True).first() or cls.objects.create()
class HeroBanner(models.Model):
"""Модель для главного баннера на сайте"""
title = models.CharField(max_length=200, verbose_name="Заголовок")
subtitle = models.TextField(blank=True, verbose_name="Подзаголовок")
description = models.TextField(blank=True, verbose_name="Описание")
image = models.ImageField(upload_to='static/img/hero/', blank=True, null=True, verbose_name="Фоновое изображение")
video = models.FileField(upload_to='static/video/hero/', blank=True, null=True,
help_text='Фоновое видео для баннера (MP4, WebM)', verbose_name="Фоновое видео")
video_poster = models.ImageField(upload_to='static/img/hero/posters/', blank=True, null=True,
help_text='Превью изображение для видео', verbose_name="Превью видео")
button_text = models.CharField(max_length=100, blank=True, verbose_name="Текст кнопки")
button_link = models.URLField(blank=True, verbose_name="Ссылка кнопки")
is_active = models.BooleanField(default=True, verbose_name="Активен")
order = models.PositiveIntegerField(default=0, verbose_name="Порядок отображения")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = 'Hero Баннер'
verbose_name_plural = 'Hero Баннеры'
ordering = ['order', '-created_at']
def __str__(self):
return self.title
class Category(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(default='Описание категории')
class Meta:
verbose_name = 'Категория'
verbose_name_plural = 'Категории'
ordering = ['name']
def __str__(self):
return self.name
class Service(models.Model):
name = models.CharField(max_length=200)
description = models.TextField(default='Описание услуги')
price = models.DecimalField(max_digits=10, decimal_places=2)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='services')
image = models.ImageField(upload_to='static/img/services/', blank=True, null=True)
video = models.FileField(upload_to='static/video/services/', blank=True, null=True, help_text='Видео файл для услуги (MP4, WebM, AVI)')
video_poster = models.ImageField(upload_to='static/img/services/posters/', blank=True, null=True, help_text='Превью изображение для видео')
video = models.FileField(upload_to='static/video/services/', blank=True, null=True, help_text='Видео файл для услуги (MP4, WebM, AVI)')
video_poster = models.ImageField(upload_to='static/img/services/posters/', blank=True, null=True, help_text='Превью изображение для видео')
class Meta:
verbose_name = 'Услуга'
verbose_name_plural = 'Услуги'
ordering = ['name']
def __str__(self):
return self.name
def average_rating(self):
reviews = self.reviews.all()
if reviews:
return sum(review.rating for review in reviews) / reviews.count()
return 0
def review_count(self):
return self.reviews.count()
class Client(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='client_profile', null=True, blank=True)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
phone_number = models.CharField(max_length=15, unique=True)
image = models.ImageField(upload_to='static/img/customer/', blank=True, null=True)
chat_id = models.CharField(max_length=100, blank=True, null=True) # Telegram chat ID
class Meta:
verbose_name = 'Клиент'
verbose_name_plural = 'Клиенты'
ordering = ['last_name', 'first_name']
def __str__(self):
return f"{self.first_name} {self.last_name} {self.chat_id}"
class BlogPost(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
published_date = models.DateTimeField(auto_now_add=True)
image = models.ImageField(upload_to='static/img/blog/', blank=True, null=True)
video = models.FileField(upload_to='static/video/blog/', blank=True, null=True, help_text='Видео файл для блог поста')
video_poster = models.ImageField(upload_to='static/img/blog/posters/', blank=True, null=True, help_text='Превью изображение для видео')
class Meta:
verbose_name = 'Блог'
verbose_name_plural = 'Блоги'
ordering = ['-published_date']
def __str__(self):
return self.title
class ServiceRequest(models.Model):
service = models.ForeignKey(Service, on_delete=models.CASCADE)
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='related_service_requests', null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
token = models.UUIDField(default=uuid.uuid4, unique=True) # Генерация уникального токена
chat_id = models.CharField(max_length=100, blank=True, null=True) # Telegram chat ID
is_verified = models.BooleanField(default=False)
class Meta:
verbose_name = 'Заявка на услугу'
verbose_name_plural = 'Заявки на услуги'
ordering = ['-is_verified', '-created_at']
def __str__(self):
return f"Request for {self.service.name} by {self.client.first_name}"
class Order(models.Model):
service_request = models.OneToOneField(ServiceRequest, on_delete=models.CASCADE, related_name='related_order')
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='related_orders')
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='related_orders')
message = models.TextField(blank=True, null=True)
order_date = models.DateTimeField(auto_now_add=True)
status = models.CharField(
max_length=50,
choices=[
('pending', 'Ожидание'),
('in_progress', 'В процессе'),
('completed', 'Завершен'),
('cancelled', 'Отменён')
],
default='pending'
)
class Meta:
ordering = ['-order_date']
verbose_name = 'Заказ'
verbose_name_plural = 'Заказы'
def __str__(self):
return f"Order #{self.id} by {self.client.first_name}"
def is_completed(self):
return self.status == 'completed'
def get_absolute_url(self):
return reverse('order_detail', kwargs={'pk': self.pk})
class Project(models.Model):
name = models.CharField(max_length=200)
description = models.TextField(default='Описание проекта')
completion_date = models.DateField(blank=True, null=True)
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='projects')
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='projects')
order = models.OneToOneField(Order, on_delete=models.CASCADE, related_name='project', null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
image = models.ImageField(upload_to='static/img/project/', blank=True, null=True)
video = models.FileField(upload_to='static/video/project/', blank=True, null=True, help_text='Видео презентация проекта')
video_poster = models.ImageField(upload_to='static/img/project/posters/', blank=True, null=True, help_text='Превью изображение для видео проекта')
status = models.CharField(max_length=50, choices=[('in_progress', 'В процессе'), ('completed', 'Завершен')], default='in_progress')
class Meta:
verbose_name = 'Проект'
verbose_name_plural = 'Проекты'
ordering = ['-completion_date']
def __str__(self):
return self.name
class Review(models.Model):
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='reviews')
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='reviews')
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='reviews', blank=True, null=True)
rating = models.IntegerField()
comment = models.TextField()
review_date = models.DateTimeField(auto_now_add=True)
image = models.ImageField(upload_to='static/img/review/', blank=True, null=True)
video = models.FileField(upload_to='static/video/review/', blank=True, null=True, help_text='Видео отзыв о работе')
video_poster = models.ImageField(upload_to='static/img/review/posters/', blank=True, null=True, help_text='Превью для видео отзыва')
class Meta:
verbose_name = 'Отзыв'
verbose_name_plural = 'Отзывы'
ordering = ['-review_date']
def __str__(self):
return f"Отзыв от {self.client.first_name} {self.client.last_name} for {self.service.name}"
class Team(models.Model):
"""Модель для управления персоналом компании"""
first_name = models.CharField(max_length=100, verbose_name="Имя")
last_name = models.CharField(max_length=100, verbose_name="Фамилия")
position = models.CharField(max_length=200, verbose_name="Должность")
department = models.CharField(max_length=100, verbose_name="Отдел", blank=True)
bio = models.TextField(verbose_name="Биография/Описание", blank=True)
photo = models.ImageField(upload_to='static/img/team/', blank=True, null=True, verbose_name="Фотография")
email = models.EmailField(blank=True, verbose_name="Email")
phone = models.CharField(max_length=20, blank=True, verbose_name="Телефон")
# Социальные сети
linkedin = models.URLField(blank=True, verbose_name="LinkedIn")
github = models.URLField(blank=True, verbose_name="GitHub")
telegram = models.CharField(max_length=100, blank=True, verbose_name="Telegram")
# Навыки и технологии
skills = models.TextField(blank=True, verbose_name="Навыки", help_text="Разделите навыки запятыми")
experience_years = models.PositiveIntegerField(default=0, verbose_name="Лет опыта")
# Настройки отображения
is_active = models.BooleanField(default=True, verbose_name="Активен")
display_order = models.PositiveIntegerField(default=0, verbose_name="Порядок отображения")
show_on_about = models.BooleanField(default=True, verbose_name="Показывать на странице О нас")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Сотрудник'
verbose_name_plural = 'Команда'
ordering = ['display_order', 'last_name', 'first_name']
def __str__(self):
return f"{self.first_name} {self.last_name} - {self.position}"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@property
def skills_list(self):
"""Возвращает список навыков"""
if self.skills:
return [skill.strip() for skill in self.skills.split(',') if skill.strip()]
return []
class Career(models.Model):
"""Модель для управления вакансиями и карьерными возможностями"""
EMPLOYMENT_TYPE_CHOICES = [
('full_time', 'Полная занятость'),
('part_time', 'Частичная занятость'),
('contract', 'Контракт'),
('internship', 'Стажировка'),
('remote', 'Удаленная работа'),
('freelance', 'Фриланс'),
]
EXPERIENCE_LEVEL_CHOICES = [
('junior', 'Junior (0-1 год)'),
('middle', 'Middle (2-4 года)'),
('senior', 'Senior (5+ лет)'),
('lead', 'Team Lead'),
('intern', 'Стажер'),
]
STATUS_CHOICES = [
('active', 'Активная'),
('paused', 'Приостановлена'),
('closed', 'Закрыта'),
('draft', 'Черновик'),
]
title = models.CharField(max_length=200, verbose_name="Название вакансии")
department = models.CharField(max_length=100, verbose_name="Отдел")
location = models.CharField(max_length=200, default="Кванджу, Южная Корея", verbose_name="Местоположение")
employment_type = models.CharField(
max_length=20,
choices=EMPLOYMENT_TYPE_CHOICES,
default='full_time',
verbose_name="Тип занятости"
)
experience_level = models.CharField(
max_length=20,
choices=EXPERIENCE_LEVEL_CHOICES,
default='middle',
verbose_name="Уровень опыта"
)
# Описание вакансии
description = models.TextField(verbose_name="Описание вакансии")
responsibilities = models.TextField(verbose_name="Обязанности")
requirements = models.TextField(verbose_name="Требования")
benefits = models.TextField(blank=True, verbose_name="Преимущества и условия")
# Зарплатная вилка
salary_min = models.PositiveIntegerField(blank=True, null=True, verbose_name="Зарплата от (₩)")
salary_max = models.PositiveIntegerField(blank=True, null=True, verbose_name="Зарплата до (₩)")
salary_currency = models.CharField(max_length=10, default="KRW", verbose_name="Валюта")
# Необходимые навыки
required_skills = models.TextField(verbose_name="Обязательные навыки", help_text="Разделите навыки запятыми")
preferred_skills = models.TextField(blank=True, verbose_name="Желательные навыки", help_text="Разделите навыки запятыми")
# Статус и метаданные
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='active',
verbose_name="Статус"
)
is_featured = models.BooleanField(default=False, verbose_name="Рекомендуемая вакансия")
application_deadline = models.DateField(blank=True, null=True, verbose_name="Дедлайн подачи заявок")
# Контактная информация
contact_email = models.EmailField(default="hr@smartsoltech.kr", verbose_name="Email для связи")
contact_person = models.CharField(max_length=200, blank=True, verbose_name="Контактное лицо")
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(blank=True, null=True, verbose_name="Дата публикации")
class Meta:
verbose_name = 'Вакансия'
verbose_name_plural = 'Карьера'
ordering = ['-is_featured', '-published_at', '-created_at']
def __str__(self):
return f"{self.title} ({self.get_experience_level_display()})"
@property
def required_skills_list(self):
"""Возвращает список обязательных навыков"""
if self.required_skills:
return [skill.strip() for skill in self.required_skills.split(',') if skill.strip()]
return []
@property
def preferred_skills_list(self):
"""Возвращает список желательных навыков"""
if self.preferred_skills:
return [skill.strip() for skill in self.preferred_skills.split(',') if skill.strip()]
return []
@property
def salary_range(self):
"""Возвращает строку с зарплатной вилкой"""
if self.salary_min and self.salary_max:
return f"{self.salary_min:,} - ₩{self.salary_max:,}"
elif self.salary_min:
return f"от ₩{self.salary_min:,}"
elif self.salary_max:
return f"до ₩{self.salary_max:,}"
return "По договоренности"
def is_active_position(self):
"""Проверяет, активна ли вакансия"""
from django.utils import timezone
if self.status != 'active':
return False
if self.application_deadline and self.application_deadline < timezone.now().date():
return False
return True