diff --git a/leasing/enums.py b/leasing/enums.py index d62f6cb0..88032d00 100644 --- a/leasing/enums.py +++ b/leasing/enums.py @@ -659,6 +659,16 @@ class Labels: KUVA_NUP = "KuVa / Nuorisopalvelut" +def is_akv_or_kuva_service_unit_id(service_unit_id: int) -> bool: + akv_kuva_service_unit_ids = [ + ServiceUnitId.AKV, + ServiceUnitId.KUVA_LIPA, + ServiceUnitId.KUVA_UPA, + ServiceUnitId.KUVA_NUP, + ] + return service_unit_id in akv_kuva_service_unit_ids + + class SapSalesOfficeNumber(Enum): """ In Finnish: SAP myyntitoimiston numero diff --git a/leasing/serializers/rent.py b/leasing/serializers/rent.py index a96f0f23..3dffbc68 100644 --- a/leasing/serializers/rent.py +++ b/leasing/serializers/rent.py @@ -5,32 +5,39 @@ from rest_framework.serializers import ListSerializer from field_permissions.serializers import FieldPermissionsSerializerMixin -from leasing.enums import DueDatesType, RentAdjustmentAmountType, RentCycle, RentType -from leasing.models import Index, ReceivableType -from leasing.models.rent import ( - EqualizedRent, - LeaseBasisOfRentManagementSubvention, - LeaseBasisOfRentTemporarySubvention, - ManagementSubvention, - ManagementSubventionFormOfManagement, - TemporarySubvention, +from leasing.enums import ( + DueDatesType, + RentAdjustmentAmountType, + RentCycle, + RentType, + is_akv_or_kuva_service_unit_id, ) -from leasing.serializers.receivable_type import ReceivableTypeSerializer -from leasing.serializers.utils import validate_seasonal_day_for_month -from users.serializers import UserSerializer - -from ..models import ( +from leasing.models import ( ContractRent, Decision, FixedInitialYearRent, + Index, IndexAdjustedRent, LeaseBasisOfRent, PayableRent, + ReceivableType, Rent, RentAdjustment, RentDueDate, RentIntendedUse, ) +from leasing.models.rent import ( + EqualizedRent, + LeaseBasisOfRentManagementSubvention, + LeaseBasisOfRentTemporarySubvention, + ManagementSubvention, + ManagementSubventionFormOfManagement, + TemporarySubvention, +) +from leasing.serializers.receivable_type import ReceivableTypeSerializer +from leasing.serializers.utils import validate_seasonal_day_for_month +from users.serializers import UserSerializer + from .decision import DecisionSerializer from .utils import ( DayMonthField, @@ -538,12 +545,18 @@ class Meta: "override_receivable_type", ) - def validate(self, data): + def validate(self, rent_data: dict): + self.validate_seasonal_values(rent_data) + self.validate_override_receivable_type_value(rent_data) + return rent_data + + def validate_seasonal_values(self, rent_data: dict) -> None: + """Raises: serializers.ValidationError""" seasonal_values = [ - data.get("seasonal_start_day"), - data.get("seasonal_start_month"), - data.get("seasonal_end_day"), - data.get("seasonal_end_month"), + rent_data.get("seasonal_start_day"), + rent_data.get("seasonal_start_month"), + rent_data.get("seasonal_end_day"), + rent_data.get("seasonal_end_month"), ] if not all(v is None for v in seasonal_values) and any( @@ -553,7 +566,10 @@ def validate(self, data): _("All seasonal values are required if one is set") ) - if all(seasonal_values) and data.get("due_dates_type") != DueDatesType.CUSTOM: + if ( + all(seasonal_values) + and rent_data.get("due_dates_type") != DueDatesType.CUSTOM + ): raise serializers.ValidationError( _("Due dates type must be custom if seasonal dates are set") ) @@ -562,7 +578,63 @@ def validate(self, data): validate_seasonal_day_for_month(start_day, start_month) validate_seasonal_day_for_month(end_day, end_month) - return data + def validate_override_receivable_type_value(self, rent_data: dict) -> None: + """ + Override receivabletype is mandatory for AKV and KuVa leases, and not + used by MaKe/Tontit. + + It is only used in index rents, fixed rents, and manual rents, because + these rent types can generate automatic invoices. + + Raises: serializers.ValidationError + """ + override_receivable_type = rent_data.get("override_receivable_type") + + if not override_receivable_type: + # Empty override receivabletype input should be allowed, because it + # is always empty for all MaKe rents, and those must be allowed. + return + + elif not is_akv_or_kuva_service_unit_id( + override_receivable_type.service_unit_id + ): + # All MaKe receivabletypes should be rejected regardless of rent + # type, because MaKe doesn't use the override feature, and if any + # other service unit would use MaKe's receivabletypes, it would be + # a mistake. + raise serializers.ValidationError( + _( + f'Override receivabletype "{override_receivable_type.name}" was unexpected. ' + "Override receivabletype is not used by MaKe/Tontit." + ) + ) + + elif is_akv_or_kuva_service_unit_id(override_receivable_type.service_unit_id): + rent_type = rent_data.get("type", {}) + + if rent_type in [ + RentType.INDEX, + RentType.INDEX2022, + RentType.FIXED, + RentType.MANUAL, + ]: + # These rent types can generate automatic invoices, so they can + # utilize the override receivabletype. + return + else: + raise serializers.ValidationError( + _( + f'Override receivabletype "{override_receivable_type.name}" was unexpected. ' + f'Rent type "{rent_type.name}" does not generate automatic invoices.' + ) + ) + + else: + raise serializers.ValidationError( + _( + "Unhandled case in override receivabletype validation. Rejecting just in case." + ) + ) class LeaseBasisOfRentManagementSubventionSerializer(serializers.ModelSerializer): diff --git a/leasing/serializers/utils.py b/leasing/serializers/utils.py index 198254ed..4ed8b191 100644 --- a/leasing/serializers/utils.py +++ b/leasing/serializers/utils.py @@ -308,7 +308,8 @@ def get_file_filename(self, obj): return os.path.basename(obj.file.name) -def validate_seasonal_day_for_month(day: int | None, month: int | None): +def validate_seasonal_day_for_month(day: int | None, month: int | None) -> None: + """Raises: serializers.ValidationError""" if day is None and month is None: return diff --git a/leasing/tests/serializers/test_rent.py b/leasing/tests/serializers/test_rent.py new file mode 100644 index 00000000..7f518aaf --- /dev/null +++ b/leasing/tests/serializers/test_rent.py @@ -0,0 +1,83 @@ +import pytest +from rest_framework.exceptions import ValidationError + +from leasing.enums import RentType, ServiceUnitId +from leasing.models import ServiceUnit +from leasing.serializers.rent import RentCreateUpdateSerializer + + +@pytest.mark.django_db +def test_is_valid_override_receivable_type(django_db_setup, receivable_type_factory): + """ + Test that the requirements described in the target function docstring hold. + """ + make = ServiceUnit.objects.get(pk=ServiceUnitId.MAKE) + akv = ServiceUnit.objects.get(pk=ServiceUnitId.AKV) + kuva_lipa = ServiceUnit.objects.get(pk=ServiceUnitId.KUVA_LIPA) + kuva_upa = ServiceUnit.objects.get(pk=ServiceUnitId.KUVA_UPA) + kuva_nup = ServiceUnit.objects.get(pk=ServiceUnitId.KUVA_NUP) + + serializer = RentCreateUpdateSerializer() + + # Validator should reject all MaKe receivable types regardless of rent type. + rent_datas_make = [ + { + "type": rent_type, + "override_receivable_type": receivable_type_factory(service_unit=make), + } + for rent_type in RentType + ] + for data in rent_datas_make: + with pytest.raises(ValidationError): + serializer.validate(data) + + # Validator should allow empty override receivable type input. + rent_datas_empty = [ + { + "type": rent_type, + "override_receivable_type": None, + } + for rent_type in RentType + ] + for data in rent_datas_empty: + assert serializer.validate(data) + + # Validator should allow AKV and KuVa receivable types, if rent type is valid. + rent_datas_akv_and_kuva = [ + { + "type": RentType.INDEX2022, + "override_receivable_type": receivable_type_factory(service_unit=unit), + } + for unit in [akv, kuva_lipa, kuva_upa, kuva_nup] + ] + for data in rent_datas_akv_and_kuva: + assert serializer.validate(data) + + # Validator should allow rent types that can generate automatic invoices. + rent_datas_valid_types = [ + { + "type": rent_type, + "override_receivable_type": receivable_type_factory(service_unit=akv), + } + for rent_type in [ + RentType.FIXED, + RentType.INDEX, + RentType.INDEX2022, + RentType.MANUAL, + ] + ] + for data in rent_datas_valid_types: + assert serializer.validate(data) + + # Validator should reject the receivable type input for rent types that + # don't generate automatic invoices. + rent_datas_invalid_types = [ + { + "type": rent_type, + "override_receivable_type": receivable_type_factory(service_unit=akv), + } + for rent_type in [RentType.FREE, RentType.ONE_TIME] + ] + for data in rent_datas_invalid_types: + with pytest.raises(ValidationError): + serializer.validate(data)