From 563042997452b1125569ae43628bef30641f668b Mon Sep 17 00:00:00 2001 From: Fazliddin Abdurahimov Date: Tue, 5 Aug 2025 15:49:53 +0500 Subject: [PATCH] add: contract create serializer, TODO: fix errors with owners create and test --- README.MD | 2 +- ...0005_alter_companyfoldermodel_contracts.py | 21 ++ core/apps/companies/models/folders.py | 2 + core/apps/contracts/models/owners.py | 4 +- .../serializers/contracts/contracts.py | 122 ++++++++- .../contracts/serializers/owners/owner.py | 242 ++++++++++-------- 6 files changed, 283 insertions(+), 110 deletions(-) create mode 100644 core/apps/companies/migrations/0005_alter_companyfoldermodel_contracts.py diff --git a/README.MD b/README.MD index c669d6f..b6a4a07 100644 --- a/README.MD +++ b/README.MD @@ -95,7 +95,7 @@ PATCH /banks/ # admin # ok GET /contracts # admin # ok -POST /contracts # admin # ok +POST /contracts # user # remake GET /contracts/ # admin # ok DELETE /contracts/ # admin # ok PATCH /contracts/ # admin # ok diff --git a/core/apps/companies/migrations/0005_alter_companyfoldermodel_contracts.py b/core/apps/companies/migrations/0005_alter_companyfoldermodel_contracts.py new file mode 100644 index 0000000..d47193c --- /dev/null +++ b/core/apps/companies/migrations/0005_alter_companyfoldermodel_contracts.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.4 on 2025-08-05 06:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("companies", "0004_alter_companymodel_logo_and_more"), + ("contracts", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="companyfoldermodel", + name="contracts", + field=models.ManyToManyField( + blank=True, null=True, related_name="folders", to="contracts.contractmodel", verbose_name="Contracts" + ), + ), + ] diff --git a/core/apps/companies/models/folders.py b/core/apps/companies/models/folders.py index 3adf3b9..e4012f5 100644 --- a/core/apps/companies/models/folders.py +++ b/core/apps/companies/models/folders.py @@ -37,6 +37,8 @@ class CompanyFolderModel(UUIDPrimaryKeyBaseModel): ContractModel, verbose_name=_("Contracts"), related_name="folders", + null=True, + blank=True ) def __str__(self): diff --git a/core/apps/contracts/models/owners.py b/core/apps/contracts/models/owners.py index 3d18d47..6fd39d3 100644 --- a/core/apps/contracts/models/owners.py +++ b/core/apps/contracts/models/owners.py @@ -19,7 +19,7 @@ from core.apps.contracts.validators.owners import ( ) -class LegalEntityModel(UUIDPrimaryKeyBaseModel): +class LegalEntityModel(UUIDPrimaryKeyBaseModel): name = models.CharField( _("Name"), validators=[ @@ -104,7 +104,7 @@ class IndividualModel(UUIDPrimaryKeyBaseModel): ) iin_code = models.CharField( - _("IIN code"), + _("IIN code"), max_length=14, validators=[ iin_code_validator, diff --git a/core/apps/contracts/serializers/contracts/contracts.py b/core/apps/contracts/serializers/contracts/contracts.py index 6703d41..ab1626a 100644 --- a/core/apps/contracts/serializers/contracts/contracts.py +++ b/core/apps/contracts/serializers/contracts/contracts.py @@ -1,8 +1,21 @@ -from rest_framework import serializers +from rest_framework import serializers # type: ignore +from rest_framework.generics import ValidationError # type: ignore +from django.utils.translation import gettext as _ +from django.db import transaction from core.apps.contracts.models import ContractModel +from core.apps.contracts.serializers.attached_files import ( + CreateContractAttachedFileSerializer +) +from core.apps.contracts.serializers.owners import ( + CreateContractOwnerSerializer, +) + +########################################################### +# Base Serializers +########################################################### class BaseContractSerializer(serializers.ModelSerializer): class Meta: model = ContractModel @@ -14,6 +27,9 @@ class BaseContractSerializer(serializers.ModelSerializer): ) +########################################################### +# List Serializers +########################################################### class ListContractSerializer(BaseContractSerializer): class Meta(BaseContractSerializer.Meta): fields = ( @@ -25,18 +41,114 @@ class ListContractSerializer(BaseContractSerializer): ) +########################################################### +# Detail/Retrieve Serializers +########################################################### class RetrieveContractSerializer(BaseContractSerializer): class Meta(BaseContractSerializer.Meta): ... -class CreateContractSerializer(BaseContractSerializer): - class Meta(BaseContractSerializer.Meta): ... - - +########################################################### +# Update Serializers +########################################################### class UpdateContractSerializer(BaseContractSerializer): class Meta(BaseContractSerializer.Meta): ... +########################################################### +# Delete/Destroy Serializers +########################################################### class DestroyContractSerializer(BaseContractSerializer): class Meta(BaseContractSerializer.Meta): fields = ["id"] + + +########################################################### +# Create Serializers +########################################################### +class CreateOwnersForContractSerializer(CreateContractOwnerSerializer): + class Meta(CreateContractOwnerSerializer.Meta): + read_only_fields = ( + *CreateContractOwnerSerializer.Meta.read_only_fields, + "contract", + ) + + def create(self, validated_data: dict[str, object]) -> Meta.model: + contract = self.context.get("contract") + contract_id = self.context.get("contract_id") + + context_data: dict[str, object] = {} + + if contract is None and contract_id is None: + raise ValidationError(_("Either contract or contract_id should be given in context.")) + + if contract is not None: + context_data["contract"] = contract + else: + context_data["contract_id"] = contract_id + + return super().create( + validated_data | context_data + ) + + +class CreateContractAttachedFileForContractSerializer(CreateContractAttachedFileSerializer): + class Meta(CreateContractAttachedFileSerializer.Meta): ... + + def create(self, validated_data: dict[str, object]) -> Meta.model: + contract = self.context.get("contract") + contract_id = self.context.get("contract_id") + + context_data: dict[str, object] = {} + + if contract is None and contract_id is None: + raise ValidationError(_("Either contract or contract_id should be given in context.")) + + if contract is not None: + context_data["contract"] = contract + else: + context_data["contract_id"] = contract_id + + return super().create( # type: ignore + validated_data | context_data + ) + + +class CreateContractSerializer(BaseContractSerializer): + owners = CreateOwnersForContractSerializer(many=True) + attached_files = CreateContractAttachedFileForContractSerializer( + many=True, default=list, required=False + ) + + class Meta(BaseContractSerializer.Meta): + pass + + def validate_owners(self, owners: list[dict[str, object]]) -> list[dict[str, object]]: + if not owners: + raise ValidationError("Contract should have minimum one owner.") + return owners + + def create(self, validated_data: dict[str, object]) -> Meta.model: + attached_files_data = validated_data.pop("attached_files", None) + owners_data = validated_data.pop("owners", None) + + with transaction.atomic(): + contract = self.Meta.model() + contract.save() + + owners = CreateOwnersForContractSerializer( + data=owners_data, many=True, context=self.context | {"contract": contract} + ) + owners.is_valid(raise_exception=True) + owners.save() # type: ignore + + if attached_files_data: + attached_files = CreateContractAttachedFileForContractSerializer( + data=attached_files_data, many=True, context=self.context | {"contract": contract} + ) + attached_files.is_valid(raise_exception=True) + attached_files.save() # type: ignore + + return contract + + diff --git a/core/apps/contracts/serializers/owners/owner.py b/core/apps/contracts/serializers/owners/owner.py index 3ef5341..3871f63 100644 --- a/core/apps/contracts/serializers/owners/owner.py +++ b/core/apps/contracts/serializers/owners/owner.py @@ -1,22 +1,19 @@ -from rest_framework import serializers +from rest_framework import serializers # type: ignore +from rest_framework.exceptions import ValidationError # type: ignore from core.apps.contracts.models import ( ContractOwnerModel, LegalEntityModel, IndividualModel, ) +from django.utils.translation import gettext as _ 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 +########################################################### +# Base Serializers +########################################################### class BaseIndividualSerializer(serializers.ModelSerializer): class Meta: model = IndividualModel @@ -25,26 +22,7 @@ class BaseIndividualSerializer(serializers.ModelSerializer): "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", + "owner", ) @@ -56,26 +34,7 @@ class BaseLegalEntitySerializer(serializers.ModelSerializer): "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", + "owner", ) @@ -92,62 +51,35 @@ class BaseContractOwnerSerializer(serializers.ModelSerializer): "updated_at", ) extra_kwargs = { - "legal_entity": { - "required": False - }, - "individual": { - "required": False - } + 'legal_entity': {'required': False, 'allow_null': True}, + 'individual': {'required': False, 'allow_null': True}, } - 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() +########################################################### +# List Serializers +########################################################### +class ListIndividualSerializer(BaseIndividualSerializer): + class Meta(BaseIndividualSerializer.Meta): + fields = ( + "id", + "full_name", + "phone", + "created_at", + "updated_at", + ) - 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) +class ListLegalEntitySerializer(BaseLegalEntitySerializer): + class Meta(BaseLegalEntitySerializer.Meta): + fields = ( + "id", + "name", + "phone", + "created_at", + "updated_at", + ) - 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) @@ -156,19 +88,125 @@ class ListContractOwnerSerializer(BaseContractOwnerSerializer): class Meta(BaseContractOwnerSerializer.Meta): ... +########################################################### +# Detail/Retrieve Serializers +########################################################### class RetrieveContractOwnerSerializer(BaseContractOwnerSerializer): class Meta(BaseContractOwnerSerializer.Meta): ... -class CreateContractOwnerSerializer(BaseContractOwnerSerializer): - class Meta(BaseContractOwnerSerializer.Meta): ... +########################################################### +# Update Serializers +########################################################### +class UpdateIndividualSeriailzer(BaseIndividualSerializer): + class Meta(BaseIndividualSerializer.Meta): ... + + +class UpdateLegalEntitySerializer(BaseLegalEntitySerializer): + class Meta(BaseLegalEntitySerializer.Meta): ... class UpdateContractOwnerSerializer(BaseContractOwnerSerializer): + individual = UpdateIndividualSeriailzer(required=False, partial=True) + legal_entity = UpdateLegalEntitySerializer(required=False, partial=True) + class Meta(BaseContractOwnerSerializer.Meta): ... + def update(self, instance: Meta.model, validated_data: dict[str, object]): + individual_data = validated_data.pop("individual", None) + legal_entity_data = validated_data.pop("legal_entity", None) + if instance.individual is not None: + if individual_data is not None: + individual_serializer = UpdateIndividualSeriailzer( + instance=instance.individual, + data=individual_data, + partial=True, + ) + individual_serializer.is_valid(raise_exception=True) + individual_serializer.save() # type: ignore + + elif instance.legal_entity is not None: + if legal_entity_data is not None: + legal_entity_serializer = UpdateLegalEntitySerializer( + instance=instance.legal_entity, + data=legal_entity_data, + partial=True + ) + legal_entity_serializer.is_valid(raise_exception=True) + legal_entity_serializer.save() # type: ignore + + return super().update(instance, validated_data) # type: ignore + + +########################################################### +# Delete/Destroy Serializers +########################################################### class DestroyContractOwnerSerializer(BaseContractOwnerSerializer): class Meta(BaseContractOwnerSerializer.Meta): fields = ["id"] - \ No newline at end of file + + +########################################################### +# Create Serializers +########################################################### +class CreateIndividualSerializer(BaseIndividualSerializer): + class Meta(BaseIndividualSerializer.Meta): ... + + +class CreateLegalEntitySerializer(BaseLegalEntitySerializer): + class Meta(BaseLegalEntitySerializer.Meta): ... + + +class CreateContractOwnerSerializer(BaseContractOwnerSerializer): #! TODO: problem: individual or legal entity can not be null + individual = CreateIndividualSerializer(required=False, allow_null=True, default=None) + legal_entity = CreateLegalEntitySerializer(required=False, allow_null=True, default=None) + + class Meta(BaseContractOwnerSerializer.Meta): + read_only_fields = ( + *BaseContractOwnerSerializer.Meta.read_only_fields, + "status" + ) + + def create(self, validated_data: dict[str, object]) -> Meta.model: + individual_data = validated_data.pop("individual", None) + legal_entity_data = validated_data.pop("legal_entity", None) + + with transaction.atomic(): + if individual_data is not None: + individual_serializer = CreateIndividualSerializer(data=individual_data) + individual_serializer.is_valid(raise_exception=True) + validated_data["individual"] = individual_serializer.save() # type: ignore + + if legal_entity_data is not None: + legal_entity_serializer = CreateLegalEntitySerializer(data=legal_entity_data) + legal_entity_serializer.is_valid(raise_exception=True) + validated_data["legal_entity"] = legal_entity_serializer.save() # type: ignore + + owner = self.Meta.model(**validated_data) + owner.save() + return owner + + def to_internal_value(self, data: dict[str, object]) -> dict[str, object]: + data = data.copy() + + if "legal_entity" in data and data["legal_entity"] in [None, ""]: + data["legal_entity"] = None + + if "individual" in data and data["individual"] in [None, ""]: + data["individual"] = None + + return super().to_internal_value(data) # type: ignore + + def validate(self, attrs: dict[str, object]): + legal = attrs.get("legal_entity") + individual = attrs.get("individual") + + # Optional: enforce at least one must be provided + if not legal and not individual: + raise serializers.ValidationError( + "Either legal_entity or individual must be provided." + ) + + return attrs + \ No newline at end of file