You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
384 lines
14 KiB
384 lines
14 KiB
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
from hitcount.models import HitCountMixin
|
|
from mptt.models import TreeForeignKey
|
|
import pydash as _; _.map = _.map_; _.filter = _.filter_
|
|
|
|
from users.models import User, Team
|
|
from specializations.models import Specialization
|
|
|
|
|
|
CURRENCIES = (
|
|
('rur', 'RUR'),
|
|
('usd', 'USD'),
|
|
('eur', 'EUR'),
|
|
)
|
|
|
|
TERM_TYPES = (
|
|
('day', 'день'),
|
|
('hour', 'час'),
|
|
('month', 'месяц'),
|
|
('project', 'проект'),
|
|
)
|
|
|
|
TERM_TYPE_MORPHS = {
|
|
'day': 'день,дня,дней',
|
|
'hour': 'час,часа,часов',
|
|
'month': 'месяц,месяца,месяцев',
|
|
'project': 'проект,проекта,проектов'
|
|
}
|
|
|
|
|
|
class BuildingClassfication(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = 'Тип здания'
|
|
verbose_name_plural = 'Типы зданий'
|
|
|
|
|
|
class ConstructionType(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = 'Вид строительства'
|
|
verbose_name_plural = 'Виды строительства'
|
|
|
|
|
|
class Realty(models.Model):
|
|
building_classification = models.ForeignKey(BuildingClassfication, related_name='realties')
|
|
construction_type = models.ForeignKey(ConstructionType, related_name='realties')
|
|
location = TreeForeignKey('common.Location', related_name='realties', null=True, blank=True)
|
|
name = models.CharField(max_length=255)
|
|
user = models.ForeignKey(User, related_name='realties') # Do we actually need this field?
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = 'Объект'
|
|
verbose_name_plural = 'Объекты'
|
|
|
|
|
|
class Project(models.Model, HitCountMixin):
|
|
WORK_TYPES = (
|
|
(1, 'Проектирование'),
|
|
(2, 'Проверка документации'),
|
|
# (3, 'Устранение замечаний в проекте'),
|
|
)
|
|
|
|
DEAL_TYPES = (
|
|
('secure_deal', 'Безопасная сделка'), # "Безопасная сделка (с резервированием бюджета)"
|
|
('direct_payment', 'Прямая оплата'), # "Прямая оплата Исполнителю на его кошелек/счет"
|
|
)
|
|
|
|
STATES = (
|
|
('active', 'Активный'),
|
|
('trashed', 'В корзине'),
|
|
('deleted', 'Удален'),
|
|
)
|
|
|
|
budget = models.DecimalField(max_digits=10, decimal_places=0)
|
|
budget_by_agreement = models.BooleanField(default=False)
|
|
created = models.DateTimeField(default=timezone.now)
|
|
cro = models.BooleanField(default=False)
|
|
currency = models.CharField(max_length=20, default='rur', choices=CURRENCIES)
|
|
customer = models.ForeignKey(User, related_name='customer_projects')
|
|
deal_type = models.CharField(max_length=20, default='secure_deal', choices=DEAL_TYPES)
|
|
name = models.CharField(max_length=255)
|
|
price_and_term_required = models.BooleanField(default=False)
|
|
realty = models.ForeignKey(Realty, blank=True, null=True, related_name='projects')
|
|
specialization = TreeForeignKey(Specialization, related_name='projects')
|
|
state = models.CharField(default='active', max_length=20, choices=STATES)
|
|
term = models.IntegerField(default=0)
|
|
term_type = models.CharField(max_length=20, choices=TERM_TYPES, default='hour')
|
|
text = models.TextField(blank=True)
|
|
work_type = models.IntegerField(default=1, choices=WORK_TYPES)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Meta:
|
|
verbose_name = 'Проект'
|
|
verbose_name_plural = 'Проекты'
|
|
ordering = ('-created',)
|
|
|
|
def secure_deal(self):
|
|
return self.deal_type == 'secure_deal'
|
|
|
|
def get_team_answers(self):
|
|
return _.filter(self.answers.all(), lambda a: isinstance(a.author, Team))
|
|
|
|
|
|
class ProjectFile(models.Model):
|
|
file = models.FileField(upload_to='projects/project_files/')
|
|
project = models.ForeignKey(Project, related_name='files', blank=True, null=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Файл проекта'
|
|
verbose_name_plural = 'Файлы проектов'
|
|
|
|
def __str__(self):
|
|
return self.file and self.file.url or self.pk
|
|
|
|
|
|
class Answer(models.Model):
|
|
budget = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True)
|
|
created = models.DateTimeField(default=timezone.now)
|
|
currency = models.CharField(max_length=5, choices=CURRENCIES, blank=True, null=True)
|
|
portfolios = models.ManyToManyField('Portfolio', related_name ='answers', blank=True)
|
|
project = models.ForeignKey(Project, related_name='answers')
|
|
secure_deal_only = models.BooleanField(default=False)
|
|
term = models.IntegerField(blank=True, null=True)
|
|
term_type = models.CharField(max_length=10, choices=TERM_TYPES, blank=True, null=True)
|
|
is_archive = models.BooleanField(default=False)
|
|
rejected = models.BooleanField(default=False)
|
|
|
|
content_type = models.ForeignKey(ContentType, limit_choices_to=Q(app_label='users', model='user') | Q(app_label='users', model='team'))
|
|
object_id = models.IntegerField()
|
|
author = GenericForeignKey('content_type', 'object_id')
|
|
|
|
def get_first_message(self):
|
|
message = self.messages.first()
|
|
|
|
if message:
|
|
return message.text
|
|
|
|
def __str__(self):
|
|
return "{author}'s answer ({id})".format(author=type(self.author).__name__, id=self.pk)
|
|
|
|
class Meta:
|
|
verbose_name = 'Отклик на проект'
|
|
verbose_name_plural = 'Отклики на проекты'
|
|
ordering = ('-created',)
|
|
|
|
|
|
class AnswerMessage(models.Model):
|
|
# Get a message's author:
|
|
#
|
|
# customer: `message.is_sender_customer and message.answer.project.customer`
|
|
# contractor/team: `not message2.is_sender_customer and message2.contractor_or_team`
|
|
|
|
answer = models.ForeignKey(Answer, related_name='messages')
|
|
created = models.DateTimeField(default=timezone.now)
|
|
is_sender_customer = models.BooleanField(default=False)
|
|
seen_by_contractor = models.BooleanField(default=False)
|
|
seen_by_customer = models.BooleanField(default=False)
|
|
text = models.TextField()
|
|
|
|
content_type = models.ForeignKey(
|
|
ContentType,
|
|
limit_choices_to=Q(app_label='users', model='user') | Q(app_label='users', model='team'),
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
object_id = models.IntegerField(null=True, blank=True)
|
|
contractor_or_team = GenericForeignKey('content_type', 'object_id')
|
|
|
|
def __str__(self):
|
|
return str(self.pk)
|
|
|
|
class Meta:
|
|
ordering = ['created']
|
|
verbose_name = 'Отклики на проекты -- переписка'
|
|
verbose_name_plural = 'Отклики на проекты -- переписки'
|
|
|
|
|
|
class AnswerFile(models.Model):
|
|
answer = models.ForeignKey(Answer, related_name='files', blank=True, null=True)
|
|
name = models.CharField(max_length=255)
|
|
file = models.FileField(upload_to='projects/answer_files/')
|
|
|
|
class Meta:
|
|
verbose_name = 'Файл для отклика'
|
|
verbose_name_plural = 'Файлы для откликов'
|
|
|
|
def __str__(self):
|
|
return self.file and self.file.url or self.pk
|
|
|
|
|
|
class Order(models.Model):
|
|
STATUSES = (
|
|
('created', 'Создан'),
|
|
('process', 'В процессе'),
|
|
('completed', 'Завершен'),
|
|
)
|
|
|
|
contractor = models.ForeignKey(User, null=True, blank=True, related_name='orders') # Related name should've been "contractor_orders"
|
|
created = models.DateTimeField(default=timezone.now)
|
|
project = models.OneToOneField(Project, related_name='order')
|
|
secure = models.BooleanField(default=False)
|
|
status = models.CharField(max_length=30, choices=STATUSES, default='created')
|
|
team = models.ForeignKey(Team, null=True, blank=True, related_name='orders')
|
|
|
|
def __str__(self):
|
|
return self.project.name
|
|
|
|
class Meta:
|
|
verbose_name = 'Заказ'
|
|
verbose_name_plural = 'Заказы'
|
|
|
|
def get_contractor_owner(self):
|
|
if self.contractor:
|
|
return self.contractor.pk
|
|
elif self.team:
|
|
return self.team.owner.pk
|
|
else:
|
|
return None
|
|
|
|
class Arbitration(models.Model):
|
|
user = models.ForeignKey(User)
|
|
text = models.TextField()
|
|
order = models.ForeignKey(Order)
|
|
created = models.DateTimeField(default=timezone.now)
|
|
|
|
def __str__(self):
|
|
return self.user.username
|
|
|
|
class Meta:
|
|
verbose_name = 'Арбитраж'
|
|
verbose_name_plural = 'Арбитраж'
|
|
|
|
unique_together = (
|
|
('user', 'order'),
|
|
)
|
|
|
|
|
|
STATUSES = (
|
|
('not_agreed', 'Не согласован'),
|
|
('send_approve', 'На согласовании'),
|
|
('cancel_approve', 'Исполнитель отказался'),
|
|
('in_process', 'В процессе'),
|
|
('completed', 'Завершен'),
|
|
)
|
|
|
|
from .validators import validate_term
|
|
|
|
class Stage(models.Model):
|
|
cost = models.DecimalField(max_digits=10, decimal_places=0)
|
|
cost_type = models.CharField(max_length=5, choices=CURRENCIES, default='rur')
|
|
name = models.CharField(max_length=255)
|
|
order = models.ForeignKey(Order, related_name='stages')
|
|
result = models.CharField(max_length=255)
|
|
term = models.DateField()
|
|
term_type = models.CharField(max_length=10, choices=TERM_TYPES, default='hour')
|
|
status = models.CharField(choices=STATUSES, max_length=30, default='not_agreed')
|
|
created = models.DateTimeField(default=timezone.now)
|
|
approve_time = models.DateTimeField(null=True, blank=True)
|
|
pos = models.IntegerField(default=0, null=True, blank=True)
|
|
is_paid = models.BooleanField(default=False)
|
|
close_contractor = models.BooleanField(default=False)
|
|
close_customer = models.BooleanField(default=False)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def clean(self, *args, **kwargs):
|
|
pass
|
|
# stage_last = self.__class__.objects.filter(order=self.order).order_by('-pos')[:1]
|
|
# if stage_last:
|
|
# stage_last = stage_last[0]
|
|
# if stage_last.term > self.term:
|
|
# raise ValidationError({'term':'Дата не должна быть меньше даты предыдущео этапа'})
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.full_clean()
|
|
super().save(*args, **kwargs)
|
|
|
|
class Meta:
|
|
ordering = ['pos']
|
|
verbose_name = 'Этап'
|
|
verbose_name_plural = 'Этапы'
|
|
|
|
|
|
class Candidate(models.Model):
|
|
answer = models.ForeignKey(Answer, related_name='candidates') # TODO: Swap to "OneToOneField"
|
|
project = models.ForeignKey(Project, related_name='candidates') # TODO: Remove this redundant field at all (we've got "candidate.answer.project")
|
|
status = models.BooleanField(default=False)
|
|
position = models.PositiveIntegerField(default=0)
|
|
|
|
def __str__(self):
|
|
return str(self.answer.pk)
|
|
|
|
class Meta:
|
|
ordering = ('position',)
|
|
verbose_name = 'Кандидат'
|
|
verbose_name_plural = 'Кандидаты'
|
|
|
|
|
|
class Portfolio(models.Model):
|
|
budget = models.DecimalField(max_digits=10, decimal_places=0, default=0, null=True, blank=True)
|
|
building_classification = models.ForeignKey(BuildingClassfication, related_name='portfolios', null=True, blank=True)
|
|
construction_type = models.ForeignKey(ConstructionType, related_name='portfolios', null=True, blank=True)
|
|
created = models.DateTimeField(auto_now_add=True, auto_created=True)
|
|
currency = models.CharField(max_length=20, default='rur', choices=CURRENCIES, null=True, blank=True)
|
|
description = models.TextField(blank=True)
|
|
location = TreeForeignKey('common.Location', related_name='portfolios', null=True, blank=True)
|
|
name = models.CharField(max_length=255)
|
|
specialization = TreeForeignKey(Specialization, related_name='portfolios', null=True, blank=True)
|
|
term = models.IntegerField(default=0, null=True, blank=True)
|
|
term_type = models.CharField(max_length=20, choices=TERM_TYPES, default='hour', null=True, blank=True)
|
|
user = models.ForeignKey(User, related_name='portfolios', null=True, blank=True)
|
|
worksell = models.BooleanField(default=False)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def get_prev(self):
|
|
try:
|
|
return self.get_previous_by_created(user=self.user)
|
|
except self.DoesNotExist:
|
|
return None
|
|
|
|
def get_next(self):
|
|
try:
|
|
return self.get_next_by_created(user=self.user)
|
|
except self.DoesNotExist:
|
|
return None
|
|
|
|
class Meta:
|
|
ordering = ['-created']
|
|
verbose_name = 'Портфолио'
|
|
verbose_name_plural = 'Портфолио'
|
|
|
|
def get_cover(self):
|
|
photo = self.photos.first()
|
|
return photo and photo.img # WTF? We could at leat return a URL, not an object
|
|
|
|
|
|
class PortfolioPhoto(models.Model):
|
|
img = models.ImageField(upload_to='projects/portfolio/')
|
|
portfolio = models.ForeignKey(Portfolio, related_name='photos')
|
|
|
|
class Meta:
|
|
verbose_name = 'Фото портфолио'
|
|
verbose_name_plural = 'Фото портфолио'
|
|
|
|
def __str__(self):
|
|
return self.img and self.img.url or str(self.img)
|
|
|
|
|
|
class ProjectWorkTypeSuggestion(models.Model):
|
|
name = models.CharField(max_length=255, verbose_name='Название')
|
|
commentary = models.TextField(blank=True, verbose_name='Комментарий')
|
|
processed = models.BooleanField(default=False, verbose_name='Рассмотрено?')
|
|
username = models.CharField(max_length=255, blank=True, verbose_name='Имя пользователя')
|
|
email = models.EmailField(verbose_name='Email')
|
|
|
|
class Meta:
|
|
verbose_name = 'Предложение типов работ (проектов)'
|
|
verbose_name_plural = 'Предложения типов работ (проектов)'
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|