initial commit

This commit is contained in:
2025-08-05 10:26:39 +05:00
commit b7412bbef6
298 changed files with 10533 additions and 0 deletions

View File

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,24 @@
from django.contrib import admin
from unfold.admin import ModelAdmin # type: ignore
from core.apps.contracts.models import ContractAttachedFileModel
@admin.register(ContractAttachedFileModel)
class FileAdmin(ModelAdmin):
list_display = (
"name",
"contract",
"allowed_types",
"created_at",
)
search_fields = (
"name",
"contract",
"created_at",
"updated_at",
)
list_display_links = (
"name",
)

View File

@@ -0,0 +1,26 @@
from django.contrib import admin
from unfold.admin import ModelAdmin # type: ignore
from core.apps.contracts.models import ContractModel
@admin.register(ContractModel)
class ContractAdmin(ModelAdmin):
list_display = (
"name",
"identifier",
"file_permissions",
"created_at",
)
search_fields = (
"name",
"identifier",
"owners",
"created_at",
"updated_at",
)
list_display_links = (
"name",
)

View File

@@ -0,0 +1,26 @@
from django.contrib import admin
from unfold.admin import ModelAdmin # type: ignore
from core.apps.contracts.models import ContractFileContentModel
@admin.register(ContractFileContentModel)
class FilecontentAdmin(ModelAdmin):
list_display = (
"owner_name",
"contract_owner",
"file",
"created_at",
"updated_at",
)
search_fields = (
"contract_owner"
"file",
"content",
"created_at",
"updated_at",
"document_url",
)
list_display_links = (
"owner_name",
)

View File

@@ -0,0 +1,78 @@
from django.contrib import admin
from unfold.admin import ModelAdmin # type: ignore
from core.apps.contracts.models import (
ContractOwnerModel,
LegalEntityModel,
IndividualModel,
)
@admin.register(IndividualModel)
class IndividualAdmin(ModelAdmin):
list_display = (
"full_name",
"individual_code",
"phone",
"use_face_id",
"created_at",
)
search_fields = (
"full_name",
"iin_code",
"person_code",
"phone",
"use_face_id",
"created_at",
"updated_at",
)
list_display_links = (
"full_name",
)
@admin.register(LegalEntityModel)
class LegalentityAdmin(ModelAdmin):
list_display = (
"name",
"role",
"entity_code",
"phone",
"created_at",
)
search_fields = (
"name",
"role",
"bin_code",
"identifier",
"phone",
"created_at",
"updated_at",
)
list_display_links = (
"name",
)
@admin.register(ContractOwnerModel)
class OwnerAdmin(ModelAdmin):
list_display = (
"owner_identity",
"owner_name",
"status",
"created_at"
)
search_fields = (
"owner_name",
"status",
"created_at",
"updated_at"
)
list_display_links = (
"owner_identity",
)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ModuleConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core.apps.contracts"

View File

View File

@@ -0,0 +1,12 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class ContractOwnerStatus(models.TextChoices):
"""
Owner status choices.
"""
ACCEPTED = "accepted", _("Accepted")
REJECTED = "rejected", _("Rejected")
PENDING = "pending", _("Pending")

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.contracts.models import ContractattachedfileModel
class ContractattachedfileFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = ContractattachedfileModel
fields = [
"name",
]

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.contracts.models import ContractModel
class ContractFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = ContractModel
fields = [
"name",
]

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.contracts.models import ContractfilecontentModel
class ContractfilecontentFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = ContractfilecontentModel
fields = [
"name",
]

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.contracts.models import ContractownerModel
class ContractownerFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = ContractownerModel
fields = [
"name",
]

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.contracts.models import ContractattachedfileModel
class ContractattachedfileForm(forms.ModelForm):
class Meta:
model = ContractattachedfileModel
fields = "__all__"

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.contracts.models import ContractModel
class ContractForm(forms.ModelForm):
class Meta:
model = ContractModel
fields = "__all__"

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.contracts.models import ContractfilecontentModel
class ContractfilecontentForm(forms.ModelForm):
class Meta:
model = ContractfilecontentModel
fields = "__all__"

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.contracts.models import ContractownerModel
class ContractownerForm(forms.ModelForm):
class Meta:
model = ContractownerModel
fields = "__all__"

View File

@@ -0,0 +1,347 @@
# Generated by Django 5.2.4 on 2025-08-01 09:53
import core.apps.contracts.validators.attached_files
import core.apps.contracts.validators.owners
import django.core.validators
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="LegalEntityModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"name",
models.CharField(
max_length=255,
validators=[core.apps.contracts.validators.owners.name_validator],
verbose_name="Name",
),
),
("role", models.CharField(verbose_name="Role")),
(
"bin_code",
models.CharField(
blank=True,
max_length=14,
null=True,
validators=[core.apps.contracts.validators.owners.bin_code_validator],
verbose_name="BIN code",
),
),
("identifier", models.CharField(blank=True, max_length=255, null=True, verbose_name="Identifier")),
(
"phone",
models.CharField(
max_length=25,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid international phone number (E.164 format, e.g., +14155552671).",
regex="^\\+?[1-9]\\d{1,14}$",
)
],
verbose_name="Phone",
),
),
],
options={
"verbose_name": "Legal Entity",
"verbose_name_plural": "Legal Entities",
"db_table": "legal_entities",
},
),
migrations.CreateModel(
name="ContractModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"name",
models.CharField(
max_length=255,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid contract name. Use letters, numbers, spaces, and limited punctuation (e.g., &, -, (), ., ', \"). Length must be 3 to 100 characters.",
regex="^[A-Za-z0-9À-ÿ&()\\-.,'\\\" ]{3,100}$",
)
],
verbose_name="name",
),
),
("identifier", models.CharField(verbose_name="Identifier")),
("allow_add_files", models.BooleanField(default=False)),
("allow_delete_files", models.BooleanField(default=False)),
],
options={
"verbose_name": "Contract",
"verbose_name_plural": "Contracts",
"db_table": "contracts",
"indexes": [models.Index(fields=["name"], name="contracts_name_inx")],
},
),
migrations.CreateModel(
name="ContractAttachedFileModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"name",
models.CharField(
max_length=150,
validators=[
django.core.validators.MinLengthValidator(
3, message="File name must be at least 3 characters long."
),
django.core.validators.MaxLengthValidator(
150, message="File name must be at most 150 characters long."
),
core.apps.contracts.validators.attached_files.starts_with_letter_validator,
django.core.validators.RegexValidator(
code="invalid_characters",
message="File name can only contain letters, digits, spaces, dots, underscores, or hyphens.",
regex="^[A-Za-z0-9\\s._-]+$",
),
],
verbose_name="name",
),
),
("allow_pdf", models.BooleanField(default=True)),
("allow_word", models.BooleanField(default=True)),
("allow_image", models.BooleanField(default=True)),
(
"contract",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="attached_files",
to="contracts.contractmodel",
verbose_name="Contract",
),
),
],
options={
"verbose_name": "Contract Attached File",
"verbose_name_plural": "Contract Attached Files",
"db_table": "contract_attached_files",
"unique_together": {("name", "contract")},
},
),
migrations.CreateModel(
name="ContractOwnerModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"status",
models.CharField(
blank=True,
choices=[("accepted", "Accepted"), ("rejected", "Rejected"), ("pending", "Pending")],
default="pending",
max_length=255,
verbose_name="Owner Status",
),
),
(
"contract",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="owners",
to="contracts.contractmodel",
verbose_name="Contract",
),
),
],
options={
"verbose_name": "Contract Owner",
"verbose_name_plural": "Contract Owners",
"db_table": "contract_owners",
},
),
migrations.CreateModel(
name="ContractFileContentModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
("document", models.FileField(upload_to="", verbose_name="Document")),
(
"file",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="contents",
to="contracts.contractattachedfilemodel",
verbose_name="File",
),
),
(
"contract_owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contracts.contractownermodel",
verbose_name="Contract Owner",
),
),
],
options={
"verbose_name": "Contract File Content",
"verbose_name_plural": "Contract File Contents",
"db_table": "contract_file_contents",
},
),
migrations.CreateModel(
name="IndividualModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"full_name",
models.CharField(
max_length=512,
validators=[
django.core.validators.RegexValidator(
message="Invalid Full Name, Please enter Full Name in the format: <first name> <last name> <father name>",
regex="^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$",
)
],
verbose_name="name",
),
),
(
"iin_code",
models.CharField(
blank=True,
max_length=14,
null=True,
unique=True,
validators=[
django.core.validators.RegexValidator(
code="invalid_iin",
message="IIN code must consist of exactly 14 digits.",
regex="^\\d{14}$",
)
],
verbose_name="IIN code",
),
),
(
"person_code",
models.CharField(
blank=True,
max_length=64,
null=True,
unique=True,
validators=[
django.core.validators.RegexValidator(
code="invalid_person_code",
message="Person Code must be 3 to 64 characters long and contain only letters, digits, dashes, or underscores.",
regex="^[A-Za-z0-9_-]{3,64}$",
)
],
verbose_name="Person Code (if no IIN code)",
),
),
(
"phone",
models.CharField(
validators=[
django.core.validators.RegexValidator(
message="Enter a valid international phone number (E.164 format, e.g., +14155552671).",
regex="^\\+?[1-9]\\d{1,14}$",
)
],
verbose_name="Phone",
),
),
("use_face_id", models.BooleanField(default=False, verbose_name="Use FaceID")),
],
options={
"verbose_name": "Individual",
"verbose_name_plural": "Individuals",
"db_table": "individuals",
"indexes": [
models.Index(fields=["full_name"], name="individuals_fullname_inx"),
models.Index(fields=["iin_code"], name="individuals_iin_inx"),
models.Index(fields=["person_code"], name="individuals_code_inx"),
models.Index(fields=["phone"], name="individuals_phone_inx"),
],
},
),
migrations.AddField(
model_name="contractownermodel",
name="individual",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="owner",
to="contracts.individualmodel",
verbose_name="Individual",
),
),
migrations.AddField(
model_name="contractownermodel",
name="legal_entity",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="owner",
to="contracts.legalentitymodel",
verbose_name="Legal Entity",
),
),
migrations.AddConstraint(
model_name="contractownermodel",
constraint=models.UniqueConstraint(fields=("individual", "contract"), name="unique_individual_contract"),
),
migrations.AddConstraint(
model_name="contractownermodel",
constraint=models.UniqueConstraint(
fields=("legal_entity", "contract"), name="unique_legal_entity_contract"
),
),
]

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,110 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from functools import cache
from django.core.validators import (
MinLengthValidator,
MaxLengthValidator,
)
from core.utils.base_model import UUIDPrimaryKeyBaseModel
from .contracts import ContractModel
from core.apps.contracts.validators.attached_files import (
ContractAttachedFileValidator,
allowed_chars_validator,
starts_with_letter_validator,
)
ALLOWED_FILE_EXTENTIONS: dict[str, list[str]] = {
'pdf': ['.pdf'],
'word': ['.doc', '.docx'],
'image': ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'],
}
class ContractAttachedFileModel(UUIDPrimaryKeyBaseModel):
name = models.CharField(
_("name"),
max_length=150,
validators=[
MinLengthValidator(
3,
message=_(
"File name must be at " \
"least 3 characters long."
)
),
MaxLengthValidator(
150,
message=_(
"File name must be at " \
"most 150 characters long."
)
),
starts_with_letter_validator,
allowed_chars_validator,
],
)
contract = models.ForeignKey(
ContractModel,
on_delete=models.CASCADE,
verbose_name=_("Contract"),
related_name="attached_files",
)
allow_pdf = models.BooleanField(default=True)
allow_word = models.BooleanField(default=True)
allow_image = models.BooleanField(default=True)
@property
def allowed_types(self) -> str:
allowed_types: list[str] = []
if self.allow_pdf:
allowed_types.append("pdf")
if self.allow_word:
allowed_types.append("word")
if self.allow_image:
allowed_types.append("image")
if len(allowed_types) == 3:
return "All"
return ", ".join(allowed_type.upper() for allowed_type in allowed_types)
@property
@cache
def allowed_extensions(self) -> list[str]:
extensions: list[str] = []
if self.allow_pdf:
extensions += ALLOWED_FILE_EXTENTIONS['pdf']
if self.allow_word:
extensions += ALLOWED_FILE_EXTENTIONS['word']
if self.allow_image:
extensions += ALLOWED_FILE_EXTENTIONS['image']
return extensions
def __str__(self):
return self.name
@classmethod
def _create_fake(cls):
return cls.objects.create(
name="mock",
)
def clean(self) -> None:
super().clean()
validator = ContractAttachedFileValidator()
validator()
class Meta: # type: ignore
db_table = "contract_attached_files"
verbose_name = _("Contract Attached File")
verbose_name_plural = _("Contract Attached Files")
unique_together = ("name", "contract")

View File

@@ -0,0 +1,70 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from core.utils.base_model import UUIDPrimaryKeyBaseModel
from core.apps.contracts.validators.contracts import (
ContractValidator,
name_validator
)
class ContractModel(UUIDPrimaryKeyBaseModel):
name = models.CharField(
_("name"),
validators=[
name_validator,
],
max_length=255
)
identifier = models.CharField(
_("Identifier"),
null=False,
blank=False
)
allow_add_files = models.BooleanField(default=False)
allow_delete_files = models.BooleanField(default=False)
@property
def file_permissions(self) -> str:
permissions: list[str] = []
if self.allow_add_files:
permissions.append("add")
if self.allow_delete_files:
permissions.append("delete")
if len(permissions) == 2:
return "All"
return ", ".join(permission.capitalize() for permission in permissions)
def __str__(self):
return self.name
@classmethod
def _create_fake(cls):
return cls.objects.create(
name="mock",
)
def clean(self) -> None:
super().clean()
validator = ContractValidator(self)
validator()
class Meta: # type: ignore
db_table = "contracts"
verbose_name = _("Contract")
verbose_name_plural = _("Contracts")
indexes = [
models.Index(
fields=["name"],
name="contracts_name_inx"
)
]

View File

@@ -0,0 +1,70 @@
import os
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.db.models.fields.files import FieldFile
from django.core.exceptions import ValidationError
from core.utils.base_model import UUIDPrimaryKeyBaseModel
from core.apps.contracts.models.attached_files import ContractAttachedFileModel
from core.apps.contracts.models.owners import ContractOwnerModel
class ContractFileContentModel(UUIDPrimaryKeyBaseModel):
file = models.ForeignKey(
ContractAttachedFileModel,
on_delete=models.CASCADE,
verbose_name=_("File"),
related_name="contents",
null=False,
blank=False,
)
contract_owner = models.ForeignKey(
ContractOwnerModel,
on_delete=models.CASCADE,
verbose_name=_("Contract Owner"),
null=False,
blank=False,
)
document = models.FileField(
_("Document"),
null=False,
blank=False,
)
@property
def owner_name(self) -> str:
return self.contract_owner.owner_name
def __str__(self):
return self.file.name
def validate_file(self, document: FieldFile):
try:
ext = os.path.splitext(document.name)[1].lower()
except IndexError:
raise ValidationError(f"Unsupported document name: {document.name}")
if ext.lower() not in self.file.allowed_extensions:
raise ValidationError(f"Unsupported document type: {ext.upper()}")
return document
@classmethod
def _create_fake(cls):
return cls.objects.create(
file=ContractAttachedFileModel._create_fake(), # type: ignore
owner=ContractOwnerModel._create_fake(), # type: ignore
document_url=(
"https://img.freepik.com/free-photo/closeup-scarlet-macaw-from-side"
"-view-scarlet-macaw-closeup-head_488145-3540.jpg?semt=ais_hybrid&w=740"
)
)
class Meta: # type: ignore
db_table = "contract_file_contents"
verbose_name = _("Contract File Content")
verbose_name_plural = _("Contract File Contents")

View File

@@ -0,0 +1,279 @@
from typing import Self
from django.db import models
from django.utils.translation import gettext_lazy as _
from core.apps.contracts.models.contracts import ContractModel
from core.apps.contracts.choices.contracts import ContractOwnerStatus
from core.utils.base_model import UUIDPrimaryKeyBaseModel
from core.apps.contracts.validators.owners import (
ContractOwnerValidator,
LegalEntityValidator,
IndividualValidator,
name_validator,
bin_code_validator,
phone_validator,
person_code_validator,
full_name_validator,
iin_code_validator,
)
class LegalEntityModel(UUIDPrimaryKeyBaseModel):
name = models.CharField(
_("Name"),
validators=[
name_validator,
],
max_length=255,
null=False,
blank=False,
)
role = models.CharField(
_("Role"),
null=False,
blank=False
)
bin_code = models.CharField(
_("BIN code"),
validators=[
bin_code_validator,
],
max_length=14,
null=True,
blank=True,
)
identifier = models.CharField(
_("Identifier"),
max_length=255,
null=True,
blank=True
)
phone = models.CharField(
_("Phone"),
validators=[
phone_validator
],
max_length=25,
null=False,
blank=False
)
def __str__(self):
return self.name
@classmethod
def _create_fake(cls):
return cls.objects.create(
name="mock",
)
@property
def entity_code(self) -> str:
if self.bin_code is not None:
return f"BIN code: {self.bin_code}"
else:
return f"Identifier: {self.identifier}"
def clean(self) -> None:
super().clean()
validator = LegalEntityValidator(self)
validator()
class Meta: # type: ignore
db_table = "legal_entities"
verbose_name = _("Legal Entity")
verbose_name_plural = _("Legal Entities")
class IndividualModel(UUIDPrimaryKeyBaseModel):
full_name = models.CharField(
_("name"),
validators=[
full_name_validator,
],
max_length=512,
null=False,
blank=False
)
iin_code = models.CharField(
_("IIN code"),
max_length=14,
validators=[
iin_code_validator,
],
null=True,
blank=True,
unique=True
)
person_code = models.CharField(
_("Person Code (if no IIN code)"),
validators=[
person_code_validator,
],
max_length=64,
null=True,
blank=True,
unique=True
)
phone = models.CharField(
_("Phone"),
validators=[
phone_validator,
],
null=False,
blank=False
)
use_face_id = models.BooleanField(
_("Use FaceID"),
null=False,
blank=False,
default=False
)
def __str__(self):
return self.full_name
@classmethod
def _create_fake(cls):
return cls.objects.create(
name="mock",
)
@property
def individual_code(self) -> str:
if self.iin_code is not None:
return f"IIN Code: {self.iin_code}"
else:
return f"Person Code: {self.person_code}"
def clean(self):
super().clean()
validator = IndividualValidator(self)
validator()
class Meta: # type: ignore
db_table = "individuals"
verbose_name = _("Individual")
verbose_name_plural = _("Individuals")
indexes = [
models.Index(
fields=["full_name"],
name="individuals_fullname_inx"
),
models.Index(
fields=["iin_code"],
name="individuals_iin_inx"
),
models.Index(
fields=["person_code"],
name="individuals_code_inx"
),
models.Index(
fields=["phone"],
name="individuals_phone_inx"
),
]
class ContractOwnerModel(UUIDPrimaryKeyBaseModel):
legal_entity = models.OneToOneField(
LegalEntityModel,
related_name="owner",
verbose_name=_("Legal Entity"),
on_delete=models.PROTECT,
null=True,
blank=True
)
individual = models.OneToOneField(
IndividualModel,
related_name="owner",
verbose_name=_("Individual"),
on_delete=models.PROTECT,
null=True,
blank=True,
)
status = models.CharField(
_("Owner Status"),
max_length=255,
choices=ContractOwnerStatus,
default=ContractOwnerStatus.PENDING,
null=False,
blank=True,
)
contract = models.ForeignKey(
ContractModel,
verbose_name=_("Contract"),
related_name="owners",
on_delete=models.PROTECT,
null=False,
blank=False
)
def clean(self) -> None:
super().clean()
validator = ContractOwnerValidator(self)
validator()
def __str__(self) -> str:
return str(self.legal_entity or self.individual)
@property
def owner_name(self) -> str:
if self.legal_entity is not None:
return str(self.legal_entity)
else:
return str(self.individual)
@property
def owner_identity(self) -> str:
if self.legal_entity is not None:
return _("Legal Entity")
else:
return _("Individual")
@classmethod
def _create_fake(cls, mock_individual: bool = True) -> Self:
kwargs: dict[str, IndividualModel | LegalEntityModel] = dict()
if mock_individual:
kwargs["individual"] = IndividualModel._create_fake() # type: ignore
else:
kwargs["legal_entity"] = LegalEntityModel._create_fake() # type: ignore
return cls.objects.create(
**kwargs
)
class Meta: # type: ignore
db_table = "contract_owners"
verbose_name = _("Contract Owner")
verbose_name_plural = _("Contract Owners")
constraints = [
models.UniqueConstraint(
fields=["individual", "contract"],
name="unique_individual_contract"
),
models.UniqueConstraint(
fields=["legal_entity", "contract"],
name="unique_legal_entity_contract"
),
]

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class ContractattachedfilePermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class ContractPermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class ContractfilecontentPermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class ContractownerPermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1 @@
from .attached_files import * # noqa

View File

@@ -0,0 +1,47 @@
from rest_framework import serializers # type: ignore
from core.apps.contracts.models import ContractAttachedFileModel
from core.apps.contracts.serializers.file_contents import (
RetrieveContractFileContentSerializer
)
class BaseContractAttachedFileSerializer(serializers.ModelSerializer):
class Meta:
model = ContractAttachedFileModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at",
)
class ListContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
class Meta(BaseContractAttachedFileSerializer.Meta):
fields = (
"id",
"name",
"created_at",
"updated_at",
)
class RetrieveContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
contents = RetrieveContractFileContentSerializer(many=True, read_only=True)
class Meta(BaseContractAttachedFileSerializer.Meta):
fields = "__all__"
class CreateContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
class Meta(BaseContractAttachedFileSerializer.Meta): ...
class UpdateContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
class Meta(BaseContractAttachedFileSerializer.Meta): ...
class DestroyContractAttachedFileSerializer(BaseContractAttachedFileSerializer):
class Meta(BaseContractAttachedFileSerializer.Meta):
fields = ["id"]

View File

@@ -0,0 +1 @@
from .contracts import * # noqa

View File

@@ -0,0 +1,42 @@
from rest_framework import serializers
from core.apps.contracts.models import ContractModel
class BaseContractSerializer(serializers.ModelSerializer):
class Meta:
model = ContractModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at"
)
class ListContractSerializer(BaseContractSerializer):
class Meta(BaseContractSerializer.Meta):
fields = (
"id",
"name",
"identifier",
"created_at",
"updated_at",
)
class RetrieveContractSerializer(BaseContractSerializer):
class Meta(BaseContractSerializer.Meta): ...
class CreateContractSerializer(BaseContractSerializer):
class Meta(BaseContractSerializer.Meta): ...
class UpdateContractSerializer(BaseContractSerializer):
class Meta(BaseContractSerializer.Meta): ...
class DestroyContractSerializer(BaseContractSerializer):
class Meta(BaseContractSerializer.Meta):
fields = ["id"]

View File

@@ -0,0 +1 @@
from .file_contents import * # noqa

View File

@@ -0,0 +1,51 @@
from rest_framework import serializers # type: ignore
from core.apps.contracts.models import ContractFileContentModel
class BaseContractFileContentSerializer(serializers.ModelSerializer):
document = serializers.FileField(required=True)
class Meta:
model = ContractFileContentModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at",
)
class ListContractFileContentSerializer(BaseContractFileContentSerializer):
class Meta(BaseContractFileContentSerializer.Meta): ...
class RetrieveContractFileContentSerializer(BaseContractFileContentSerializer):
class Meta(BaseContractFileContentSerializer.Meta): ...
class CreateContractFileContentSerializer(BaseContractFileContentSerializer):
class Meta(BaseContractFileContentSerializer.Meta): ...
class UpdateContractFileContentSerializer(BaseContractFileContentSerializer):
class Meta(BaseContractFileContentSerializer.Meta): ...
class DestroyContractFileContentSerializer(BaseContractFileContentSerializer):
class Meta(BaseContractFileContentSerializer.Meta):
fields = ["id"]
class CreateContractFileContentFromOwnerSerializer(BaseContractFileContentSerializer):
class Meta(BaseContractFileContentSerializer.Meta):
fields = "__all__"
read_only_fields = (
*BaseContractFileContentSerializer.Meta.read_only_fields,
"file",
"contract_owner",
)
def create(self, validated_data): # type: ignore
validated_data["contract_owner_id"] = self.context["owner_id"]
validated_data["file_id"] = self.context["file_id"]
return super().create(validated_data) # type: ignore

View File

@@ -0,0 +1 @@
from .owner import * # noqa

View File

@@ -0,0 +1,174 @@
from rest_framework import serializers
from core.apps.contracts.models import (
ContractOwnerModel,
LegalEntityModel,
IndividualModel,
)
from django.db import transaction
# from core.apps.contracts.serializers.attached_files import (
# BaseContractAttachedFileSerializer,
# )
# from core.apps.contracts.serializers.file_contents import (
# BaseContractFileContentSerializer
# )
#! TODO fix: BaseContractOwnerSerializer (.create/.update) fix
class BaseIndividualSerializer(serializers.ModelSerializer):
class Meta:
model = IndividualModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at",
)
extra_kwargs = {
"iin_code": {
"required": False
},
"person_code": {
"required": False
}
}
class ListIndividualSerializer(BaseIndividualSerializer):
class Meta(BaseIndividualSerializer.Meta):
fields = (
"id",
"full_name",
"phone",
"created_at",
"updated_at",
)
class BaseLegalEntitySerializer(serializers.ModelSerializer):
class Meta:
model = LegalEntityModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at",
)
extra_kwargs = {
"bin_code": {
"required": False
},
"identifier": {
"required": False
}
}
class ListLegalEntitySerializer(BaseLegalEntitySerializer):
class Meta(BaseLegalEntitySerializer.Meta):
fields = (
"id",
"name",
"phone",
"created_at",
"updated_at",
)
class BaseContractOwnerSerializer(serializers.ModelSerializer):
legal_entity = BaseLegalEntitySerializer(required=False)
individual = BaseIndividualSerializer(required=False)
class Meta:
model = ContractOwnerModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at",
)
extra_kwargs = {
"legal_entity": {
"required": False
},
"individual": {
"required": False
}
}
def create(self, validated_data):
legal_entity_data = validated_data.pop("legal_entity", None)
individual_data = validated_data.pop("individual", None)
with transaction.atomic():
if legal_entity_data is not None:
legal_entity_serializer = BaseLegalEntitySerializer(data=legal_entity_data)
legal_entity_serializer.is_valid(raise_exception=True)
validated_data["legal_entity"] = legal_entity_serializer.save()
if individual_data is not None:
individual_serializer = BaseIndividualSerializer(data=individual_data)
individual_serializer.is_valid(raise_exception=True)
validated_data["individual"] = individual_serializer.save()
contract_owner = ContractOwnerModel.objects.create(**validated_data)
return contract_owner
def update(self, instance, validated_data):
legal_entity_data = validated_data.pop("legal_entity", None)
individual_data = validated_data.pop("individual", None)
with transaction.atomic():
if legal_entity_data is not None:
if instance.legal_entity:
for attr, value in legal_entity_data.items():
setattr(instance.legal_entity, attr, value)
instance.legal_entity.save()
else:
legal_entity = LegalEntityModel.objects.create(**legal_entity_data)
instance.legal_entity = legal_entity
if individual_data is not None:
if instance.individual:
for attr, value in individual_data.items():
setattr(instance.individual, attr, value)
instance.individual.save()
else:
individual = IndividualModel.objects.create(**individual_data)
instance.individual = individual
# Update ContractOwnerModel fields
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
class ListContractOwnerSerializer(BaseContractOwnerSerializer):
legal_entity = ListLegalEntitySerializer(read_only=True)
individual = ListIndividualSerializer(read_only=True)
class Meta(BaseContractOwnerSerializer.Meta): ...
class RetrieveContractOwnerSerializer(BaseContractOwnerSerializer):
class Meta(BaseContractOwnerSerializer.Meta): ...
class CreateContractOwnerSerializer(BaseContractOwnerSerializer):
class Meta(BaseContractOwnerSerializer.Meta): ...
class UpdateContractOwnerSerializer(BaseContractOwnerSerializer):
class Meta(BaseContractOwnerSerializer.Meta): ...
class DestroyContractOwnerSerializer(BaseContractOwnerSerializer):
class Meta(BaseContractOwnerSerializer.Meta):
fields = ["id"]

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.contracts.models import ContractattachedfileModel
@receiver(post_save, sender=ContractattachedfileModel)
def ContractattachedfileSignal(sender, instance, created, **kwargs): ...

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.contracts.models import ContractModel
@receiver(post_save, sender=ContractModel)
def ContractSignal(sender, instance, created, **kwargs): ...

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.contracts.models import ContractfilecontentModel
@receiver(post_save, sender=ContractfilecontentModel)
def ContractfilecontentSignal(sender, instance, created, **kwargs): ...

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.contracts.models import ContractownerModel
@receiver(post_save, sender=ContractownerModel)
def ContractownerSignal(sender, instance, created, **kwargs): ...

View File

@@ -0,0 +1,4 @@
from .test_attached_files import * # noqa
from .test_contracts import * # noqa
from .test_file_contents import * # noqa
from .test_owners import * # noqa

View File

@@ -0,0 +1,47 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.contracts.models import ContractattachedfileModel
class ContractattachedfileTest(TestCase):
def _create_data(self):
return ContractattachedfileModel._create_fake()
def setUp(self):
self.client = APIClient()
self.instance = self._create_data()
self.urls = {
"list": reverse("ContractAttachedFile-list"),
"retrieve": reverse("ContractAttachedFile-detail", kwargs={"pk": self.instance.pk}),
"retrieve-not-found": reverse("ContractAttachedFile-detail", kwargs={"pk": 1000}),
}
def test_create(self):
self.assertTrue(True)
def test_update(self):
self.assertTrue(True)
def test_partial_update(self):
self.assertTrue(True)
def test_destroy(self):
self.assertTrue(True)
def test_list(self):
response = self.client.get(self.urls["list"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve(self):
response = self.client.get(self.urls["retrieve"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve_not_found(self):
response = self.client.get(self.urls["retrieve-not-found"])
self.assertFalse(response.json()["status"])
self.assertEqual(response.status_code, 404)

View File

@@ -0,0 +1,47 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.contracts.models import ContractModel
class ContractTest(TestCase):
def _create_data(self):
return ContractModel._create_fake()
def setUp(self):
self.client = APIClient()
self.instance = self._create_data()
self.urls = {
"list": reverse("Contract-list"),
"retrieve": reverse("Contract-detail", kwargs={"pk": self.instance.pk}),
"retrieve-not-found": reverse("Contract-detail", kwargs={"pk": 1000}),
}
def test_create(self):
self.assertTrue(True)
def test_update(self):
self.assertTrue(True)
def test_partial_update(self):
self.assertTrue(True)
def test_destroy(self):
self.assertTrue(True)
def test_list(self):
response = self.client.get(self.urls["list"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve(self):
response = self.client.get(self.urls["retrieve"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve_not_found(self):
response = self.client.get(self.urls["retrieve-not-found"])
self.assertFalse(response.json()["status"])
self.assertEqual(response.status_code, 404)

View File

@@ -0,0 +1,47 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.contracts.models import ContractfilecontentModel
class ContractfilecontentTest(TestCase):
def _create_data(self):
return ContractfilecontentModel._create_fake()
def setUp(self):
self.client = APIClient()
self.instance = self._create_data()
self.urls = {
"list": reverse("ContractFileContent-list"),
"retrieve": reverse("ContractFileContent-detail", kwargs={"pk": self.instance.pk}),
"retrieve-not-found": reverse("ContractFileContent-detail", kwargs={"pk": 1000}),
}
def test_create(self):
self.assertTrue(True)
def test_update(self):
self.assertTrue(True)
def test_partial_update(self):
self.assertTrue(True)
def test_destroy(self):
self.assertTrue(True)
def test_list(self):
response = self.client.get(self.urls["list"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve(self):
response = self.client.get(self.urls["retrieve"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve_not_found(self):
response = self.client.get(self.urls["retrieve-not-found"])
self.assertFalse(response.json()["status"])
self.assertEqual(response.status_code, 404)

View File

@@ -0,0 +1,47 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.contracts.models import ContractownerModel
class ContractownerTest(TestCase):
def _create_data(self):
return ContractownerModel._create_fake()
def setUp(self):
self.client = APIClient()
self.instance = self._create_data()
self.urls = {
"list": reverse("ContractOwner-list"),
"retrieve": reverse("ContractOwner-detail", kwargs={"pk": self.instance.pk}),
"retrieve-not-found": reverse("ContractOwner-detail", kwargs={"pk": 1000}),
}
def test_create(self):
self.assertTrue(True)
def test_update(self):
self.assertTrue(True)
def test_partial_update(self):
self.assertTrue(True)
def test_destroy(self):
self.assertTrue(True)
def test_list(self):
response = self.client.get(self.urls["list"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve(self):
response = self.client.get(self.urls["retrieve"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve_not_found(self):
response = self.client.get(self.urls["retrieve-not-found"])
self.assertFalse(response.json()["status"])
self.assertEqual(response.status_code, 404)

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,8 @@
from modeltranslation.translator import TranslationOptions, register
from core.apps.contracts.models import ContractattachedfileModel
@register(ContractattachedfileModel)
class ContractattachedfileTranslation(TranslationOptions):
fields = []

View File

@@ -0,0 +1,8 @@
from modeltranslation.translator import TranslationOptions, register
from core.apps.contracts.models import ContractModel
@register(ContractModel)
class ContractTranslation(TranslationOptions):
fields = []

View File

@@ -0,0 +1,8 @@
from modeltranslation.translator import TranslationOptions, register
from core.apps.contracts.models import ContractfilecontentModel
@register(ContractfilecontentModel)
class ContractfilecontentTranslation(TranslationOptions):
fields = []

View File

@@ -0,0 +1,8 @@
from modeltranslation.translator import TranslationOptions, register
from core.apps.contracts.models import ContractownerModel
@register(ContractownerModel)
class ContractownerTranslation(TranslationOptions):
fields = []

View File

@@ -0,0 +1,37 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter # type: ignore
from . import views
router = DefaultRouter()
router.register(r"contract-attached-files", views.ContractAttachedFileView, "contract-attached-files") # type: ignore
router.register(r"contracts", views.ContractView, "contracts") # type: ignore
router.register(r"contract-file-contents", views.ContractFileContentView, "contract-file-contents") # type: ignore
router.register(r"contract-owners", views.ContractOwnerView, "contract-owners") # type: ignore
router.register(r"contract-owners", views.ContractOwnerFileViewSet, "contract-owner-files") # type: ignore
urlpatterns = [ # type: ignore
path("", include(router.urls)), # type: ignore
path(
r"contract-owners/<uuid:owner_id>/contract",
views.ContractDetailView.as_view(),
name="contract-detail"
),
path(
"contract-owners/<uuid:owner_id>/files/<uuid:file_id>",
views.ContractAttachedFileDeleteView.as_view(),
name="contract-file-delete"
),
path(
r"contract-owners/<uuid:owner_id>/files/<uuid:file_id>/upload",
views.UploadFileContentView.as_view(),
name="upload-file-content"
),
path(
r"folders/<uuid:pk>/contracts",
views.ListFolderContractsView.as_view(),
name="list-folder-contracts"
)
]

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,34 @@
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from django_core.models.base import AbstractBaseModel # type: ignore
def starts_with_letter_validator(value: str) -> None:
"""
Checks if the value starts with a letter.
"""
if value[0].isalpha():
return
raise ValidationError(
_("File name must start with a letter."),
code="invalid_start",
)
allowed_chars_validator = RegexValidator(
regex=r"^[A-Za-z0-9\s._-]+$",
message=_(
"File name can only contain letters, " \
"digits, spaces, dots, underscores, or hyphens."
),
code="invalid_characters",
)
class ContractAttachedFileValidator:
def __init__(self): ...
def __call__(self):
return True

View File

@@ -0,0 +1,23 @@
# from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from django_core.models.base import AbstractBaseModel # type: ignore
name_validator = RegexValidator(
regex=r"^[A-Za-z0-9À-ÿ&()\-.,'\" ]{3,100}$",
message=_(
"Enter a valid contract name. "
"Use letters, numbers, spaces, and "
"limited punctuation (e.g., &, -, (), ., ', \"). "
"Length must be 3 to 100 characters."
)
)
class ContractValidator:
def __init__(self, instance: AbstractBaseModel):
self.instance = instance
def __call__(self):
return True

View File

@@ -0,0 +1,8 @@
# from django.core.exceptions import ValidationError
class ContractfilecontentValidator:
def __init__(self): ...
def __call__(self):
return True

View File

@@ -0,0 +1,146 @@
import re
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django_core.models.base import AbstractBaseModel # type: ignore
from django.core.validators import RegexValidator
full_name_validator = RegexValidator(
regex=r"^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$",
message=_(
"Invalid Full Name, Please enter Full Name in the "
"format: <first name> <last name> <father name>"
)
)
iin_code_validator = RegexValidator(
regex=r'^\d{14}$',
message=_("IIN code must consist of exactly 14 digits."),
code='invalid_iin'
)
person_code_validator = RegexValidator(
regex=r'^[A-Za-z0-9_-]{3,64}$',
message=_(
"Person Code must be 3 to 64 characters long and contain"
" only letters, digits, dashes, or underscores."
),
code='invalid_person_code'
)
phone_validator = RegexValidator(
regex=r'^\+?[1-9]\d{1,14}$',
message=_(
"Enter a valid international phone number "
"(E.164 format, e.g., +14155552671)."
)
)
def bin_code_validator(value: str):
if not value.isdigit():
raise ValidationError(_("BIN code must contain only digits."))
if len(value) != 14:
raise ValidationError(_("BIN code must be exactly 14 digits long."))
if not re.match(r"^\d{14}$", value):
raise ValidationError(_("Invalid BIN code format."))
# Optional: check if first 6 digits represent a valid date (YYMMDD)
try:
from datetime import datetime
datetime.strptime(value[:6], "%y%m%d")
except ValueError:
raise ValidationError(_("BIN code starts with an invalid date."))
def name_validator(value: str):
if len(value.strip()) < 3:
raise ValidationError(_("Name is too short."))
if re.fullmatch(r"(.)\1{2,}", value): # e.g., "aaa", "!!!"
raise ValidationError(_("Name cannot contain excessive repeated characters."))
if not re.match(r"^[A-Za-z0-9&\-\.\,\(\)\s]+$", value):
raise ValidationError(_("Name contains invalid characters. Only letters, numbers, spaces, and symbols like . , & - ( ) are allowed."))
if value.lower() in {"test", "company", "name", "example", "sample"}:
raise ValidationError(_("This name is too generic. Please choose a more specific name."))
if value.isupper() or value.islower():
raise ValidationError(_("Name must have both uppercase and lowercase letters."))
if not value[0].isupper():
raise ValidationError(_("Name should start with a capital letter."))
if len(set(value.lower())) < 4:
raise ValidationError(_("Name is too repetitive or lacks meaningful structure."))
class IndividualValidator:
def __init__(self, instance: AbstractBaseModel):
self.instance = instance
def __call__(self):
if (
self.instance.iin_code is None # type: ignore
and self.instance.person_code is None # type: ignore
):
raise ValidationError(_(
"Either IIN code or Person Code must be provided."
))
if (
self.instance.iin_code is not None # type: ignore
and self.instance.person_code is not None # type: ignore
):
raise ValidationError(_(
"One of IIN code or Person code must be empty"
))
class LegalEntityValidator:
def __init__(self, instance: AbstractBaseModel):
self.instance = instance
def __call__(self):
if (
self.instance.identifier is None # type: ignore
and self.instance.bin_code is None # type: ignore
):
raise ValidationError(_(
"Either Identifier or BIN code "
"must contain a value"
))
if (
self.instance.identifier is not None # type: ignore
and self.instance.bin_code is not None # type: ignore
):
raise ValidationError(_(
"One of Indentifier or BIN code "
"must be empty"
))
class ContractOwnerValidator:
def __init__(self, instance: AbstractBaseModel):
self.instance = instance
def __call__(self):
if (
self.instance.legal_entity is None # type: ignore
and self.instance.individual is None # type: ignore
):
raise ValidationError("Either entity or individual should be not None")
if (
self.instance.legal_entity is not None # type: ignore
and self.instance.individual is not None # type: ignore
):
raise ValidationError("One of Individual or Legal entity should be given")

View File

@@ -0,0 +1,4 @@
from .attached_files import * # noqa
from .contracts import * # noqa
from .file_contents import * # noqa
from .owners import * # noqa

View File

@@ -0,0 +1,35 @@
from django_core.mixins import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import AllowAny, IsAdminUser
from rest_framework.viewsets import ModelViewSet
from core.apps.contracts.models import ContractAttachedFileModel
from core.apps.contracts.serializers.attached_files import (
CreateContractAttachedFileSerializer,
ListContractAttachedFileSerializer,
RetrieveContractAttachedFileSerializer,
UpdateContractAttachedFileSerializer,
DestroyContractAttachedFileSerializer,
)
@extend_schema(tags=["ContractAttachedFile"])
class ContractAttachedFileView(BaseViewSetMixin, ModelViewSet):
queryset = ContractAttachedFileModel.objects.all()
serializer_class = ListContractAttachedFileSerializer
permission_classes = [AllowAny]
action_permission_classes = {
"list": [IsAdminUser],
"retrieve": [IsAdminUser],
"create": [IsAdminUser],
"update": [IsAdminUser],
"destroy": [IsAdminUser],
}
action_serializer_class = {
"list": ListContractAttachedFileSerializer,
"retrieve": RetrieveContractAttachedFileSerializer,
"create": CreateContractAttachedFileSerializer,
"update": UpdateContractAttachedFileSerializer,
"destroy": DestroyContractAttachedFileSerializer,
}

View File

@@ -0,0 +1,123 @@
import uuid
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import AllowAny, IsAdminUser # type: ignore
from rest_framework.viewsets import ModelViewSet # type: ignore
from rest_framework.views import APIView # type: ignore
from rest_framework.request import HttpRequest # type: ignore
from rest_framework.response import Response # type: ignore
from rest_framework.decorators import action # type: ignore
from rest_framework.generics import get_object_or_404 # type: ignore
from rest_framework import status # type: ignore
from django_core.mixins import BaseViewSetMixin # type: ignore
from core.apps.contracts.models import (
ContractModel,
ContractAttachedFileModel,
ContractOwnerModel
)
from core.apps.contracts.serializers import (
CreateContractSerializer,
ListContractSerializer,
RetrieveContractSerializer,
UpdateContractSerializer,
DestroyContractSerializer,
ListContractAttachedFileSerializer,
RetrieveContractOwnerSerializer
)
@extend_schema(tags=["Contract"])
class ContractView(BaseViewSetMixin, ModelViewSet):
queryset = ContractModel.objects.all()
serializer_class = ListContractSerializer
permission_classes = [AllowAny]
action_permission_classes = { # type: ignore
"list": [IsAdminUser],
"retrieve": [IsAdminUser],
"create": [IsAdminUser],
"update": [IsAdminUser],
"destroy": [IsAdminUser],
"list_file": [AllowAny],
"list_owner": [AllowAny],
}
action_serializer_class = { # type: ignore
"list": ListContractSerializer,
"retrieve": RetrieveContractSerializer,
"create": CreateContractSerializer,
"update": UpdateContractSerializer,
"destroy": DestroyContractSerializer,
"list_file": ListContractAttachedFileSerializer,
"list_owner": RetrieveContractOwnerSerializer,
}
@extend_schema(
summary="Get List Of Files",
description="Get List Of Files"
)
@action(url_path="files", detail=True, methods=["GET"])
def list_file(
self,
request: HttpRequest,
pk: uuid.UUID,
*args: object,
**kwargs: object
) -> Response:
contract = get_object_or_404(ContractModel, pk=pk)
files = ContractAttachedFileModel.objects.filter(contract=contract)
ser = self.get_serializer(instance=files, many=True) # type: ignore
return Response(data=ser.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Get List Of Owners",
description="Get list of owners"
)
@action(url_path="owners", detail=True, methods=["GET"])
def list_owner(
self,
request: HttpRequest,
pk: uuid.UUID,
*args: object,
**kwargs: object
) -> Response:
contract = get_object_or_404(ContractModel, pk=pk)
owners = ContractOwnerModel.objects.filter(contract=contract)
ser = self.get_serializer(instance=owners, many=True) # type: ignore
return Response(ser.data, status.HTTP_200_OK)
class ContractDetailView(APIView):
permission_classes = [AllowAny]
@extend_schema(
summary="Uploads a file for contract attached files",
description="Creates a file for contract attached files.",
)
def get(
self,
request: HttpRequest,
owner_id: uuid.UUID,
*args: object,
**kwargs: object
) -> Response:
contract = ContractModel.objects.filter(owners__id=owner_id)[0]
ser = RetrieveContractSerializer(instance=contract)
return Response(ser.data, status=status.HTTP_200_OK)
class ListFolderContractsView(APIView):
permission_classes = [IsAdminUser]
def get(
self,
request: HttpRequest,
pk: uuid.UUID,
*args: object,
**kwargs: object
) -> Response:
contracts = ContractModel.objects.filter(folders__id=pk)
ser = ListContractSerializer(instance=contracts, many=True)
return Response(ser.data, status.HTTP_200_OK)

View File

@@ -0,0 +1,35 @@
from django_core.mixins import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import AllowAny, IsAdminUser
from rest_framework.viewsets import ModelViewSet
from core.apps.contracts.models import ContractFileContentModel
from core.apps.contracts.serializers.file_contents import (
CreateContractFileContentSerializer,
ListContractFileContentSerializer,
RetrieveContractFileContentSerializer,
UpdateContractFileContentSerializer,
DestroyContractFileContentSerializer
)
@extend_schema(tags=["ContractFileContent"])
class ContractFileContentView(BaseViewSetMixin, ModelViewSet):
queryset = ContractFileContentModel.objects.all()
serializer_class = ListContractFileContentSerializer
permission_classes = [AllowAny]
action_permission_classes = {
"list": [IsAdminUser],
"retrieve": [IsAdminUser],
"create": [IsAdminUser],
"update": [IsAdminUser],
"destroy": [IsAdminUser],
}
action_serializer_class = {
"list": ListContractFileContentSerializer,
"retrieve": RetrieveContractFileContentSerializer,
"create": CreateContractFileContentSerializer,
"update": UpdateContractFileContentSerializer,
"destroy": DestroyContractFileContentSerializer,
}

View File

@@ -0,0 +1,171 @@
import uuid
from typing import cast
from django_core.mixins import BaseViewSetMixin # type: ignore
from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework.exceptions import PermissionDenied # type: ignore
from rest_framework.decorators import action # type: ignore
from rest_framework.permissions import AllowAny, IsAdminUser # type: ignore
from rest_framework.viewsets import ModelViewSet # type: ignore
from rest_framework.request import HttpRequest # type: ignore
from rest_framework.response import Response # type: ignore
from rest_framework import status # type: ignore
from rest_framework.views import APIView # type: ignore
from rest_framework.parsers import MultiPartParser, FormParser # type: ignore
from rest_framework.generics import get_object_or_404 # type: ignore
from core.apps.contracts.models import (
ContractOwnerModel,
ContractAttachedFileModel
)
from core.apps.contracts.serializers import (
CreateContractOwnerSerializer,
ListContractOwnerSerializer,
RetrieveContractOwnerSerializer,
UpdateContractOwnerSerializer,
DestroyContractOwnerSerializer,
RetrieveContractAttachedFileSerializer,
CreateContractAttachedFileSerializer,
CreateContractFileContentFromOwnerSerializer,
)
@extend_schema(tags=["ContractOwner"])
class ContractOwnerView(BaseViewSetMixin, ModelViewSet):
queryset = ContractOwnerModel.objects.all()
serializer_class = ListContractOwnerSerializer
permission_classes = [AllowAny]
action_permission_classes = {
"list": [IsAdminUser],
"retrieve": [IsAdminUser],
"create": [IsAdminUser],
"update": [IsAdminUser],
"destroy": [IsAdminUser],
}
action_serializer_class = { # type: ignore
"list": ListContractOwnerSerializer,
"retrieve": RetrieveContractOwnerSerializer,
"create": CreateContractOwnerSerializer,
"update": UpdateContractOwnerSerializer,
"destroy": DestroyContractOwnerSerializer,
}
class ContractOwnerFileViewSet(BaseViewSetMixin, ModelViewSet):
queryset = ContractOwnerModel.objects.all()
serializer_class = RetrieveContractAttachedFileSerializer
permission_classes = [AllowAny]
# action_permission_classes = {
# "create_file": [AllowAny],
# "list_file": [AllowAny]
# }
action_serializer_class = { # type: ignore
"list_file": RetrieveContractAttachedFileSerializer,
"create_file": CreateContractAttachedFileSerializer,
}
@extend_schema(
summary="Contract Files Related to Owner",
description="Contract Files Related to Owner",
)
@action(url_path="files", methods=["GET"], detail=True)
def list_file(
self,
request: HttpRequest,
*args: object,
**kwargs: object,
) -> Response:
owner = cast(ContractOwnerModel, self.get_object())
files = ContractAttachedFileModel.objects.filter(
contract__owners=owner, contents__owner=owner
).select_related("contents")
serializer = self.get_serializer(instance=files, many=True)
return Response(serializer.data, status.HTTP_200_OK)
@extend_schema(
summary="Create Contract Files Related to Owner",
description="Create Contract Files Related to Owner"
)
@action(url_path="files", methods=["GET"], detail=True)
def create_file(
self,
request: HttpRequest,
*args: object,
**kwargs: object,
) -> Response:
owner = cast(
ContractOwnerModel,
self.get_queryset().select_related("contract")
)
if not owner.contract.allow_add_files:
raise PermissionDenied(_("Attaching new files was restricted for this contract."))
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
ser.save() # type: ignore
return Response(ser.data, status.HTTP_201_CREATED)
class ContractAttachedFileDeleteView(APIView):
permission_classes = [AllowAny]
@extend_schema(
# request=ContractFileDeleteRequestSerializer,
responses={204: OpenApiResponse(description="File successfully deleted.")},
summary="Delete a file from contract",
description="Deletes a contract-attached file if contract allows file deletion.",
)
def delete(
self,
request: HttpRequest,
owner_id: uuid.UUID,
file_id: uuid.UUID,
*args: object,
**kwargs: object
) -> Response:
owner = get_object_or_404(
ContractOwnerModel.objects.all().select_related("contract"), pk=owner_id
)
if not owner.contract.allow_delete_files:
raise PermissionDenied(_("Deleting attached files was restricted for this contract"))
file = get_object_or_404(
ContractAttachedFileModel.objects.all().select_related("contract"),
pk=file_id
)
if owner.contract.pk != file.contract.pk:
raise PermissionDenied(_("Contract have no such file attached"))
file.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class UploadFileContentView(APIView):
permission_classes = [AllowAny]
parser_classes = [MultiPartParser, FormParser] # type: ignore
@extend_schema(
summary="Uploads a file for contract attached files",
description="Creates a file for contract attached files.",
request=CreateContractFileContentFromOwnerSerializer,
responses={201: CreateContractFileContentFromOwnerSerializer},
)
def post(
self,
request: HttpRequest,
owner_id: uuid.UUID,
file_id: uuid.UUID,
*args: object,
**kwargs: object
) -> Response:
serializer = CreateContractFileContentFromOwnerSerializer(
data=request.data, # type: ignore
context={
"file_id": file_id,
"contract_owner_id": owner_id,
}
)
serializer.is_valid(raise_exception=True)
serializer.save() # type: ignore
return Response(serializer.data, status=status.HTTP_201_CREATED)