From 9804562aeec9522b78d86964390044fe36d168e7 Mon Sep 17 00:00:00 2001 From: leevi-identio Date: Thu, 23 Oct 2025 09:50:55 +0300 Subject: [PATCH 1/3] Add different recipient for final installment on invoice pdf - Add fields `payment_recipient` and `payment_recipient_final` to ApartmentDocument - Refactor tests, add stub for `get_hitas_contract_pdf_data` func - Hitas contract pdf data creation to its own function --- apartment/elastic/documents.py | 2 + application_form/pdf/hitas.py | 481 ++++++++++++----------- application_form/tests/pdf_utils.py | 74 +++- application_form/tests/test_pdf_haso.py | 14 +- application_form/tests/test_pdf_hitas.py | 5 +- application_form/tests/utils.py | 1 - invoicing/pdf.py | 45 ++- invoicing/tests/test_pdf.py | 46 ++- 8 files changed, 418 insertions(+), 250 deletions(-) diff --git a/apartment/elastic/documents.py b/apartment/elastic/documents.py index f34d99a45..179a40e6e 100644 --- a/apartment/elastic/documents.py +++ b/apartment/elastic/documents.py @@ -100,6 +100,8 @@ class ApartmentDocument(ReadOnlyDocument): project_barred_bank_account = Keyword() project_regular_bank_account = Keyword() + project_payment_recipient = Keyword() + project_payment_recipient_final = Keyword() uuid = Keyword(required=True) diff --git a/application_form/pdf/hitas.py b/application_form/pdf/hitas.py index edc32410b..77f1bf2cf 100644 --- a/application_form/pdf/hitas.py +++ b/application_form/pdf/hitas.py @@ -390,253 +390,278 @@ class HitasContractPDFData(PDFData): } -def create_hitas_contract_pdf( - reservation: ApartmentReservation, - sales_price_paid_place: str, - sales_price_paid_time: str, - salesperson: User, -) -> BytesIO: - customer = SafeAttributeObject(reservation.customer) - primary_profile = SafeAttributeObject(customer.primary_profile) - secondary_profile = SafeAttributeObject(customer.secondary_profile) - apartment: ApartmentDocument = SafeAttributeObject( - get_apartment(reservation.apartment_uuid, include_project_fields=True) - ) +def get_hitas_contract_pdf_data( + apartment: ApartmentDocument, + reservation: ApartmentReservation, + sales_price_paid_place: str, + sales_price_paid_time: str, + salesperson: User, + ) -> Union[HitasContractPDFData, HitasCompleteApartmentContractPDFData]: + customer = SafeAttributeObject(reservation.customer) + primary_profile = SafeAttributeObject(customer.primary_profile) + secondary_profile = SafeAttributeObject(customer.secondary_profile) + + # use contract for complete apartment + # can possibly be None, use bool() to convert to False in that case + complete_apartment = bool(apartment.project_use_complete_contract) - # use contract for complete apartment - # can possibly be None, use bool() to convert to False in that case - complete_apartment = bool(apartment.project_use_complete_contract) + ( + payment_1, + payment_2, + payment_3, + payment_4, + payment_5, + payment_6, + payment_7, + ) = _get_numbered_installments(apartment, reservation) + + down_payment = SafeAttributeObject( + reservation.apartment_installments.filter( + type=InstallmentType.DOWN_PAYMENT + ).first() + ) - ( - payment_1, - payment_2, - payment_3, - payment_4, - payment_5, - payment_6, - payment_7, - ) = _get_numbered_installments(apartment, reservation) - - down_payment = SafeAttributeObject( - reservation.apartment_installments.filter( - type=InstallmentType.DOWN_PAYMENT - ).first() - ) + sales_price_paid_place_and_time = ( + f"{sales_price_paid_place} {sales_price_paid_time}" + ) - sales_price_paid_place_and_time = ( - f"{sales_price_paid_place} {sales_price_paid_time}" - ) + def hitas_price(cents: Union[int, None]) -> Union[PDFCurrencyField, None]: + """Turns the price in cents to whole euros (division by 100). Outputs + a PDFCurrencyField prefilled with a string that has the euro sum + as words (in Finnish) and as numbers. - def hitas_price(cents: Union[int, None]) -> Union[PDFCurrencyField, None]: - """Turns the price in cents to whole euros (division by 100). Outputs - a PDFCurrencyField prefilled with a string that has the euro sum - as words (in Finnish) and as numbers. + e.g. + 12000 -> "tuhat kaksisataa 1200 €" - e.g. - 12000 -> "tuhat kaksisataa 1200 €" + 11000000 -> "satakymmenentuhatta 110000 €" - 11000000 -> "satakymmenentuhatta 110000 €" + 31115224 -> "kolmekymmentäyksimiljoonaa sataviisitoistatuhatta + kaksisataakaksikymmentäneljä 31115224 €" - 31115224 -> "kolmekymmentäyksimiljoonaa sataviisitoistatuhatta - kaksisataakaksikymmentäneljä 31115224 €" + Args: + cents (Union[int, None]): The sum in cents - Args: - cents (Union[int, None]): The sum in cents + Returns: Union[PDFCurrencyField, None]: + """ + if cents is None: + return None + return PDFCurrencyField( + prefix=num2words(Decimal(cents) / 100, lang="fi") + " ", + cents=cents, + suffix=" €", + ) - Returns: Union[PDFCurrencyField, None]: - """ - if cents is None: - return None - return PDFCurrencyField( - prefix=num2words(Decimal(cents) / 100, lang="fi") + " ", - cents=cents, - suffix=" €", + signing_buyers = " & ".join( + name + for name in [primary_profile.full_name, secondary_profile.full_name] + if name ) - signing_buyers = " & ".join( - name - for name in [primary_profile.full_name, secondary_profile.full_name] - if name - ) + contract_data = { + "occupant_1": primary_profile.full_name, + "occupant_1_share_of_ownership": None, + "occupant_1_address": ( + (primary_profile.street_address or "") + + ", " + + (primary_profile.postal_code or "") + + " " + + (primary_profile.city or "") + ).strip(), + "occupant_1_phone_number": primary_profile.phone_number, + "occupant_1_email": primary_profile.email, + "occupant_1_ssn_or_business_id": primary_profile.national_identification_number, + "occupant_2": secondary_profile.full_name, + "occupant_2_share_of_ownership": None, + "occupant_2_address": ( + (secondary_profile.street_address or "") + + ", " + + (secondary_profile.postal_code or "") + + " " + + (secondary_profile.city or "") + ).strip(), + "occupant_2_phone_number": secondary_profile.phone_number, + "occupant_2_email": secondary_profile.email, + "occupant_2_ssn_or_business_id": secondary_profile.national_identification_number, # noqa: E501 + "project_housing_company": apartment.project_housing_company, + "project_contract_business_id": apartment.project_contract_business_id, + "project_address": " ".join( + [ + apartment.project_street_address, + f"{apartment.project_postal_code} {apartment.project_city}", + ] + ), + "project_realty_id": apartment.project_realty_id, + "housing_type_ownership": False, + "housing_type_rental": True, + "housing_shares": f"{apartment.stock_start_number or ''} - {apartment.stock_end_number or ''}", # noqa: E501 + "apartment_street_address": None, + "apartment_structure": apartment.apartment_structure, + "apartment_number": apartment.apartment_number, + "floor": apartment.floor, + "living_area": apartment.living_area, + "other_space": None, + "other_space_area": None, + "project_contract_transfer_restriction_false": apartment.project_contract_transfer_restriction # noqa E501 + is False, + "project_contract_transfer_restriction_true": apartment.project_contract_transfer_restriction, # noqa E501 + "project_contract_transfer_restriction_text": apartment.project_contract_article_of_association, # noqa E501 + "project_contract_material_selection_later_false": apartment.project_contract_material_selection_later # noqa E501 + is False, + "project_contract_material_selection_later_true": apartment.project_contract_material_selection_later, # noqa E501 + "project_contract_material_selection_description": apartment.project_contract_material_selection_description, # noqa E501 + "project_contract_material_selection_date": apartment.project_contract_material_selection_date, # noqa E501 + "sales_price": hitas_price(apartment.sales_price), + "loan_share": hitas_price(apartment.loan_share), + "debt_free_sales_price": hitas_price(apartment.debt_free_sales_price), + "payment_1_label": payment_1.type, + "payment_1_amount": PDFCurrencyField(euros=payment_1.value), + "payment_1_due_date": payment_1.due_date, + "payment_1_percentage": payment_1._percentage, + "payment_2_label": payment_2.type, + "payment_2_amount": PDFCurrencyField(euros=payment_2.value), + "payment_2_due_date": payment_2.due_date, + "payment_2_percentage": payment_2._percentage, + "payment_3_label": payment_3.type, + "payment_3_amount": PDFCurrencyField(euros=payment_3.value), + "payment_3_due_date": payment_3.due_date, + "payment_3_percentage": payment_3._percentage, + "payment_4_label": payment_4.type, + "payment_4_amount": PDFCurrencyField(euros=payment_4.value), + "payment_4_due_date": payment_4.due_date, + "payment_4_percentage": payment_4._percentage, + "payment_5_label": payment_5.type, + "payment_5_amount": PDFCurrencyField(euros=payment_5.value), + "payment_5_due_date": payment_5.due_date, + "payment_5_percentage": payment_5._percentage, + "second_last_payment_label": "6", + "second_last_payment_basis_sales_price": False, + "second_last_payment_basis_debt_free_sales_price": True, + "second_last_payment_dfsp_percentage": payment_6._percentage, + "second_last_payment_dfsp_amount": PDFCurrencyField(euros=payment_6.value), + "last_payment_label": "7", + "last_payment_basis_sales_price": False, + "last_payment_basis_debt_free_sales_price": True, + "last_payment_dfsp_percentage": payment_7._percentage, + "last_payment_dfsp_amount": PDFCurrencyField(euros=payment_7.value), + "payment_bank_1": apartment.project_contract_depositary, + "payment_account_number_1": apartment.project_regular_bank_account, + "payment_bank_2": apartment.project_contract_depositary, + "payment_account_number_2": apartment.project_barred_bank_account, + "down_payment_amount": PDFCurrencyField( + euros=down_payment.amount if down_payment.amount else Decimal(0), + suffix=" €", + ), + "project_contract_apartment_completion_selection_1": apartment.project_contract_apartment_completion_selection_1, # noqa E501 + "project_contract_apartment_completion_selection_1_date": apartment.project_contract_apartment_completion_selection_1_date, # noqa E501 + "project_contract_apartment_completion_selection_2": apartment.project_contract_apartment_completion_selection_2, # noqa E501 + "project_contract_apartment_completion_selection_2_start": apartment.project_contract_apartment_completion_selection_2_start, # noqa E501 + "project_contract_apartment_completion_selection_2_end": apartment.project_contract_apartment_completion_selection_2_end, # noqa E501 + "project_contract_apartment_completion_selection_3": apartment.project_contract_apartment_completion_selection_3, # noqa E501 + "project_contract_apartment_completion_selection_3_date": apartment.project_contract_apartment_completion_selection_3_date, # noqa E501 + "project_contract_depositary": apartment.project_contract_depositary, + "project_contract_repository": apartment.project_contract_repository, + "breach_of_contract_option_1": False, + "breach_of_contract_option_2": True, + "project_contract_collateral_type": apartment.project_contract_collateral_type, + "project_contract_default_collateral": apartment.project_contract_default_collateral, # noqa E501 + "project_contract_construction_permit_requested": ( + (apartment.project_contract_construction_permit_requested) + if apartment.project_contract_construction_permit_requested + else None + ), + "project_contract_other_terms": apartment.project_contract_combined_terms, + "project_documents_delivered": apartment.project_documents_delivered, + "signing_place_and_time": sales_price_paid_place_and_time, + "salesperson": salesperson.profile_or_user_full_name, + "signing_buyers": signing_buyers, + "project_contract_collateral_bank_and_address": " ".join( + [ + apartment.project_contract_depositary or "", + apartment.project_contract_repository or "", + ] + ), + } + + # override language to Finnish, as the user's browser settings etc. + # shouldn't affect the printed out PDFs + # further info on how Django resolves language preference: + # https://docs.djangoproject.com/en/5.1/topics/i18n/translation/ + with translation.override("fi"): + payment_1_price = hitas_price(payment_1.value * 100) + payment_terms_rest_of_price = f"{payment_1.type.label}" + if payment_1.due_date: + due_date = payment_1.due_date.strftime("%d.%m.%Y") + payment_terms_rest_of_price += f" {due_date}" + + payment_terms_rest_of_price += f" {payment_1_price.formatted_number_string()} {payment_1_price.suffix}" # noqa: E501 + + # full apartment contract data is mostly the same fields but with some changes + full_apartment_contract_data = { + **contract_data, + "building_permit_applied_for": apartment.project_construction_permit_claim, + "buyer_has_paid_down_payment": "", + "credit_interest": "0,00%", + "debt_free_price_x_0_014": True, + "project_documents_delivered": apartment.project_documents_delivered, + "final_payment": "", + "guarantee": "", + "guarantee_attachment_exists": True, + "guarantee_attachment_not_exists": False, + "project_contract_collateral_type": apartment.project_contract_default_collateral, # noqa: E501 + "loan_share_and_sales_price": hitas_price(apartment.debt_free_sales_price), + "occupants_signatures": signing_buyers, + "other_contract_terms": apartment.project_contract_combined_terms, + "payment_terms_rest_of_price": payment_terms_rest_of_price, + "project_built_according_to_regulations": "", # noqa: E501 + "sales_price_paid": "", + "sales_price_paid_place_and_time": sales_price_paid_place_and_time, # noqa: E501 + "sales_price_paid_salesperson_signature": salesperson.profile_or_user_full_name, + "sales_price_x_0_02": False, + "other_space": "", + "other_space_area": "", + "salesperson_signature": salesperson.profile_or_user_full_name, + "transfer_of_shares": apartment.project_shares_transferred_when, + "transfer_of_posession": apartment.project_control_transferred_when, + "transfer_of_shares_confirmed": sales_price_paid_place_and_time, + "transfer_of_shares_signature": signing_buyers, + } + + contract_dataclass = HitasContractPDFData + pdf_template_path = HITAS_CONTRACT_PDF_TEMPLATE_FILE_NAME + + if complete_apartment: + contract_dataclass = HitasCompleteApartmentContractPDFData + contract_data = full_apartment_contract_data + pdf_template_path = HITAS_COMPLETE_APARTMENT_CONTRACT_PDF_TEMPLATE_FILE_NAME + + pdf_data = contract_dataclass(**contract_data) + + return pdf_data - contract_data = { - "occupant_1": primary_profile.full_name, - "occupant_1_share_of_ownership": None, - "occupant_1_address": ( - (primary_profile.street_address or "") - + ", " - + (primary_profile.postal_code or "") - + " " - + (primary_profile.city or "") - ).strip(), - "occupant_1_phone_number": primary_profile.phone_number, - "occupant_1_email": primary_profile.email, - "occupant_1_ssn_or_business_id": primary_profile.national_identification_number, - "occupant_2": secondary_profile.full_name, - "occupant_2_share_of_ownership": None, - "occupant_2_address": ( - (secondary_profile.street_address or "") - + ", " - + (secondary_profile.postal_code or "") - + " " - + (secondary_profile.city or "") - ).strip(), - "occupant_2_phone_number": secondary_profile.phone_number, - "occupant_2_email": secondary_profile.email, - "occupant_2_ssn_or_business_id": secondary_profile.national_identification_number, # noqa: E501 - "project_housing_company": apartment.project_housing_company, - "project_contract_business_id": apartment.project_contract_business_id, - "project_address": " ".join( - [ - apartment.project_street_address, - f"{apartment.project_postal_code} {apartment.project_city}", - ] - ), - "project_realty_id": apartment.project_realty_id, - "housing_type_ownership": False, - "housing_type_rental": True, - "housing_shares": f"{apartment.stock_start_number or ''} - {apartment.stock_end_number or ''}", # noqa: E501 - "apartment_street_address": None, - "apartment_structure": apartment.apartment_structure, - "apartment_number": apartment.apartment_number, - "floor": apartment.floor, - "living_area": apartment.living_area, - "other_space": None, - "other_space_area": None, - "project_contract_transfer_restriction_false": apartment.project_contract_transfer_restriction # noqa E501 - is False, - "project_contract_transfer_restriction_true": apartment.project_contract_transfer_restriction, # noqa E501 - "project_contract_transfer_restriction_text": apartment.project_contract_article_of_association, # noqa E501 - "project_contract_material_selection_later_false": apartment.project_contract_material_selection_later # noqa E501 - is False, - "project_contract_material_selection_later_true": apartment.project_contract_material_selection_later, # noqa E501 - "project_contract_material_selection_description": apartment.project_contract_material_selection_description, # noqa E501 - "project_contract_material_selection_date": apartment.project_contract_material_selection_date, # noqa E501 - "sales_price": hitas_price(apartment.sales_price), - "loan_share": hitas_price(apartment.loan_share), - "debt_free_sales_price": hitas_price(apartment.debt_free_sales_price), - "payment_1_label": payment_1.type, - "payment_1_amount": PDFCurrencyField(euros=payment_1.value), - "payment_1_due_date": payment_1.due_date, - "payment_1_percentage": payment_1._percentage, - "payment_2_label": payment_2.type, - "payment_2_amount": PDFCurrencyField(euros=payment_2.value), - "payment_2_due_date": payment_2.due_date, - "payment_2_percentage": payment_2._percentage, - "payment_3_label": payment_3.type, - "payment_3_amount": PDFCurrencyField(euros=payment_3.value), - "payment_3_due_date": payment_3.due_date, - "payment_3_percentage": payment_3._percentage, - "payment_4_label": payment_4.type, - "payment_4_amount": PDFCurrencyField(euros=payment_4.value), - "payment_4_due_date": payment_4.due_date, - "payment_4_percentage": payment_4._percentage, - "payment_5_label": payment_5.type, - "payment_5_amount": PDFCurrencyField(euros=payment_5.value), - "payment_5_due_date": payment_5.due_date, - "payment_5_percentage": payment_5._percentage, - "second_last_payment_label": "6", - "second_last_payment_basis_sales_price": False, - "second_last_payment_basis_debt_free_sales_price": True, - "second_last_payment_dfsp_percentage": payment_6._percentage, - "second_last_payment_dfsp_amount": PDFCurrencyField(euros=payment_6.value), - "last_payment_label": "7", - "last_payment_basis_sales_price": False, - "last_payment_basis_debt_free_sales_price": True, - "last_payment_dfsp_percentage": payment_7._percentage, - "last_payment_dfsp_amount": PDFCurrencyField(euros=payment_7.value), - "payment_bank_1": apartment.project_contract_depositary, - "payment_account_number_1": apartment.project_regular_bank_account, - "payment_bank_2": apartment.project_contract_depositary, - "payment_account_number_2": apartment.project_barred_bank_account, - "down_payment_amount": PDFCurrencyField( - euros=down_payment.amount if down_payment.amount else Decimal(0), - suffix=" €", - ), - "project_contract_apartment_completion_selection_1": apartment.project_contract_apartment_completion_selection_1, # noqa E501 - "project_contract_apartment_completion_selection_1_date": apartment.project_contract_apartment_completion_selection_1_date, # noqa E501 - "project_contract_apartment_completion_selection_2": apartment.project_contract_apartment_completion_selection_2, # noqa E501 - "project_contract_apartment_completion_selection_2_start": apartment.project_contract_apartment_completion_selection_2_start, # noqa E501 - "project_contract_apartment_completion_selection_2_end": apartment.project_contract_apartment_completion_selection_2_end, # noqa E501 - "project_contract_apartment_completion_selection_3": apartment.project_contract_apartment_completion_selection_3, # noqa E501 - "project_contract_apartment_completion_selection_3_date": apartment.project_contract_apartment_completion_selection_3_date, # noqa E501 - "project_contract_depositary": apartment.project_contract_depositary, - "project_contract_repository": apartment.project_contract_repository, - "breach_of_contract_option_1": False, - "breach_of_contract_option_2": True, - "project_contract_collateral_type": apartment.project_contract_collateral_type, - "project_contract_default_collateral": apartment.project_contract_default_collateral, # noqa E501 - "project_contract_construction_permit_requested": ( - (apartment.project_contract_construction_permit_requested) - if apartment.project_contract_construction_permit_requested - else None - ), - "project_contract_other_terms": apartment.project_contract_combined_terms, - "project_documents_delivered": apartment.project_documents_delivered, - "signing_place_and_time": sales_price_paid_place_and_time, - "salesperson": salesperson.profile_or_user_full_name, - "signing_buyers": signing_buyers, - "project_contract_collateral_bank_and_address": " ".join( - [ - apartment.project_contract_depositary or "", - apartment.project_contract_repository or "", - ] - ), - } - # override language to Finnish, as the user's browser settings etc. - # shouldn't affect the printed out PDFs - # further info on how Django resolves language preference: - # https://docs.djangoproject.com/en/5.1/topics/i18n/translation/ - with translation.override("fi"): - payment_1_price = hitas_price(payment_1.value * 100) - payment_terms_rest_of_price = f"{payment_1.type.label}" - if payment_1.due_date: - due_date = payment_1.due_date.strftime("%d.%m.%Y") - payment_terms_rest_of_price += f" {due_date}" - - payment_terms_rest_of_price += f" {payment_1_price.formatted_number_string()} {payment_1_price.suffix}" # noqa: E501 - - # full apartment contract data is mostly the same fields but with some changes - full_apartment_contract_data = { - **contract_data, - "building_permit_applied_for": apartment.project_construction_permit_claim, - "buyer_has_paid_down_payment": "", - "credit_interest": "0,00%", - "debt_free_price_x_0_014": True, - "project_documents_delivered": apartment.project_documents_delivered, - "final_payment": "", - "guarantee": "", - "guarantee_attachment_exists": True, - "guarantee_attachment_not_exists": False, - "project_contract_collateral_type": apartment.project_contract_default_collateral, # noqa: E501 - "loan_share_and_sales_price": hitas_price(apartment.debt_free_sales_price), - "occupants_signatures": signing_buyers, - "other_contract_terms": apartment.project_contract_combined_terms, - "payment_terms_rest_of_price": payment_terms_rest_of_price, - "project_built_according_to_regulations": "", # noqa: E501 - "sales_price_paid": "", - "sales_price_paid_place_and_time": sales_price_paid_place_and_time, # noqa: E501 - "sales_price_paid_salesperson_signature": salesperson.profile_or_user_full_name, - "sales_price_x_0_02": False, - "other_space": "", - "other_space_area": "", - "salesperson_signature": salesperson.profile_or_user_full_name, - "transfer_of_shares": apartment.project_shares_transferred_when, - "transfer_of_posession": apartment.project_control_transferred_when, - "transfer_of_shares_confirmed": sales_price_paid_place_and_time, - "transfer_of_shares_signature": signing_buyers, - } +def create_hitas_contract_pdf( + reservation: ApartmentReservation, + sales_price_paid_place: str, + sales_price_paid_time: str, + salesperson: User, +) -> BytesIO: + apartment: ApartmentDocument = SafeAttributeObject( + get_apartment(reservation.apartment_uuid, include_project_fields=True) + ) - contract_dataclass = HitasContractPDFData + pdf_data = get_hitas_contract_pdf_data( + apartment=apartment, + reservation=reservation, + sales_price_paid_place=sales_price_paid_place, + sales_price_paid_time=sales_price_paid_time, + salesperson=salesperson + ) pdf_template_path = HITAS_CONTRACT_PDF_TEMPLATE_FILE_NAME + complete_apartment = bool(apartment.project_use_complete_contract) if complete_apartment: - contract_dataclass = HitasCompleteApartmentContractPDFData - contract_data = full_apartment_contract_data pdf_template_path = HITAS_COMPLETE_APARTMENT_CONTRACT_PDF_TEMPLATE_FILE_NAME - pdf_data = contract_dataclass(**contract_data) return create_hitas_contract_pdf_from_data(pdf_data, pdf_template_path) diff --git a/application_form/tests/pdf_utils.py b/application_form/tests/pdf_utils.py index 979453679..c8d35caf9 100644 --- a/application_form/tests/pdf_utils.py +++ b/application_form/tests/pdf_utils.py @@ -1,8 +1,19 @@ +from datetime import date import re import subprocess -from typing import List - +from typing import List, Union +from faker import Faker +from apartment.elastic.documents import ApartmentDocument +from apartment.enums import OwnershipType +from apartment.tests.factories import ApartmentDocumentFactory +from application_form.models.reservation import ApartmentReservation +from application_form.pdf.haso import HasoContractPDFData, get_haso_contract_pdf_data +from application_form.pdf.hitas import HitasCompleteApartmentContractPDFData, HitasContractPDFData, get_hitas_contract_pdf_data +from application_form.tests.factories import ApartmentReservationFactory +from invoicing.enums import InstallmentType +from invoicing.tests.factories import ApartmentInstallmentFactory import pytest +from users.tests.factories import UserFactory def assert_pdf_has_text(pdf: bytes, text: str) -> bool: @@ -51,3 +62,62 @@ def remove_pdf_id(pdf: bytes) -> bytes: Remove the /ID entry from the PDF file. """ return re.sub(rb"/ID\s+\[<[^]]+>\]", b"", pdf) + + +def set_up_contract_pdf_test_data( + ownership_type:Union[OwnershipType, None]=OwnershipType.HASO, + apartment: Union[ApartmentDocument, None]=None, + reservation: Union[ApartmentReservation, None]=None, + salesperson:Union[str, None]=None, + sales_price_paid_place:Union[str, None]=None, + sales_price_paid_time:Union[str, None]=None + ) -> Union[HitasContractPDFData, HitasCompleteApartmentContractPDFData, HasoContractPDFData]: # noqa: E501 + + faker = Faker() + if not apartment: + apartment = ApartmentDocumentFactory( + project_ownership_type=ownership_type.value + ) + + if not reservation: + reservation = ApartmentReservationFactory(apartment_uuid=apartment.uuid) + + installment_types = [ + InstallmentType.PAYMENT_1, + InstallmentType.PAYMENT_2, + InstallmentType.PAYMENT_3, + InstallmentType.PAYMENT_4, + InstallmentType.PAYMENT_5, + InstallmentType.PAYMENT_6, + InstallmentType.PAYMENT_7, + ] + for installment_type in installment_types: + ApartmentInstallmentFactory( + apartment_reservation=reservation, + value=100_000, + type=installment_type, + ) + pass + + if not salesperson: + salesperson = UserFactory() + + if not sales_price_paid_place: + sales_price_paid_place = faker.city() + + if not sales_price_paid_time: + sales_price_paid_time = f"{date.today():%d.%m.%Y}" + + func = { + OwnershipType.HASO: get_haso_contract_pdf_data, + OwnershipType.HITAS: get_hitas_contract_pdf_data + }[ownership_type] + + pdf_data = func( + reservation, + salesperson=salesperson, + sales_price_paid_place=sales_price_paid_place, + sales_price_paid_time=sales_price_paid_time, + ) + + return pdf_data diff --git a/application_form/tests/test_pdf_haso.py b/application_form/tests/test_pdf_haso.py index b744907eb..4a7aa2221 100644 --- a/application_form/tests/test_pdf_haso.py +++ b/application_form/tests/test_pdf_haso.py @@ -16,7 +16,7 @@ get_haso_contract_pdf_data, HasoContractPDFData, ) -from .pdf_utils import get_cleaned_pdf_texts, remove_pdf_id +from .pdf_utils import get_cleaned_pdf_texts, remove_pdf_id, set_up_contract_pdf_test_data # This variable should be normally False, but can be set temporarily to # True to override the expected test result PDF file. This is useful @@ -83,18 +83,22 @@ def setUp(self) -> None: def test_pdf_content_is_not_empty(self): assert self.pdf_content + @pytest.mark.django_db + def test_payment_recipient_field_goes_on_pdf(self): + + pass + @pytest.mark.django_db def test_salesperson_signing_info_is_formatted_correctly(self): """Assert that the chosen salesperson's name and signing time/place get passed correctly to the HASO contract PDF generation. Small test mainly for TDD purposes.""" - apt = ApartmentDocumentFactory(project_ownership_type=OwnershipType.HASO.value) - res = ApartmentReservationFactory(apartment_uuid=apt.uuid) + salesperson = UserFactory(first_name="Markku", last_name="Myyjä") paid_place = "Helsinki" paid_time = "10.9.2025" - pdf_data = get_haso_contract_pdf_data( - res, + + pdf_data = set_up_contract_pdf_test_data( salesperson=salesperson, sales_price_paid_place=paid_place, sales_price_paid_time=paid_time, diff --git a/application_form/tests/test_pdf_hitas.py b/application_form/tests/test_pdf_hitas.py index cba14ca6e..a5b2a71cb 100644 --- a/application_form/tests/test_pdf_hitas.py +++ b/application_form/tests/test_pdf_hitas.py @@ -5,6 +5,7 @@ from datetime import date, timedelta from decimal import Decimal +from application_form.tests.factories import ApartmentReservationFactory import pytest from django.contrib.auth import get_user_model @@ -26,7 +27,7 @@ HitasCompleteApartmentContractPDFData, HitasContractPDFData, ) -from .pdf_utils import get_cleaned_pdf_texts, remove_pdf_id +from .pdf_utils import get_cleaned_pdf_texts, remove_pdf_id, set_up_contract_pdf_test_data # This variable should be normally False, but can be set temporarily to # True to override the expected test result PDF file. This is useful @@ -250,6 +251,7 @@ def test_pdf_content_without_id_is_expected(self): # printed in the test output. assert False, "Invalid PDF content" + def test_pdf_content_is_correct(self): # regression test @@ -625,6 +627,7 @@ def setUp(self) -> None: def test_pdf_content_is_not_empty(self): assert self.pdf_content + def test_pdf_content_text_is_correct(self): # acquire a new version of this PDF array by running # python manage.py pdf_as_array application_form/tests/hitas_contract_test_result.pdf # noqa: E501 diff --git a/application_form/tests/utils.py b/application_form/tests/utils.py index 10a3cf114..b55504d64 100644 --- a/application_form/tests/utils.py +++ b/application_form/tests/utils.py @@ -5,7 +5,6 @@ from typing import List, Tuple from django.utils import timezone - from apartment.elastic.documents import ApartmentDocument from apartment.elastic.queries import apartment_query from connections.enums import ApartmentStateOfSale diff --git a/invoicing/pdf.py b/invoicing/pdf.py index 309ddf175..9170995ca 100644 --- a/invoicing/pdf.py +++ b/invoicing/pdf.py @@ -12,8 +12,10 @@ from apartment.elastic.documents import ApartmentDocument from apartment.elastic.queries import get_apartment, get_project +from apartment.enums import OwnershipType from apartment_application_service.pdf import create_pdf, PDFData from customer.models import Customer +from invoicing.enums import InstallmentType from invoicing.models import ApartmentInstallment _logger = logging.getLogger(__name__) @@ -53,20 +55,18 @@ class InvoicePDFData(PDFData): "apartment": "Huoneisto", } +def get_invoice_pdf_data_from_installment( + installment: ApartmentInstallment, + ) -> InvoicePDFData: + @lru_cache + def get_cached_project(project_uuid: UUID): + return get_project(project_uuid) -def create_invoice_pdf_from_installments( - installments: Union[QuerySet, List[ApartmentInstallment]] -): - @lru_cache - def get_cached_project(project_uuid: UUID): - return get_project(project_uuid) + @lru_cache + def get_cached_apartment(apartment_uuid: UUID) -> ApartmentDocument: + return get_apartment(apartment_uuid, include_project_fields=True) - @lru_cache - def get_cached_apartment(apartment_uuid: UUID) -> ApartmentDocument: - return get_apartment(apartment_uuid, include_project_fields=True) - invoice_pdf_data_list = [] - for installment in installments: reservation = installment.apartment_reservation payer_name_and_address = _get_payer_name_and_address( installment.apartment_reservation.customer @@ -85,8 +85,18 @@ def get_cached_apartment(apartment_uuid: UUID) -> ApartmentDocument: + " €" ) + final_installment_type = InstallmentType.PAYMENT_7 + if apartment.project_ownership_type == OwnershipType.HASO.value: + final_installment_type = InstallmentType.RIGHT_OF_OCCUPANCY_PAYMENT_3 + pass + + payment_recipient = apartment.project_payment_recipient + if installment.type == final_installment_type: + payment_recipient = apartment.project_payment_recipient_final + pass + invoice_pdf_data = InvoicePDFData( - recipient=project.project_housing_company, + recipient=payment_recipient, recipient_account_number=f"{project.project_contract_rs_bank or ''} " f"{installment.account_number}".strip(), payer_name_and_address=payer_name_and_address, @@ -95,5 +105,16 @@ def get_cached_apartment(apartment_uuid: UUID) -> ApartmentDocument: amount=installment.value, apartment=apartment_text, ) + + return invoice_pdf_data + +def create_invoice_pdf_from_installments( + installments: Union[QuerySet, List[ApartmentInstallment]] +): + + invoice_pdf_data_list = [] + for installment in installments: + invoice_pdf_data = get_invoice_pdf_data_from_installment(installment) invoice_pdf_data_list.append(invoice_pdf_data) + return create_pdf(INVOICE_PDF_TEMPLATE_FILE_NAME, invoice_pdf_data_list) diff --git a/invoicing/tests/test_pdf.py b/invoicing/tests/test_pdf.py index 50e93128b..294be21d9 100644 --- a/invoicing/tests/test_pdf.py +++ b/invoicing/tests/test_pdf.py @@ -1,10 +1,54 @@ +from apartment.enums import OwnershipType +from apartment.tests.factories import ApartmentDocumentFactory +from application_form.tests.factories import ApartmentReservationFactory +from invoicing.enums import InstallmentType +from invoicing.tests.factories import ApartmentInstallmentFactory import pytest from django.utils.translation import gettext_lazy as _ from customer.tests.factories import CustomerFactory -from invoicing.pdf import _get_payer_name_and_address +from invoicing.pdf import _get_payer_name_and_address, get_invoice_pdf_data_from_installment from users.tests.factories import ProfileFactory +@pytest.mark.django_db +@pytest.mark.parametrize( + "ownership_type", (OwnershipType.HASO, OwnershipType.HITAS) +) +def test_pdf_payment_recipients_set_correctly(ownership_type): + apartment = ApartmentDocumentFactory( + project_ownership_type=ownership_type.value, + project_payment_recipient="Payment recipient", + project_payment_recipient_final="Final payment recipient", + ) + reservation = ApartmentReservationFactory(apartment_uuid=apartment.uuid) + + first_installment_type = InstallmentType.PAYMENT_1 + final_installment_type = InstallmentType.PAYMENT_7 + + if ownership_type == OwnershipType.HASO: + first_installment_type = InstallmentType.RIGHT_OF_OCCUPANCY_PAYMENT + final_installment_type = InstallmentType.RIGHT_OF_OCCUPANCY_PAYMENT_3 + + first_installment = ApartmentInstallmentFactory( + apartment_reservation=reservation, + value=100_000, + type=first_installment_type, + ) + + final_installment = ApartmentInstallmentFactory( + apartment_reservation=reservation, + value=100_000, + type=final_installment_type, + ) + + + first_installment_pdf_data = get_invoice_pdf_data_from_installment(first_installment) + final_installment_pdf_data = get_invoice_pdf_data_from_installment(final_installment) + + assert first_installment_pdf_data.recipient == apartment.project_payment_recipient + assert final_installment_pdf_data.recipient == apartment.project_payment_recipient_final # noqa: E501 + + pass @pytest.mark.django_db def test_pdf_payer_name_address_correct(): From 00d70407a00a780db79cf032f5d812d51292352e Mon Sep 17 00:00:00 2001 From: leevi-identio Date: Thu, 23 Oct 2025 12:25:32 +0300 Subject: [PATCH 2/3] Fix lint errs --- application_form/pdf/hitas.py | 470 +++++++++++------------ application_form/tests/pdf_utils.py | 116 +++--- application_form/tests/test_pdf_haso.py | 12 +- application_form/tests/test_pdf_hitas.py | 8 +- invoicing/pdf.py | 97 ++--- invoicing/tests/test_pdf.py | 27 +- 6 files changed, 373 insertions(+), 357 deletions(-) diff --git a/application_form/pdf/hitas.py b/application_form/pdf/hitas.py index 77f1bf2cf..5ddfbca97 100644 --- a/application_form/pdf/hitas.py +++ b/application_form/pdf/hitas.py @@ -391,252 +391,250 @@ class HitasContractPDFData(PDFData): def get_hitas_contract_pdf_data( - apartment: ApartmentDocument, - reservation: ApartmentReservation, - sales_price_paid_place: str, - sales_price_paid_time: str, - salesperson: User, - ) -> Union[HitasContractPDFData, HitasCompleteApartmentContractPDFData]: - customer = SafeAttributeObject(reservation.customer) - primary_profile = SafeAttributeObject(customer.primary_profile) - secondary_profile = SafeAttributeObject(customer.secondary_profile) - - # use contract for complete apartment - # can possibly be None, use bool() to convert to False in that case - complete_apartment = bool(apartment.project_use_complete_contract) + apartment: ApartmentDocument, + reservation: ApartmentReservation, + sales_price_paid_place: str, + sales_price_paid_time: str, + salesperson: User, +) -> Union[HitasContractPDFData, HitasCompleteApartmentContractPDFData]: + customer = SafeAttributeObject(reservation.customer) + primary_profile = SafeAttributeObject(customer.primary_profile) + secondary_profile = SafeAttributeObject(customer.secondary_profile) - ( - payment_1, - payment_2, - payment_3, - payment_4, - payment_5, - payment_6, - payment_7, - ) = _get_numbered_installments(apartment, reservation) - - down_payment = SafeAttributeObject( - reservation.apartment_installments.filter( - type=InstallmentType.DOWN_PAYMENT - ).first() - ) + # use contract for complete apartment + # can possibly be None, use bool() to convert to False in that case + complete_apartment = bool(apartment.project_use_complete_contract) - sales_price_paid_place_and_time = ( - f"{sales_price_paid_place} {sales_price_paid_time}" - ) + ( + payment_1, + payment_2, + payment_3, + payment_4, + payment_5, + payment_6, + payment_7, + ) = _get_numbered_installments(apartment, reservation) + + down_payment = SafeAttributeObject( + reservation.apartment_installments.filter( + type=InstallmentType.DOWN_PAYMENT + ).first() + ) - def hitas_price(cents: Union[int, None]) -> Union[PDFCurrencyField, None]: - """Turns the price in cents to whole euros (division by 100). Outputs - a PDFCurrencyField prefilled with a string that has the euro sum - as words (in Finnish) and as numbers. + sales_price_paid_place_and_time = ( + f"{sales_price_paid_place} {sales_price_paid_time}" + ) - e.g. - 12000 -> "tuhat kaksisataa 1200 €" + def hitas_price(cents: Union[int, None]) -> Union[PDFCurrencyField, None]: + """Turns the price in cents to whole euros (division by 100). Outputs + a PDFCurrencyField prefilled with a string that has the euro sum + as words (in Finnish) and as numbers. - 11000000 -> "satakymmenentuhatta 110000 €" + e.g. + 12000 -> "tuhat kaksisataa 1200 €" - 31115224 -> "kolmekymmentäyksimiljoonaa sataviisitoistatuhatta - kaksisataakaksikymmentäneljä 31115224 €" + 11000000 -> "satakymmenentuhatta 110000 €" - Args: - cents (Union[int, None]): The sum in cents + 31115224 -> "kolmekymmentäyksimiljoonaa sataviisitoistatuhatta + kaksisataakaksikymmentäneljä 31115224 €" - Returns: Union[PDFCurrencyField, None]: - """ - if cents is None: - return None - return PDFCurrencyField( - prefix=num2words(Decimal(cents) / 100, lang="fi") + " ", - cents=cents, - suffix=" €", - ) + Args: + cents (Union[int, None]): The sum in cents - signing_buyers = " & ".join( - name - for name in [primary_profile.full_name, secondary_profile.full_name] - if name + Returns: Union[PDFCurrencyField, None]: + """ + if cents is None: + return None + return PDFCurrencyField( + prefix=num2words(Decimal(cents) / 100, lang="fi") + " ", + cents=cents, + suffix=" €", ) - contract_data = { - "occupant_1": primary_profile.full_name, - "occupant_1_share_of_ownership": None, - "occupant_1_address": ( - (primary_profile.street_address or "") - + ", " - + (primary_profile.postal_code or "") - + " " - + (primary_profile.city or "") - ).strip(), - "occupant_1_phone_number": primary_profile.phone_number, - "occupant_1_email": primary_profile.email, - "occupant_1_ssn_or_business_id": primary_profile.national_identification_number, - "occupant_2": secondary_profile.full_name, - "occupant_2_share_of_ownership": None, - "occupant_2_address": ( - (secondary_profile.street_address or "") - + ", " - + (secondary_profile.postal_code or "") - + " " - + (secondary_profile.city or "") - ).strip(), - "occupant_2_phone_number": secondary_profile.phone_number, - "occupant_2_email": secondary_profile.email, - "occupant_2_ssn_or_business_id": secondary_profile.national_identification_number, # noqa: E501 - "project_housing_company": apartment.project_housing_company, - "project_contract_business_id": apartment.project_contract_business_id, - "project_address": " ".join( - [ - apartment.project_street_address, - f"{apartment.project_postal_code} {apartment.project_city}", - ] - ), - "project_realty_id": apartment.project_realty_id, - "housing_type_ownership": False, - "housing_type_rental": True, - "housing_shares": f"{apartment.stock_start_number or ''} - {apartment.stock_end_number or ''}", # noqa: E501 - "apartment_street_address": None, - "apartment_structure": apartment.apartment_structure, - "apartment_number": apartment.apartment_number, - "floor": apartment.floor, - "living_area": apartment.living_area, - "other_space": None, - "other_space_area": None, - "project_contract_transfer_restriction_false": apartment.project_contract_transfer_restriction # noqa E501 - is False, - "project_contract_transfer_restriction_true": apartment.project_contract_transfer_restriction, # noqa E501 - "project_contract_transfer_restriction_text": apartment.project_contract_article_of_association, # noqa E501 - "project_contract_material_selection_later_false": apartment.project_contract_material_selection_later # noqa E501 - is False, - "project_contract_material_selection_later_true": apartment.project_contract_material_selection_later, # noqa E501 - "project_contract_material_selection_description": apartment.project_contract_material_selection_description, # noqa E501 - "project_contract_material_selection_date": apartment.project_contract_material_selection_date, # noqa E501 - "sales_price": hitas_price(apartment.sales_price), - "loan_share": hitas_price(apartment.loan_share), - "debt_free_sales_price": hitas_price(apartment.debt_free_sales_price), - "payment_1_label": payment_1.type, - "payment_1_amount": PDFCurrencyField(euros=payment_1.value), - "payment_1_due_date": payment_1.due_date, - "payment_1_percentage": payment_1._percentage, - "payment_2_label": payment_2.type, - "payment_2_amount": PDFCurrencyField(euros=payment_2.value), - "payment_2_due_date": payment_2.due_date, - "payment_2_percentage": payment_2._percentage, - "payment_3_label": payment_3.type, - "payment_3_amount": PDFCurrencyField(euros=payment_3.value), - "payment_3_due_date": payment_3.due_date, - "payment_3_percentage": payment_3._percentage, - "payment_4_label": payment_4.type, - "payment_4_amount": PDFCurrencyField(euros=payment_4.value), - "payment_4_due_date": payment_4.due_date, - "payment_4_percentage": payment_4._percentage, - "payment_5_label": payment_5.type, - "payment_5_amount": PDFCurrencyField(euros=payment_5.value), - "payment_5_due_date": payment_5.due_date, - "payment_5_percentage": payment_5._percentage, - "second_last_payment_label": "6", - "second_last_payment_basis_sales_price": False, - "second_last_payment_basis_debt_free_sales_price": True, - "second_last_payment_dfsp_percentage": payment_6._percentage, - "second_last_payment_dfsp_amount": PDFCurrencyField(euros=payment_6.value), - "last_payment_label": "7", - "last_payment_basis_sales_price": False, - "last_payment_basis_debt_free_sales_price": True, - "last_payment_dfsp_percentage": payment_7._percentage, - "last_payment_dfsp_amount": PDFCurrencyField(euros=payment_7.value), - "payment_bank_1": apartment.project_contract_depositary, - "payment_account_number_1": apartment.project_regular_bank_account, - "payment_bank_2": apartment.project_contract_depositary, - "payment_account_number_2": apartment.project_barred_bank_account, - "down_payment_amount": PDFCurrencyField( - euros=down_payment.amount if down_payment.amount else Decimal(0), - suffix=" €", - ), - "project_contract_apartment_completion_selection_1": apartment.project_contract_apartment_completion_selection_1, # noqa E501 - "project_contract_apartment_completion_selection_1_date": apartment.project_contract_apartment_completion_selection_1_date, # noqa E501 - "project_contract_apartment_completion_selection_2": apartment.project_contract_apartment_completion_selection_2, # noqa E501 - "project_contract_apartment_completion_selection_2_start": apartment.project_contract_apartment_completion_selection_2_start, # noqa E501 - "project_contract_apartment_completion_selection_2_end": apartment.project_contract_apartment_completion_selection_2_end, # noqa E501 - "project_contract_apartment_completion_selection_3": apartment.project_contract_apartment_completion_selection_3, # noqa E501 - "project_contract_apartment_completion_selection_3_date": apartment.project_contract_apartment_completion_selection_3_date, # noqa E501 - "project_contract_depositary": apartment.project_contract_depositary, - "project_contract_repository": apartment.project_contract_repository, - "breach_of_contract_option_1": False, - "breach_of_contract_option_2": True, - "project_contract_collateral_type": apartment.project_contract_collateral_type, - "project_contract_default_collateral": apartment.project_contract_default_collateral, # noqa E501 - "project_contract_construction_permit_requested": ( - (apartment.project_contract_construction_permit_requested) - if apartment.project_contract_construction_permit_requested - else None - ), - "project_contract_other_terms": apartment.project_contract_combined_terms, - "project_documents_delivered": apartment.project_documents_delivered, - "signing_place_and_time": sales_price_paid_place_and_time, - "salesperson": salesperson.profile_or_user_full_name, - "signing_buyers": signing_buyers, - "project_contract_collateral_bank_and_address": " ".join( - [ - apartment.project_contract_depositary or "", - apartment.project_contract_repository or "", - ] - ), - } - - # override language to Finnish, as the user's browser settings etc. - # shouldn't affect the printed out PDFs - # further info on how Django resolves language preference: - # https://docs.djangoproject.com/en/5.1/topics/i18n/translation/ - with translation.override("fi"): - payment_1_price = hitas_price(payment_1.value * 100) - payment_terms_rest_of_price = f"{payment_1.type.label}" - if payment_1.due_date: - due_date = payment_1.due_date.strftime("%d.%m.%Y") - payment_terms_rest_of_price += f" {due_date}" - - payment_terms_rest_of_price += f" {payment_1_price.formatted_number_string()} {payment_1_price.suffix}" # noqa: E501 - - # full apartment contract data is mostly the same fields but with some changes - full_apartment_contract_data = { - **contract_data, - "building_permit_applied_for": apartment.project_construction_permit_claim, - "buyer_has_paid_down_payment": "", - "credit_interest": "0,00%", - "debt_free_price_x_0_014": True, - "project_documents_delivered": apartment.project_documents_delivered, - "final_payment": "", - "guarantee": "", - "guarantee_attachment_exists": True, - "guarantee_attachment_not_exists": False, - "project_contract_collateral_type": apartment.project_contract_default_collateral, # noqa: E501 - "loan_share_and_sales_price": hitas_price(apartment.debt_free_sales_price), - "occupants_signatures": signing_buyers, - "other_contract_terms": apartment.project_contract_combined_terms, - "payment_terms_rest_of_price": payment_terms_rest_of_price, - "project_built_according_to_regulations": "", # noqa: E501 - "sales_price_paid": "", - "sales_price_paid_place_and_time": sales_price_paid_place_and_time, # noqa: E501 - "sales_price_paid_salesperson_signature": salesperson.profile_or_user_full_name, - "sales_price_x_0_02": False, - "other_space": "", - "other_space_area": "", - "salesperson_signature": salesperson.profile_or_user_full_name, - "transfer_of_shares": apartment.project_shares_transferred_when, - "transfer_of_posession": apartment.project_control_transferred_when, - "transfer_of_shares_confirmed": sales_price_paid_place_and_time, - "transfer_of_shares_signature": signing_buyers, - } - - contract_dataclass = HitasContractPDFData - pdf_template_path = HITAS_CONTRACT_PDF_TEMPLATE_FILE_NAME - - if complete_apartment: - contract_dataclass = HitasCompleteApartmentContractPDFData - contract_data = full_apartment_contract_data - pdf_template_path = HITAS_COMPLETE_APARTMENT_CONTRACT_PDF_TEMPLATE_FILE_NAME - - pdf_data = contract_dataclass(**contract_data) - - return pdf_data + signing_buyers = " & ".join( + name + for name in [primary_profile.full_name, secondary_profile.full_name] + if name + ) + + contract_data = { + "occupant_1": primary_profile.full_name, + "occupant_1_share_of_ownership": None, + "occupant_1_address": ( + (primary_profile.street_address or "") + + ", " + + (primary_profile.postal_code or "") + + " " + + (primary_profile.city or "") + ).strip(), + "occupant_1_phone_number": primary_profile.phone_number, + "occupant_1_email": primary_profile.email, + "occupant_1_ssn_or_business_id": primary_profile.national_identification_number, + "occupant_2": secondary_profile.full_name, + "occupant_2_share_of_ownership": None, + "occupant_2_address": ( + (secondary_profile.street_address or "") + + ", " + + (secondary_profile.postal_code or "") + + " " + + (secondary_profile.city or "") + ).strip(), + "occupant_2_phone_number": secondary_profile.phone_number, + "occupant_2_email": secondary_profile.email, + "occupant_2_ssn_or_business_id": secondary_profile.national_identification_number, # noqa: E501 + "project_housing_company": apartment.project_housing_company, + "project_contract_business_id": apartment.project_contract_business_id, + "project_address": " ".join( + [ + apartment.project_street_address, + f"{apartment.project_postal_code} {apartment.project_city}", + ] + ), + "project_realty_id": apartment.project_realty_id, + "housing_type_ownership": False, + "housing_type_rental": True, + "housing_shares": f"{apartment.stock_start_number or ''} - {apartment.stock_end_number or ''}", # noqa: E501 + "apartment_street_address": None, + "apartment_structure": apartment.apartment_structure, + "apartment_number": apartment.apartment_number, + "floor": apartment.floor, + "living_area": apartment.living_area, + "other_space": None, + "other_space_area": None, + "project_contract_transfer_restriction_false": apartment.project_contract_transfer_restriction # noqa E501 + is False, + "project_contract_transfer_restriction_true": apartment.project_contract_transfer_restriction, # noqa E501 + "project_contract_transfer_restriction_text": apartment.project_contract_article_of_association, # noqa E501 + "project_contract_material_selection_later_false": apartment.project_contract_material_selection_later # noqa E501 + is False, + "project_contract_material_selection_later_true": apartment.project_contract_material_selection_later, # noqa E501 + "project_contract_material_selection_description": apartment.project_contract_material_selection_description, # noqa E501 + "project_contract_material_selection_date": apartment.project_contract_material_selection_date, # noqa E501 + "sales_price": hitas_price(apartment.sales_price), + "loan_share": hitas_price(apartment.loan_share), + "debt_free_sales_price": hitas_price(apartment.debt_free_sales_price), + "payment_1_label": payment_1.type, + "payment_1_amount": PDFCurrencyField(euros=payment_1.value), + "payment_1_due_date": payment_1.due_date, + "payment_1_percentage": payment_1._percentage, + "payment_2_label": payment_2.type, + "payment_2_amount": PDFCurrencyField(euros=payment_2.value), + "payment_2_due_date": payment_2.due_date, + "payment_2_percentage": payment_2._percentage, + "payment_3_label": payment_3.type, + "payment_3_amount": PDFCurrencyField(euros=payment_3.value), + "payment_3_due_date": payment_3.due_date, + "payment_3_percentage": payment_3._percentage, + "payment_4_label": payment_4.type, + "payment_4_amount": PDFCurrencyField(euros=payment_4.value), + "payment_4_due_date": payment_4.due_date, + "payment_4_percentage": payment_4._percentage, + "payment_5_label": payment_5.type, + "payment_5_amount": PDFCurrencyField(euros=payment_5.value), + "payment_5_due_date": payment_5.due_date, + "payment_5_percentage": payment_5._percentage, + "second_last_payment_label": "6", + "second_last_payment_basis_sales_price": False, + "second_last_payment_basis_debt_free_sales_price": True, + "second_last_payment_dfsp_percentage": payment_6._percentage, + "second_last_payment_dfsp_amount": PDFCurrencyField(euros=payment_6.value), + "last_payment_label": "7", + "last_payment_basis_sales_price": False, + "last_payment_basis_debt_free_sales_price": True, + "last_payment_dfsp_percentage": payment_7._percentage, + "last_payment_dfsp_amount": PDFCurrencyField(euros=payment_7.value), + "payment_bank_1": apartment.project_contract_depositary, + "payment_account_number_1": apartment.project_regular_bank_account, + "payment_bank_2": apartment.project_contract_depositary, + "payment_account_number_2": apartment.project_barred_bank_account, + "down_payment_amount": PDFCurrencyField( + euros=down_payment.amount if down_payment.amount else Decimal(0), + suffix=" €", + ), + "project_contract_apartment_completion_selection_1": apartment.project_contract_apartment_completion_selection_1, # noqa E501 + "project_contract_apartment_completion_selection_1_date": apartment.project_contract_apartment_completion_selection_1_date, # noqa E501 + "project_contract_apartment_completion_selection_2": apartment.project_contract_apartment_completion_selection_2, # noqa E501 + "project_contract_apartment_completion_selection_2_start": apartment.project_contract_apartment_completion_selection_2_start, # noqa E501 + "project_contract_apartment_completion_selection_2_end": apartment.project_contract_apartment_completion_selection_2_end, # noqa E501 + "project_contract_apartment_completion_selection_3": apartment.project_contract_apartment_completion_selection_3, # noqa E501 + "project_contract_apartment_completion_selection_3_date": apartment.project_contract_apartment_completion_selection_3_date, # noqa E501 + "project_contract_depositary": apartment.project_contract_depositary, + "project_contract_repository": apartment.project_contract_repository, + "breach_of_contract_option_1": False, + "breach_of_contract_option_2": True, + "project_contract_collateral_type": apartment.project_contract_collateral_type, + "project_contract_default_collateral": apartment.project_contract_default_collateral, # noqa E501 + "project_contract_construction_permit_requested": ( + (apartment.project_contract_construction_permit_requested) + if apartment.project_contract_construction_permit_requested + else None + ), + "project_contract_other_terms": apartment.project_contract_combined_terms, + "project_documents_delivered": apartment.project_documents_delivered, + "signing_place_and_time": sales_price_paid_place_and_time, + "salesperson": salesperson.profile_or_user_full_name, + "signing_buyers": signing_buyers, + "project_contract_collateral_bank_and_address": " ".join( + [ + apartment.project_contract_depositary or "", + apartment.project_contract_repository or "", + ] + ), + } + + # override language to Finnish, as the user's browser settings etc. + # shouldn't affect the printed out PDFs + # further info on how Django resolves language preference: + # https://docs.djangoproject.com/en/5.1/topics/i18n/translation/ + with translation.override("fi"): + payment_1_price = hitas_price(payment_1.value * 100) + payment_terms_rest_of_price = f"{payment_1.type.label}" + if payment_1.due_date: + due_date = payment_1.due_date.strftime("%d.%m.%Y") + payment_terms_rest_of_price += f" {due_date}" + + payment_terms_rest_of_price += f" {payment_1_price.formatted_number_string()} {payment_1_price.suffix}" # noqa: E501 + + # full apartment contract data is mostly the same fields but with some changes + full_apartment_contract_data = { + **contract_data, + "building_permit_applied_for": apartment.project_construction_permit_claim, + "buyer_has_paid_down_payment": "", + "credit_interest": "0,00%", + "debt_free_price_x_0_014": True, + "project_documents_delivered": apartment.project_documents_delivered, + "final_payment": "", + "guarantee": "", + "guarantee_attachment_exists": True, + "guarantee_attachment_not_exists": False, + "project_contract_collateral_type": apartment.project_contract_default_collateral, # noqa: E501 + "loan_share_and_sales_price": hitas_price(apartment.debt_free_sales_price), + "occupants_signatures": signing_buyers, + "other_contract_terms": apartment.project_contract_combined_terms, + "payment_terms_rest_of_price": payment_terms_rest_of_price, + "project_built_according_to_regulations": "", # noqa: E501 + "sales_price_paid": "", + "sales_price_paid_place_and_time": sales_price_paid_place_and_time, # noqa: E501 + "sales_price_paid_salesperson_signature": salesperson.profile_or_user_full_name, + "sales_price_x_0_02": False, + "other_space": "", + "other_space_area": "", + "salesperson_signature": salesperson.profile_or_user_full_name, + "transfer_of_shares": apartment.project_shares_transferred_when, + "transfer_of_posession": apartment.project_control_transferred_when, + "transfer_of_shares_confirmed": sales_price_paid_place_and_time, + "transfer_of_shares_signature": signing_buyers, + } + + contract_dataclass = HitasContractPDFData + + if complete_apartment: + contract_dataclass = HitasCompleteApartmentContractPDFData + contract_data = full_apartment_contract_data + + pdf_data = contract_dataclass(**contract_data) + + return pdf_data def create_hitas_contract_pdf( @@ -654,7 +652,7 @@ def create_hitas_contract_pdf( reservation=reservation, sales_price_paid_place=sales_price_paid_place, sales_price_paid_time=sales_price_paid_time, - salesperson=salesperson + salesperson=salesperson, ) pdf_template_path = HITAS_CONTRACT_PDF_TEMPLATE_FILE_NAME complete_apartment = bool(apartment.project_use_complete_contract) diff --git a/application_form/tests/pdf_utils.py b/application_form/tests/pdf_utils.py index c8d35caf9..48485c67e 100644 --- a/application_form/tests/pdf_utils.py +++ b/application_form/tests/pdf_utils.py @@ -8,7 +8,11 @@ from apartment.tests.factories import ApartmentDocumentFactory from application_form.models.reservation import ApartmentReservation from application_form.pdf.haso import HasoContractPDFData, get_haso_contract_pdf_data -from application_form.pdf.hitas import HitasCompleteApartmentContractPDFData, HitasContractPDFData, get_hitas_contract_pdf_data +from application_form.pdf.hitas import ( + HitasCompleteApartmentContractPDFData, + HitasContractPDFData, + get_hitas_contract_pdf_data, +) from application_form.tests.factories import ApartmentReservationFactory from invoicing.enums import InstallmentType from invoicing.tests.factories import ApartmentInstallmentFactory @@ -65,59 +69,61 @@ def remove_pdf_id(pdf: bytes) -> bytes: def set_up_contract_pdf_test_data( - ownership_type:Union[OwnershipType, None]=OwnershipType.HASO, - apartment: Union[ApartmentDocument, None]=None, - reservation: Union[ApartmentReservation, None]=None, - salesperson:Union[str, None]=None, - sales_price_paid_place:Union[str, None]=None, - sales_price_paid_time:Union[str, None]=None - ) -> Union[HitasContractPDFData, HitasCompleteApartmentContractPDFData, HasoContractPDFData]: # noqa: E501 - - faker = Faker() - if not apartment: - apartment = ApartmentDocumentFactory( - project_ownership_type=ownership_type.value - ) - - if not reservation: - reservation = ApartmentReservationFactory(apartment_uuid=apartment.uuid) - - installment_types = [ - InstallmentType.PAYMENT_1, - InstallmentType.PAYMENT_2, - InstallmentType.PAYMENT_3, - InstallmentType.PAYMENT_4, - InstallmentType.PAYMENT_5, - InstallmentType.PAYMENT_6, - InstallmentType.PAYMENT_7, - ] - for installment_type in installment_types: - ApartmentInstallmentFactory( - apartment_reservation=reservation, - value=100_000, - type=installment_type, - ) - pass - - if not salesperson: - salesperson = UserFactory() - - if not sales_price_paid_place: - sales_price_paid_place = faker.city() - - if not sales_price_paid_time: - sales_price_paid_time = f"{date.today():%d.%m.%Y}" - - func = { - OwnershipType.HASO: get_haso_contract_pdf_data, - OwnershipType.HITAS: get_hitas_contract_pdf_data - }[ownership_type] - - pdf_data = func( - reservation, - salesperson=salesperson, - sales_price_paid_place=sales_price_paid_place, - sales_price_paid_time=sales_price_paid_time, + ownership_type: Union[OwnershipType, None] = OwnershipType.HASO, + apartment: Union[ApartmentDocument, None] = None, + reservation: Union[ApartmentReservation, None] = None, + salesperson: Union[str, None] = None, + sales_price_paid_place: Union[str, None] = None, + sales_price_paid_time: Union[str, None] = None, +) -> Union[ + HitasContractPDFData, HitasCompleteApartmentContractPDFData, HasoContractPDFData +]: # noqa: E501 + + faker = Faker() + if not apartment: + apartment = ApartmentDocumentFactory( + project_ownership_type=ownership_type.value ) - return pdf_data + if not reservation: + reservation = ApartmentReservationFactory(apartment_uuid=apartment.uuid) + + installment_types = [ + InstallmentType.PAYMENT_1, + InstallmentType.PAYMENT_2, + InstallmentType.PAYMENT_3, + InstallmentType.PAYMENT_4, + InstallmentType.PAYMENT_5, + InstallmentType.PAYMENT_6, + InstallmentType.PAYMENT_7, + ] + for installment_type in installment_types: + ApartmentInstallmentFactory( + apartment_reservation=reservation, + value=100_000, + type=installment_type, + ) + pass + + if not salesperson: + salesperson = UserFactory() + + if not sales_price_paid_place: + sales_price_paid_place = faker.city() + + if not sales_price_paid_time: + sales_price_paid_time = f"{date.today():%d.%m.%Y}" + + func = { + OwnershipType.HASO: get_haso_contract_pdf_data, + OwnershipType.HITAS: get_hitas_contract_pdf_data, + }[ownership_type] + + pdf_data = func( + reservation, + salesperson=salesperson, + sales_price_paid_place=sales_price_paid_place, + sales_price_paid_time=sales_price_paid_time, + ) + + return pdf_data diff --git a/application_form/tests/test_pdf_haso.py b/application_form/tests/test_pdf_haso.py index 4a7aa2221..55201b439 100644 --- a/application_form/tests/test_pdf_haso.py +++ b/application_form/tests/test_pdf_haso.py @@ -5,18 +5,18 @@ import pytest -from apartment.enums import OwnershipType -from apartment.tests.factories import ApartmentDocumentFactory from apartment_application_service.pdf import PDFCurrencyField as CF -from application_form.tests.factories import ApartmentReservationFactory from users.tests.factories import UserFactory from ..pdf.haso import ( create_haso_contract_pdf_from_data, - get_haso_contract_pdf_data, HasoContractPDFData, ) -from .pdf_utils import get_cleaned_pdf_texts, remove_pdf_id, set_up_contract_pdf_test_data +from .pdf_utils import ( + get_cleaned_pdf_texts, + remove_pdf_id, + set_up_contract_pdf_test_data, +) # This variable should be normally False, but can be set temporarily to # True to override the expected test result PDF file. This is useful @@ -85,7 +85,7 @@ def test_pdf_content_is_not_empty(self): @pytest.mark.django_db def test_payment_recipient_field_goes_on_pdf(self): - + pass @pytest.mark.django_db diff --git a/application_form/tests/test_pdf_hitas.py b/application_form/tests/test_pdf_hitas.py index a5b2a71cb..f7c135310 100644 --- a/application_form/tests/test_pdf_hitas.py +++ b/application_form/tests/test_pdf_hitas.py @@ -5,7 +5,6 @@ from datetime import date, timedelta from decimal import Decimal -from application_form.tests.factories import ApartmentReservationFactory import pytest from django.contrib.auth import get_user_model @@ -27,7 +26,10 @@ HitasCompleteApartmentContractPDFData, HitasContractPDFData, ) -from .pdf_utils import get_cleaned_pdf_texts, remove_pdf_id, set_up_contract_pdf_test_data +from .pdf_utils import ( + get_cleaned_pdf_texts, + remove_pdf_id, +) # This variable should be normally False, but can be set temporarily to # True to override the expected test result PDF file. This is useful @@ -251,7 +253,6 @@ def test_pdf_content_without_id_is_expected(self): # printed in the test output. assert False, "Invalid PDF content" - def test_pdf_content_is_correct(self): # regression test @@ -627,7 +628,6 @@ def setUp(self) -> None: def test_pdf_content_is_not_empty(self): assert self.pdf_content - def test_pdf_content_text_is_correct(self): # acquire a new version of this PDF array by running # python manage.py pdf_as_array application_form/tests/hitas_contract_test_result.pdf # noqa: E501 diff --git a/invoicing/pdf.py b/invoicing/pdf.py index 9170995ca..562a19ce0 100644 --- a/invoicing/pdf.py +++ b/invoicing/pdf.py @@ -55,58 +55,59 @@ class InvoicePDFData(PDFData): "apartment": "Huoneisto", } -def get_invoice_pdf_data_from_installment( - installment: ApartmentInstallment, - ) -> InvoicePDFData: - @lru_cache - def get_cached_project(project_uuid: UUID): - return get_project(project_uuid) - @lru_cache - def get_cached_apartment(apartment_uuid: UUID) -> ApartmentDocument: - return get_apartment(apartment_uuid, include_project_fields=True) +def get_invoice_pdf_data_from_installment( + installment: ApartmentInstallment, +) -> InvoicePDFData: + @lru_cache + def get_cached_project(project_uuid: UUID): + return get_project(project_uuid) + + @lru_cache + def get_cached_apartment(apartment_uuid: UUID) -> ApartmentDocument: + return get_apartment(apartment_uuid, include_project_fields=True) + + reservation = installment.apartment_reservation + payer_name_and_address = _get_payer_name_and_address( + installment.apartment_reservation.customer + ) + apartment = get_cached_apartment(reservation.apartment_uuid) + project = get_cached_project(apartment.project_uuid) + + # override language to Finnish, as the user's browser settings etc. + # shouldn't affect the printed out PDFs + with translation.override("fi"): + apartment_text = ( + _("Apartment") + + f" {apartment.apartment_number}\n\n{installment.type}" + + 20 * " " + + str(installment.value).replace(".", ",") + + " €" + ) + final_installment_type = InstallmentType.PAYMENT_7 + if apartment.project_ownership_type == OwnershipType.HASO.value: + final_installment_type = InstallmentType.RIGHT_OF_OCCUPANCY_PAYMENT_3 + pass + + payment_recipient = apartment.project_payment_recipient + if installment.type == final_installment_type: + payment_recipient = apartment.project_payment_recipient_final + pass + + invoice_pdf_data = InvoicePDFData( + recipient=payment_recipient, + recipient_account_number=f"{project.project_contract_rs_bank or ''} " + f"{installment.account_number}".strip(), + payer_name_and_address=payer_name_and_address, + reference_number=installment.reference_number, + due_date=installment.due_date, + amount=installment.value, + apartment=apartment_text, + ) - reservation = installment.apartment_reservation - payer_name_and_address = _get_payer_name_and_address( - installment.apartment_reservation.customer - ) - apartment = get_cached_apartment(reservation.apartment_uuid) - project = get_cached_project(apartment.project_uuid) - - # override language to Finnish, as the user's browser settings etc. - # shouldn't affect the printed out PDFs - with translation.override("fi"): - apartment_text = ( - _("Apartment") - + f" {apartment.apartment_number}\n\n{installment.type}" - + 20 * " " - + str(installment.value).replace(".", ",") - + " €" - ) - - final_installment_type = InstallmentType.PAYMENT_7 - if apartment.project_ownership_type == OwnershipType.HASO.value: - final_installment_type = InstallmentType.RIGHT_OF_OCCUPANCY_PAYMENT_3 - pass - - payment_recipient = apartment.project_payment_recipient - if installment.type == final_installment_type: - payment_recipient = apartment.project_payment_recipient_final - pass - - invoice_pdf_data = InvoicePDFData( - recipient=payment_recipient, - recipient_account_number=f"{project.project_contract_rs_bank or ''} " - f"{installment.account_number}".strip(), - payer_name_and_address=payer_name_and_address, - reference_number=installment.reference_number, - due_date=installment.due_date, - amount=installment.value, - apartment=apartment_text, - ) + return invoice_pdf_data - return invoice_pdf_data def create_invoice_pdf_from_installments( installments: Union[QuerySet, List[ApartmentInstallment]] diff --git a/invoicing/tests/test_pdf.py b/invoicing/tests/test_pdf.py index 294be21d9..6095f0261 100644 --- a/invoicing/tests/test_pdf.py +++ b/invoicing/tests/test_pdf.py @@ -7,13 +7,15 @@ from django.utils.translation import gettext_lazy as _ from customer.tests.factories import CustomerFactory -from invoicing.pdf import _get_payer_name_and_address, get_invoice_pdf_data_from_installment +from invoicing.pdf import ( + _get_payer_name_and_address, + get_invoice_pdf_data_from_installment, +) from users.tests.factories import ProfileFactory + @pytest.mark.django_db -@pytest.mark.parametrize( - "ownership_type", (OwnershipType.HASO, OwnershipType.HITAS) -) +@pytest.mark.parametrize("ownership_type", (OwnershipType.HASO, OwnershipType.HITAS)) def test_pdf_payment_recipients_set_correctly(ownership_type): apartment = ApartmentDocumentFactory( project_ownership_type=ownership_type.value, @@ -41,15 +43,24 @@ def test_pdf_payment_recipients_set_correctly(ownership_type): type=final_installment_type, ) - - first_installment_pdf_data = get_invoice_pdf_data_from_installment(first_installment) - final_installment_pdf_data = get_invoice_pdf_data_from_installment(final_installment) + first_installment_pdf_data = get_invoice_pdf_data_from_installment( + first_installment + ) + final_installment_pdf_data = get_invoice_pdf_data_from_installment( + final_installment + ) assert first_installment_pdf_data.recipient == apartment.project_payment_recipient - assert final_installment_pdf_data.recipient == apartment.project_payment_recipient_final # noqa: E501 + assert ( + final_installment_pdf_data.recipient + == apartment.project_payment_recipient_final + ) # noqa: E501 + + # assert the final payment still gets the final recipient pass + @pytest.mark.django_db def test_pdf_payer_name_address_correct(): customer = CustomerFactory( From 7748430b6f882004f7652b9c96ec4ed6b244c11c Mon Sep 17 00:00:00 2001 From: leevi-identio Date: Thu, 23 Oct 2025 12:31:32 +0300 Subject: [PATCH 3/3] sort imports --- application_form/tests/pdf_utils.py | 10 ++++++---- application_form/tests/test_pdf_haso.py | 5 +---- application_form/tests/test_pdf_hitas.py | 5 +---- application_form/tests/utils.py | 1 + invoicing/tests/test_pdf.py | 10 +++++----- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/application_form/tests/pdf_utils.py b/application_form/tests/pdf_utils.py index 48485c67e..49d450e0d 100644 --- a/application_form/tests/pdf_utils.py +++ b/application_form/tests/pdf_utils.py @@ -1,22 +1,24 @@ -from datetime import date import re import subprocess +from datetime import date from typing import List, Union + +import pytest from faker import Faker + from apartment.elastic.documents import ApartmentDocument from apartment.enums import OwnershipType from apartment.tests.factories import ApartmentDocumentFactory from application_form.models.reservation import ApartmentReservation -from application_form.pdf.haso import HasoContractPDFData, get_haso_contract_pdf_data +from application_form.pdf.haso import get_haso_contract_pdf_data, HasoContractPDFData from application_form.pdf.hitas import ( + get_hitas_contract_pdf_data, HitasCompleteApartmentContractPDFData, HitasContractPDFData, - get_hitas_contract_pdf_data, ) from application_form.tests.factories import ApartmentReservationFactory from invoicing.enums import InstallmentType from invoicing.tests.factories import ApartmentInstallmentFactory -import pytest from users.tests.factories import UserFactory diff --git a/application_form/tests/test_pdf_haso.py b/application_form/tests/test_pdf_haso.py index 55201b439..30c168cae 100644 --- a/application_form/tests/test_pdf_haso.py +++ b/application_form/tests/test_pdf_haso.py @@ -8,10 +8,7 @@ from apartment_application_service.pdf import PDFCurrencyField as CF from users.tests.factories import UserFactory -from ..pdf.haso import ( - create_haso_contract_pdf_from_data, - HasoContractPDFData, -) +from ..pdf.haso import create_haso_contract_pdf_from_data, HasoContractPDFData from .pdf_utils import ( get_cleaned_pdf_texts, remove_pdf_id, diff --git a/application_form/tests/test_pdf_hitas.py b/application_form/tests/test_pdf_hitas.py index f7c135310..cba14ca6e 100644 --- a/application_form/tests/test_pdf_hitas.py +++ b/application_form/tests/test_pdf_hitas.py @@ -26,10 +26,7 @@ HitasCompleteApartmentContractPDFData, HitasContractPDFData, ) -from .pdf_utils import ( - get_cleaned_pdf_texts, - remove_pdf_id, -) +from .pdf_utils import get_cleaned_pdf_texts, remove_pdf_id # This variable should be normally False, but can be set temporarily to # True to override the expected test result PDF file. This is useful diff --git a/application_form/tests/utils.py b/application_form/tests/utils.py index b55504d64..10a3cf114 100644 --- a/application_form/tests/utils.py +++ b/application_form/tests/utils.py @@ -5,6 +5,7 @@ from typing import List, Tuple from django.utils import timezone + from apartment.elastic.documents import ApartmentDocument from apartment.elastic.queries import apartment_query from connections.enums import ApartmentStateOfSale diff --git a/invoicing/tests/test_pdf.py b/invoicing/tests/test_pdf.py index 6095f0261..fbcd0e002 100644 --- a/invoicing/tests/test_pdf.py +++ b/invoicing/tests/test_pdf.py @@ -1,16 +1,16 @@ -from apartment.enums import OwnershipType -from apartment.tests.factories import ApartmentDocumentFactory -from application_form.tests.factories import ApartmentReservationFactory -from invoicing.enums import InstallmentType -from invoicing.tests.factories import ApartmentInstallmentFactory import pytest from django.utils.translation import gettext_lazy as _ +from apartment.enums import OwnershipType +from apartment.tests.factories import ApartmentDocumentFactory +from application_form.tests.factories import ApartmentReservationFactory from customer.tests.factories import CustomerFactory +from invoicing.enums import InstallmentType from invoicing.pdf import ( _get_payer_name_and_address, get_invoice_pdf_data_from_installment, ) +from invoicing.tests.factories import ApartmentInstallmentFactory from users.tests.factories import ProfileFactory