Skip to content

Commit 888d4fe

Browse files
committed
Созданы "Компании", "Ресурсы", "КомпанииПроекта"
1 parent 7e61a1f commit 888d4fe

7 files changed

Lines changed: 756 additions & 6 deletions

File tree

projects/admin.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
from projects.models import (
44
Achievement,
55
Collaborator,
6+
Company,
67
DefaultProjectAvatar,
78
DefaultProjectCover,
89
Project,
10+
ProjectCompany,
911
ProjectGoal,
1012
ProjectLink,
1113
ProjectNews,
14+
Resource,
1215
)
1316

1417

@@ -20,6 +23,31 @@ class ProjectGoalInline(admin.TabularInline):
2023
autocomplete_fields = ("responsible",)
2124

2225

26+
class ProjectCompanyInline(admin.TabularInline):
27+
model = ProjectCompany
28+
extra = 1
29+
autocomplete_fields = ("company", "decision_maker")
30+
fields = ("company", "contribution", "decision_maker")
31+
verbose_name = "Партнёр проекта"
32+
verbose_name_plural = "Партнёры проекта"
33+
34+
35+
class ResourceInline(admin.StackedInline):
36+
model = Resource
37+
extra = 0
38+
fields = ("type", "description", "partner_company")
39+
show_change_link = True
40+
verbose_name = "Ресурс"
41+
verbose_name_plural = "Ресурсы"
42+
43+
def get_formset(self, request, obj=None, **kwargs):
44+
formset = super().get_formset(request, obj, **kwargs)
45+
if obj is not None:
46+
qs = obj.companies.all()
47+
formset.form.base_fields["partner_company"].queryset = qs
48+
return formset
49+
50+
2351
@admin.register(Project)
2452
class ProjectAdmin(admin.ModelAdmin):
2553
list_display = (
@@ -85,7 +113,28 @@ class ProjectAdmin(admin.ModelAdmin):
85113
),
86114
)
87115
readonly_fields = ("datetime_created", "datetime_updated")
88-
inlines = [ProjectGoalInline]
116+
inlines = [ProjectGoalInline, ProjectCompanyInline, ResourceInline]
117+
118+
119+
@admin.register(Company)
120+
class CompanyAdmin(admin.ModelAdmin):
121+
list_display = ("id", "name", "inn")
122+
list_display_links = ("id", "name")
123+
search_fields = ("name", "inn")
124+
list_filter = ()
125+
ordering = ("name",)
126+
readonly_fields = ()
127+
fieldsets = (
128+
(
129+
"Компания",
130+
{
131+
"fields": (
132+
"name",
133+
"inn",
134+
)
135+
},
136+
),
137+
)
89138

90139

91140
@admin.register(ProjectGoal)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Generated by Django 4.2.24 on 2025-10-06 09:13
2+
3+
from django.conf import settings
4+
import django.core.validators
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
("projects", "0029_projectgoal"),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name="Company",
19+
fields=[
20+
(
21+
"id",
22+
models.BigAutoField(
23+
auto_created=True,
24+
primary_key=True,
25+
serialize=False,
26+
verbose_name="ID",
27+
),
28+
),
29+
("name", models.CharField(max_length=255)),
30+
(
31+
"inn",
32+
models.CharField(
33+
max_length=12,
34+
unique=True,
35+
validators=[
36+
django.core.validators.RegexValidator(
37+
message="ИНН должен содержать 10 или 12 цифр.",
38+
regex="^\\d{10}(\\d{2})?$",
39+
)
40+
],
41+
),
42+
),
43+
],
44+
options={
45+
"verbose_name": "Компания",
46+
"verbose_name_plural": "Компании",
47+
"ordering": ["name"],
48+
},
49+
),
50+
migrations.CreateModel(
51+
name="Resource",
52+
fields=[
53+
(
54+
"id",
55+
models.BigAutoField(
56+
auto_created=True,
57+
primary_key=True,
58+
serialize=False,
59+
verbose_name="ID",
60+
),
61+
),
62+
(
63+
"type",
64+
models.CharField(
65+
choices=[
66+
("infrastructure", "Инфраструктурный"),
67+
("staff", "Кадровый"),
68+
("financial", "Финансовый"),
69+
("information", "Информационный"),
70+
],
71+
max_length=32,
72+
),
73+
),
74+
("description", models.TextField()),
75+
(
76+
"partner_company",
77+
models.ForeignKey(
78+
blank=True,
79+
help_text="Если не указано — ресурс в поиске партнёра.",
80+
null=True,
81+
on_delete=django.db.models.deletion.PROTECT,
82+
related_name="resources",
83+
to="projects.company",
84+
),
85+
),
86+
(
87+
"project",
88+
models.ForeignKey(
89+
on_delete=django.db.models.deletion.CASCADE,
90+
related_name="resources",
91+
to="projects.project",
92+
),
93+
),
94+
],
95+
options={
96+
"verbose_name": "Ресурс",
97+
"verbose_name_plural": "Ресурсы",
98+
"ordering": ["project", "type", "id"],
99+
},
100+
),
101+
migrations.CreateModel(
102+
name="ProjectCompany",
103+
fields=[
104+
(
105+
"id",
106+
models.BigAutoField(
107+
auto_created=True,
108+
primary_key=True,
109+
serialize=False,
110+
verbose_name="ID",
111+
),
112+
),
113+
("contribution", models.TextField(blank=True)),
114+
(
115+
"company",
116+
models.ForeignKey(
117+
on_delete=django.db.models.deletion.CASCADE,
118+
related_name="project_links",
119+
to="projects.company",
120+
),
121+
),
122+
(
123+
"decision_maker",
124+
models.ForeignKey(
125+
blank=True,
126+
null=True,
127+
on_delete=django.db.models.deletion.SET_NULL,
128+
related_name="partner_decisions",
129+
to=settings.AUTH_USER_MODEL,
130+
),
131+
),
132+
(
133+
"project",
134+
models.ForeignKey(
135+
on_delete=django.db.models.deletion.CASCADE,
136+
related_name="project_companies",
137+
to="projects.project",
138+
),
139+
),
140+
],
141+
options={
142+
"verbose_name": "Связь проекта и компании",
143+
"verbose_name_plural": "Связи проекта и компании",
144+
},
145+
),
146+
migrations.AddField(
147+
model_name="project",
148+
name="companies",
149+
field=models.ManyToManyField(
150+
related_name="projects",
151+
through="projects.ProjectCompany",
152+
to="projects.company",
153+
),
154+
),
155+
migrations.AddConstraint(
156+
model_name="projectcompany",
157+
constraint=models.UniqueConstraint(
158+
fields=("project", "company"), name="uq_project_company_unique_pair"
159+
),
160+
),
161+
]

projects/models.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from files.models import UserFile
1717
from industries.models import Industry
1818
from projects.managers import AchievementManager, CollaboratorManager, ProjectManager
19+
from projects.validators import inn_validator
1920
from users.models import CustomUser
2021

2122
User = get_user_model()
@@ -170,6 +171,12 @@ class Project(models.Model):
170171
User, verbose_name="Подписчики", related_name="subscribed_projects"
171172
)
172173

174+
companies = models.ManyToManyField(
175+
"Company",
176+
through="ProjectCompany",
177+
related_name="projects",
178+
)
179+
173180
datetime_created = models.DateTimeField(
174181
verbose_name="Дата создания", null=False, auto_now_add=True
175182
)
@@ -426,3 +433,114 @@ def __str__(self) -> str:
426433
class Meta:
427434
verbose_name = "Цель"
428435
verbose_name_plural = "Цели"
436+
437+
438+
class Company(models.Model):
439+
name = models.CharField(max_length=255)
440+
inn = models.CharField(max_length=12, unique=True, validators=[inn_validator])
441+
442+
class Meta:
443+
verbose_name = "Компания"
444+
verbose_name_plural = "Компании"
445+
ordering = ["name"]
446+
447+
def __str__(self):
448+
return f"{self.name} ({self.inn})"
449+
450+
451+
class ProjectCompany(models.Model):
452+
project = models.ForeignKey(
453+
"projects.Project",
454+
on_delete=models.CASCADE,
455+
related_name="project_companies",
456+
)
457+
company = models.ForeignKey(
458+
Company,
459+
on_delete=models.CASCADE,
460+
related_name="project_links",
461+
)
462+
contribution = models.TextField(blank=True)
463+
decision_maker = models.ForeignKey(
464+
User,
465+
on_delete=models.SET_NULL,
466+
null=True,
467+
blank=True,
468+
related_name="partner_decisions",
469+
)
470+
471+
class Meta:
472+
verbose_name = "Связь проекта и компании"
473+
verbose_name_plural = "Связи проекта и компании"
474+
constraints = [
475+
models.UniqueConstraint(
476+
fields=["project", "company"],
477+
name="uq_project_company_unique_pair",
478+
),
479+
]
480+
481+
def __str__(self):
482+
return f"{self.project} - {self.company}"
483+
484+
485+
class Resource(models.Model):
486+
class ResourceType(models.TextChoices):
487+
INFRASTRUCTURE = "infrastructure", "Инфраструктурный"
488+
STAFF = "staff", "Кадровый"
489+
FINANCIAL = "financial", "Финансовый"
490+
INFORMATION = "information", "Информационный"
491+
492+
project = models.ForeignKey(
493+
"projects.Project",
494+
on_delete=models.CASCADE,
495+
related_name="resources",
496+
)
497+
type = models.CharField(
498+
max_length=32,
499+
choices=ResourceType.choices,
500+
)
501+
description = models.TextField()
502+
503+
partner_company = models.ForeignKey(
504+
Company,
505+
on_delete=models.PROTECT,
506+
null=True,
507+
blank=True,
508+
related_name="resources",
509+
help_text="Если не указано — ресурс в поиске партнёра.",
510+
)
511+
512+
class Meta:
513+
verbose_name = "Ресурс"
514+
verbose_name_plural = "Ресурсы"
515+
ordering = ["project", "type", "id"]
516+
517+
def __str__(self):
518+
base = f"{self.get_type_display()} ресурс для {self.project}"
519+
return f"{base}{self.partner_display}"
520+
521+
@property
522+
def partner_display(self):
523+
return (
524+
self.partner_company.name
525+
if self.partner_company
526+
else "в поиске партнёра для данного ресурса"
527+
)
528+
529+
def clean(self):
530+
"""
531+
Проверяет, что выбранная partner_company действительно является партнёром проекта.
532+
"""
533+
super().clean()
534+
if self.partner_company:
535+
exists = ProjectCompany.objects.filter(
536+
project=self.project, company=self.partner_company
537+
).exists()
538+
if not exists:
539+
raise ValidationError(
540+
{
541+
"partner_company": (
542+
"Эта компания не является партнёром данного проекта. "
543+
"Сначала добавьте её в партнёры проекта."
544+
)
545+
}
546+
)

0 commit comments

Comments
 (0)