Skip to content

15. Ćwiczenie praktyczne – wersja 4

Image

Powracamy tutaj do ćwiczenia opisanego w akapicie |Version 3| i rozwiązujemy je teraz przy użyciu klas i interfejsów. Napiszemy dwie aplikacje:

Aplikacja 1 będzie wyglądała następująco:

Image

Główny skrypt [main] utworzy instancję warstwy [dao] oraz warstwy [métier]:

  • warstwa [dao] będzie odpowiedzialna za zarządzanie danymi przechowywanymi w plikach tekstowych, a w dalszej kolejności w bazie danych;
  • warstwa [métier] będzie odpowiedzialna za obliczanie podatku;

W tej aplikacji użytkownik nie będzie musiał wykonywać żadnych czynności: dane podatników będą pobierane z pliku tekstowego, którego nazwę poda się modułowi [main].

W aplikacji 2 to użytkownik będzie wprowadzał dane podatników za pomocą klawiatury. Architektura ulegnie wówczas następującej zmianie:

Image

  • warstwa [dao] (Data Access Object) odpowiada za dostęp do danych zewnętrznych
  • warstwa [métier] zajmuje się zagadnieniami biznesowymi, w tym przypadku obliczaniem podatku. Nie zajmuje się ona danymi. Dane te mogą pochodzić z dwóch źródeł:
    • warstwa [dao] dla danych trwałych;
    • warstwa [ui] dla danych dostarczonych przez użytkownika.
  • warstwa [ui] (interfejs użytkownika) odpowiada za interakcje z użytkownikiem;
  • [main] pełni rolę koordynatora;

W dalszej części warstwy [dao], [métier] i [ui] zostaną zaimplementowane za pomocą odpowiednich klas. Warstwy [métier] i [dao] będą takie same dla obu aplikacji. Z tego powodu zostały one połączone w jednej wersji ćwiczenia aplikacyjnego.

15.1. Wersja 4 – aplikacja 1

Wersja 4 oblicza podatek dla listy podatników umieszczonej w pliku tekstowym. Ma następującą architekturę:

Image

15.1.1. Elementy

Image

Entities to klasy danych. Ich rolą jest hermetyzacja danych oraz udostępnianie metod getterów i setterów, które pozwalają na weryfikację ich poprawności. Entities są przekazywane między warstwami. Ta sama entitatea może przejść z warstwy [ui] do warstwy [dao] i odwrotnie.

15.1.1.1. Klasa [ImpôtsError]

Wykorzystamy własną klasę wyjątków:


# -------------------------------
# klasa wyjątków
from MyException import MyException


class ImpôtsError(MyException):
    pass

Gdy tylko warstwy [métier] i [dao] napotkają problem, wygenerują ten wyjątek. Klasa ta wywodzi się z klasy [MyException]. Stosuje się ją zatem w następujący sposób: [raise ImpôtsError(code_erreur, msg_erreur)].

15.1.1.2. Klasa [AdminData]

Klasa [AdminData] zawiera stałe wykorzystywane przy obliczaniu podatku:


from BaseEntity import BaseEntity


# dane administracji podatkowej
class AdminData(BaseEntity):
    # klucze wykluczone ze stanu klasy
    excluded_keys = []

    # klucze dozwolone
    @staticmethod
    def get_allowed_keys() -> list:
        return [
            "limites",
            "coeffr",
            "coeffn",
            "plafond_qf_demi_part",
            "plafond_revenus_celibataire_pour_reduction",
            "plafond_revenus_couple_pour_reduction",
            "valeur_reduc_demi_part",
            "plafond_decote_celibataire",
            "plafond_decote_couple",
            "plafond_impot_couple_pour_decote",
            "plafond_impot_celibataire_pour_decote",
            "abattement_dixpourcent_max",
            "abattement_dixpourcent_min"
        ]
  • wiersz 5: klasa [AdminData] rozszerza klasę [BaseEntity] opisaną w akapicie |BaseEntity|. Należy pamiętać, że klasy rozszerzające klasę [BaseEntity] muszą zdefiniować:
    • atrybut klasy [excluded_keys] (wiersz 7), który zawiera listę właściwości obiektu pomijanych podczas przekształcania obiektu w słownik;
    • statyczną metodę [get_allowed_keys] (wiersze 10–26), która zwraca listę właściwości akceptowanych podczas inicjalizacji obiektu za pomocą słownika;

Nie zastosowano metod ustawiających (setterów) w celu sprawdzenia poprawności danych używanych do inicjalizacji obiektu [AdminData]. Obiekt ten jest bowiem unikalny i definiowany przez konfigurację, a zatem nie może zawierać błędów.

15.1.1.3. Klasa [TaxPayer]

Klasa [TaxPayer] będzie modelować podatnika:


# importy
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError


# podatnik
class TaxPayer(BaseEntity):
    # modeluje podatnika
    # id: identyfikator
    # stan cywilny: tak / nie
    # dzieci: liczba dzieci
    # wynagrodzenie: jego roczne wynagrodzenie
    # podatek: kwota podatku do zapłaty
    # dopłata: dopłata do podatku do zapłaty
    # ulga: ulga od podatku do zapłaty
    # ulga: ulga od podatku do zapłaty
    # stawka: stawka podatkowa podatnika

    # klucze wykluczone z zestawienia klasy
    excluded_keys = []

    # klucze dozwolone
    @staticmethod
    def get_allowed_keys() -> list:
        return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']

    # właściwości
    @property
    def marié(self) -> str:
        return self.__marié

    @property
    def enfants(self) -> int:
        return self.__enfants

    @property
    def salaire(self) -> int:
        return self.__salaire

    @property
    def impôt(self) -> int:
        return self.__impôt

    @property
    def surcôte(self) -> int:
        return self.__surcôte

    @property
    def décôte(self) -> int:
        return self.__décôte

    @property
    def réduction(self) -> int:
        return self.__réduction

    @property
    def taux(self) -> float:
        return self.__taux

    # ustawki
    @marié.setter
    def marié(self, marié: str):
        ok = isinstance(marié, str)
        if ok:
            marié = marié.strip().lower()
            ok = marié == "oui" or marié == "non"
        if ok:
            self.__marié = marié
        else:
            raise ImpôtsError(31, f"l'attribut marié [{marié}] doit avoir l'une des valeurs oui / non")

    @enfants.setter
    def enfants(self, enfants):
        # liczba dzieci musi być liczbą całkowitą >=0
        try:
            enfants = int(enfants)
            erreur = enfants < 0
        except:
            erreur = True
        if not erreur:
            self.__enfants = enfants
        else:
            raise ImpôtsError(32, f"L'attribut enfants [{enfants}] doit être un entier >=0")

    @salaire.setter
    def salaire(self, salaire):
        # wynagrodzenie musi być liczbą całkowitą >=0
        try:
            salaire = int(salaire)
            erreur = salaire < 0
        except:
            erreur = True
        if not erreur:
            self.__salaire = salaire
        else:
            raise ImpôtsError(33, f"L'attribut salaire [{salaire}] doit être un entier >=0")

    @impôt.setter
    def impôt(self, impôt):
        # podatek musi być liczbą całkowitą >=0
        try:
            impôt = int(impôt)
            erreur = impôt < 0
        except:
            erreur = True
        if not erreur:
            self.__impôt = impôt
        else:
            raise ImpôtsError(34, f"L'attribut impôt [{impôt}] doit être un nombre >=0")

    @décôte.setter
    def décôte(self, décôte):
        # obniżka musi być liczbą całkowitą >=0
        try:
            décôte = int(décôte)
            erreur = décôte < 0
        except:
            erreur = True
        if not erreur:
            self.__décôte = décôte
        else:
            raise ImpôtsError(35, f"L'attribut décôte [{décôte}] doit être un nombre >=0")

    @surcôte.setter
    def surcôte(self, surcôte):
        # dopłata musi być liczbą całkowitą >=0
        try:
            surcôte = int(surcôte)
            erreur = surcôte < 0
        except:
            erreur = True
        if not erreur:
            self.__surcôte = surcôte
        else:
            raise ImpôtsError(36, f"L'attribut surcôte [{surcôte}] doit être un nombre >=0")

    @réduction.setter
    def réduction(self, réduction):
        # dopłata musi być liczbą całkowitą >=0
        try:
            réduction = int(réduction)
            erreur = réduction < 0
        except:
            erreur = True
        if not erreur:
            self.__réduction = réduction
        else:
            raise ImpôtsError(37, f"L'attribut réduction [{réduction}] doit être un nombre >=0")

    @taux.setter
    def taux(self, taux):
        # stawka musi być liczbą rzeczywistą >=0
        try:
            taux = float(taux)
            erreur = taux < 0
        except:
            erreur = True
        if not erreur:
            self.__taux = taux
        else:
            raise ImpôtsError(38, f"L'attribut taux [{taux}] doit être un nombre >=0")

Uwagi:

  • klasa [TaxPayer] zawiera podatnika;
  • wiersz 7: klasa [TaxPayer] wywodzi się z klasy [BaseEntity]. Posiada zatem identyfikator [id];
  • wiersz 20: żadna właściwość nie jest wykluczona ze stanu obiektu [AdminData];
  • wiersze 22–25: właściwości klasy. Są one opisane w wierszach 9–17;
  • wiersze 27–58: metody pobierające atrybutów klasy;
  • wiersze 60–161: metody ustawiające atrybutów klasy. Przypominamy, że zaletą klasy hermetyzującej dane w porównaniu z prostym słownikiem jest to, że klasa może sprawdzać poprawność swoich właściwości za pomocą metod ustawiających;

15.1.2. Warstwa [dao]

Image

Zgromadzimy implementacje warstw w folderze [services]. Klasy te będą implementować interfejsy zdefiniowane w folderze [interfaces].

Image

15.1.2.1. Interfejs [InterfaceImpôtsDao]

Warstwa [dao] będzie implementować następujący interfejs [InterfaceImpôtsDao] (plik InterfaceImpôtsDao.py):


# importy
from abc import ABC, abstractmethod


# interfejs IImpôtsDao
from AdminData import AdminData


class InterfaceImpôtsDao(ABC):
    # lista przedziałów podatkowych
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

    # lista danych podatników
    @abstractmethod
    def get_taxpayers_data(self) -> dict:
        pass

    # zapis wyników obliczeń podatku
    @abstractmethod
    def write_taxpayers_results(self, taxpayers_results: list):
        pass

Interfejs definiuje trzy metody:

  • [get_admindata]: jest to metoda pobierająca tablicę przedziałów podatkowych. Należy zauważyć, że nie podano żadnych informacji dotyczących sposobu uzyskania tych danych. W dalszej części zostaną one najpierw pobrane z pliku tekstowego, a następnie z bazy danych. Klasy implementujące ten interfejs będą musiały dostosować się do sposobu przechowywania danych. Będziemy więc mieli jedną klasę do pobierania przedziałów podatkowych z pliku tekstowego i drugą do pobierania ich z bazy danych. Obie będą implementowały metodę [get_admindata];
  • [get_taxpayers_data]: jest to metoda pobierająca dane podatników. Również w tym przypadku nie określamy, gdzie dane te będą się znajdować. Omówimy jedynie przypadek, w którym znajdują się one w pliku tekstowym;
  • [write_taxpayers_results]: jest to metoda, która zapisuje wyniki obliczeń podatku. Nie określamy, gdzie. Rozpatrzymy wyłącznie przypadek, w którym wyniki są zapisywane w pliku tekstowym. Parametr [taxpayers_results] będzie listą wyników do zapisania;

15.1.2.2. Klasa [AbstractImpôtsDao]

Warstwa [dao] zostanie zaimplementowana przez dwie klasy:

  • jedna będzie pobierać dane (podatnicy, wyniki, progi podatkowe) z plików tekstowych;
  • druga będzie pobierać dane (podatnicy, wyniki) z plików tekstowych, a progi podatkowe z bazy danych;

Obie klasy będą się różnić jedynie sposobem obsługi przedziałów podatkowych. Dane podatników oraz wyniki obliczeń podatku będą natomiast obsługiwane w ten sam sposób. Z tego powodu będziemy nimi zarządzać w klasie nadrzędnej [AbstractImpôtsDao]. Specyfika obsługi przedziałów podatkowych będzie natomiast realizowana w dwóch klasach podrzędnych:

  • klasa [ImpôtsDaoWithAdminDataInJsonFile] pobierze przedziały podatkowe z pliku tekstowego w formacie jSON;
  • klasa [ImpôtsDaoWithAdminDataInDatabase] pobierze przedziały podatkowe z bazy danych;

Klasa nadrzędna [AbstractImpôtsDao] będzie wyglądać następująco:


# importy
import codecs
import json
from abc import abstractmethod

from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDao import InterfaceImpôtsDao
from TaxPayer import TaxPayer


# klasa bazowa dla warstwy [dao]
class AbstractImpôtsDao(InterfaceImpôtsDao):
    # podatnicy i ich podatki będą znajdować się w plikach tekstowych
    # konstruktor
    def __init__(self, config: dict):
        # konfiguracja[taxpayersFilename]: nazwa pliku tekstowego zawierającego podatników
        # konfiguracja[resultsFilename]: nazwa pliku jSON z wynikami
        # config[errorsFilename]: nazwa pliku błędów

        # zapisywanie parametrów
        self.taxpayers_filename = config.get("taxpayersFilename")
        self.taxpayers_results_filename = config.get("resultsFilename")
        self.errors_filename = config.get("errorsFilename")

    # ------------------
    # interfejs IImpôtsDao
    # ------------------

    # lista danych podatników
    def get_taxpayers_data(self) -> dict:
        

    # księgowanie podatku podatników
    def write_taxpayers_results(self, taxpayers: list):
        

    # odczyt przedziałów podatkowych
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass
  • wiersz 13: klasa [AbstractImpôtsDao] implementuje interfejs [InterfaceImpôtsDao]. Znajdują się tam zatem trzy metody tego interfejsu:

    • [get_taxpayers_data]: wiersz 31;
    • [write_taxpayers_results]: wiersz 35;
    • [get_admindata]: wiersz 40. Metoda ta nie zostanie zaimplementowana przez klasę [AbstractImpôtsDao], dlatego została zadeklarowana jako abstrakcyjna (wiersz 39);
  • wiersz 16: konstruktor otrzymuje słownik [config] zawierający następujące informacje:

    • [taxpayersFilename]: nazwa pliku tekstowego zawierającego dane podatników;
    • [resultsFilename]: nazwa pliku tekstowego, w którym zostaną zapisane wyniki;
    • [errorsFilename]: nazwa pliku tekstowego zawierającego listę błędów napotkanych podczas przetwarzania pliku [taxpayersFilename];

Metoda [get_taxpayers_data] wygląda następująco:


    # lista danych podatników
    def get_taxpayers_data(self) -> dict:
        # inicjalizacje
        taxpayers_data = []
        datafile = None
        erreurs = []
        try:
            # otwarcie pliku danych
            datafile = open(self.taxpayers_filename, "r")
            # przetwarzanie bieżącego wiersza pliku
            ligne = datafile.readline()
            # numer wiersza
            numligne = 0
            while ligne != '':
                # wiersz z +
                numligne += 1
                # usuwanie spacji
                ligne = ligne.strip()
                # pomijamy puste wiersze i komentarze
                if ligne != "" and ligne[0] != "#":
                    try:
                        # pobieramy 4 pola: id, żonaty, dzieci, wynagrodzenie, które tworzą wiersz podatnika
                        (id, marié, enfants, salaire) = ligne.split(",")
                        # tworzymy nowy plik TaxPayer
                        taxpayers_data.append(
                            TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
                    except BaseException as erreur:
                        # odnotowuje się błąd
                        erreurs.append(f"Ligne {numligne}, {erreur}")
                # odczytuje się nowy wiersz podatnika
                ligne = datafile.readline()
            # zapisuje się ewentualne błędy
            if erreurs:
                text = f"Analyse du fichier {self.taxpayers_filename}\n\n" + "\n".join(erreurs)
                with codecs.open(self.errors_filename, "w", "utf-8") as fd:
                    fd.write(text)
            # zwracamy wynik
            return {"taxpayers": taxpayers_data, "erreurs": erreurs}
        except BaseException as erreur:
            # rzucamy wyjątek ImpôtsError
            raise ImpôtsError(11, f"{erreur}")
        finally:
            # zamykamy plik
            if datafile:
                datafile.close()
  • wiersz 4: dane podatników (stan cywilny, dzieci, wynagrodzenie) zostaną umieszczone w liście obiektów typu [TaxPayer];
  • wiersze 8–9: otwieramy plik tekstowy podatników w trybie odczytu. Jego zawartość ma następującą postać:
# dane poprawne: identyfikator, stan cywilny, dzieci, wynagrodzenie
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# mogą występować puste wiersze

# tworzymy błędne wiersze
# za mało wartości
11,12
# zbyt wiele wartości
12,oui,3,200000, x, y
# błędne wartości
x,x,x,x

W porównaniu z poprzednimi wersjami:

  • każdy wiersz pliku [taxpayersFilename] zaczyna się od identyfikatora podatnika, czyli zwykłego numeru;
  • dozwolone są komentarze i puste wiersze;
  • będziemy obsługiwać błędy. W związku z tym wiersze 17, 19 i 21 należy oznaczyć jako błędne. Błędy są zapisywane w osobnym pliku;

Kontynuujmy analizę kodu:

  • wiersz 4: dane z pliku tekstowego są przenoszone do listy [taxPayersData];
  • wiersze 14–31: plik podatników jest odczytywany wiersz po wierszu;
  • wiersz 14: koniec pliku zostaje osiągnięty po odczytaniu pustego wiersza (nic – nawet znak końca wiersza \r\n);
  • wiersz 20: puste wiersze i komentarze są pomijane. Wiersz jest komentarzem, jeśli po usunięciu spacji przed i po tekście pierwszym znakiem jest znak #;
  • wiersz 24: poprawny wiersz składa się z czterech pól oddzielonych przecinkami. Pola te są pobierane. Przypisanie danych do krotki składającej się z czterech elementów kończy się niepowodzeniem, jeśli nie ma dokładnie czterech przypisanych danych;
  • wiersz 25: jeśli jedno z czterech pobranych pól [id, marié, enfants, salaire] jest nieprawidłowe, wówczas metoda [BaseEntity.fromdict] zgłosi wyjątek typu [MyException];
  • wiersze 25–26: obiekt [TaxPayer] jest dodawany do listy podatników [taxpayers_data];
  • wiersze 27–29: ewentualne błędy są gromadzone na liście [erreurs]. Lista ta została utworzona w wierszu 6;
  • wiersze 33–36: lista napotkanych błędów jest zapisywana w pliku tekstowym [errorsFilename]. Błędy te dzielą się na dwa rodzaje:
    • wiersz nie zawierał prawidłowej liczby oczekiwanych pól;
    • informacje w wierszu były błędne i nie udało się utworzyć obiektu [TaxPayer];
  • wiersze 39–41: przechwytuje się każdy błąd (BaseException) i przekazuje go wyżej, zamykając go w typie [ImpôtsError];
  • wiersze 42–45: niezależnie od wyniku (powodzenie lub niepowodzenie) plik tekstowy podatników zostaje zamknięty, jeśli został otwarty;

Metoda [write_taxpayers_results] musi generować plik jSON o następującej postaci:


[
  {
    "id": 1,
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555,
    "impôt": 2814,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 0,
    "réduction": 0
  },
  {
    "id": 2,
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000,
    "impôt": 1384,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 384,
    "réduction": 347
  },
  {
    "id": 3,
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000,
    "impôt": 0,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 720,
    "réduction": 0
  },

]

Metoda [write_taxpayers_results] wygląda następująco:


    # zapisywanie podatku podatników
    def write_taxpayers_results(self, taxpayers: list):
        # zapisywanie wyników do pliku jSON
        # podatnicy: lista obiektów typu TaxPayer
        # (id, stan cywilny, dzieci, wynagrodzenie, podatek, dopłata, ulga, obniżka, stawka)
        # lista [taxpayers] jest zapisana w pliku tekstowym [self.taxpayers_results_filename]
        file = None
        try:
            # otwarcie pliku wyników
            file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
            # utworzenie listy do serializacji w formacie jSON
            mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
            # serializacja jSON
            json.dump(list(mapping), file, ensure_ascii=False)
        except BaseException as erreur:
            # ponowne zgłoszenie błędu jako innego typu
            raise ImpôtsError(12, f"{erreur}")
        finally:
            # zamykamy plik, jeśli został otwarty
            if file:
                file.close()
  • wiersz 2: metoda otrzymuje listę podatników [taxpayers], którą musi zapisać w pliku tekstowym [self.taxpayers_results_filename] w formacie jSON;
  • wiersz 10: utworzenie pliku wyników o nazwie UTF-8;
  • wiersz 12: wprowadzamy tutaj funkcję [map], której składnia w tym przypadku brzmi [map (fonction, liste1)]. Funkcja [fonction] jest stosowana do każdego elementu [liste1] i generuje nowy element, który zasila listę [liste2]. Na koniec, dla każdego i:

liste2[i]=fonction(liste1[i])

W tym przypadku [liste1] to lista [taxPayers], czyli lista obiektów typu [TaxPayer]. Funkcja [fonction] jest tutaj wyrażona w postaci funkcji zwanej [lambda], która określa transformację dokonywaną na elemencie [taxpayer] z listy [taxpayers]: każdy element [taxpayer] jest zastępowany swoim słownikiem [taxpayer.asdict()]. Ostatecznie uzyskana lista [liste2] jest listą słowników elementów z listy [taxpayers];

  • wiersz 12: wynikiem zwracanym przez funkcję [map] nie jest lista [liste2], lecz obiekt typu [map]. Aby uzyskać [liste2], należy użyć wyrażenia [list(mapping)] (wiersz 14);
  • wiersz 14: lista [liste2] jest zapisywana w formacie jSON w pliku [self.taxpayers_results_filename];
  • wiersze 15–17: każdy rodzaj wyjątku jest przechwytywany i enkapsulowany w błąd typu [ImpôtsError], a następnie ponownie uruchamiany (wiersz 17);
  • wiersze 19–21: w każdym przypadku, niezależnie od powodzenia lub niepowodzenia, plik wyników jest zamykany, jeśli został otwarty;

15.1.2.3. Klasa [ImpôtsDaoWithAdminDataInJsonFile]

Klasa [ImpôtsDaoWithAdminDataInJsonFile] będzie pochodzić od klasy [AbstractImpôtsDao] i zaimplementuje metodę [getAdminData], której jej klasa nadrzędna nie zaimplementowała. Będzie ona pobierać dane z urzędu skarbowego z pliku jSON:


{
    "limites": [9964, 27519, 73779, 156244, 0],
    "coeffr": [0, 0.14, 0.3, 0.41, 0.45],
    "coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
    "plafond_qf_demi_part": 1551,
    "plafond_revenus_celibataire_pour_reduction": 21037,
    "plafond_revenus_couple_pour_reduction": 42074,
    "valeur_reduc_demi_part": 3797,
    "plafond_decote_celibataire": 1196,
    "plafond_decote_couple": 1970,
    "plafond_impot_couple_pour_decote": 2627,
    "plafond_impot_celibataire_pour_decote": 1595,
    "abattement_dixpourcent_max": 12502,
    "abattement_dixpourcent_min": 437
}

Klasa [ImpôtsDaoWithAdminDataInJsonFile] ma następującą postać:


# importy
import codecs
import json

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError


# implementacja warstwy [dao], w której dane urzędu skarbowego znajdują się w pliku jSON
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
    # konstruktor
    def __init__(self, config: dict):
        # konfiguracja[admindataFilename]: nazwa pliku jSON zawierającego dane urzędu skarbowego
        # config[taxpayersFilename]: nazwa pliku tekstowego podatników
        # config[resultsFilename]: nazwa pliku jSON z wynikami
        # config[errorsFilename]: nazwa pliku błędów

        # inicjalizacja klasy Parent
        AbstractImpôtsDao.__init__(self, config)
        # odczyt danych z urzędu skarbowego
        file = None
        try:
            # otwarcie pliku jSON zawierającego dane podatkowe w trybie odczytu
            file = codecs.open(config["admindataFilename"], "r", "utf8")
            # przeniesienie zawartości pliku jSON do obiektu [AdminData]
            self.admindata = AdminData().fromdict(json.load(file))
        except BaseException as erreur:
            # błąd jest ponownie zgłaszany w postaci typu [ImpôtsError]
            raise ImpôtsError(21, f"{erreur}")
        finally:
            # zamknięcie pliku, jeśli został otwarty
            if file:
                file.close()

    # -------------
    # interfejs
    # -------------

    # pobieranie danych z urzędu skarbowego
    # metoda zwraca obiekt [AdminData]
    def get_admindata(self) -> AdminData:
        return self.admindata
  • wiersz 11: klasa [ImpôtsDaoWithAdminDataInJsonFile] dziedziczy po klasie [AbstractImpôtsDao]. W związku z tym implementuje interfejs [InterfaceImpôtsDao];
  • wiersz 13: konstruktor otrzymuje jako parametr słownik zawierający informacje z wierszy 14–17;
  • wiersz 20: klasa nadrzędna zostaje zainicjowana;
  • wiersz 24: otwieranie pliku jSON zawierającego dane urzędu skarbowego;
  • wiersz 25: otwierany jest plik UTF-8 zawierający dane z urzędu skarbowego;
  • wiersz 27: zawartość pliku jest odczytywana i umieszczana w obiekcie [self.admindata] typu [AdminData]. Klucze pliku jSON muszą odpowiadać właściwościom akceptowanym dla obiektu typu [AdminData], w przeciwnym razie metoda [fromdict] wygeneruje wyjątek;
  • wiersze 28–30: obsługa wyjątków. Wyjątki, które mogą wystąpić, są enkapsulowane w typie [ImpôtsError] przed ponownym zgłoszeniem;
  • wiersze 32–34: plik zostaje zamknięty, jeśli był otwarty;
  • wiersze 42–43: implementacja metody [get_admindata] z interfejsu [InterfaceImpôtsDao];

15.1.3. Warstwa [métier]

Image

15.1.3.1. Interfejs [InterfaceImpôtsMétier]

Interfejs warstwy [métier] będzie wyglądał następująco:


# importy
from abc import ABC, abstractmethod

from AdminData import AdminData
from TaxPayer import TaxPayer


# interfejs IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
    # obliczenie podatku dla 1 podatnika
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        pass
  • Interfejs [InterfaceImpôtsMétier] definiuje jedną metodę:
  • wiersz 12: metoda [calculate_tax] umożliwia obliczenie podatku dla pojedynczego podatnika [taxpayer]. [admindata] to obiekt [AdminData] zawierający dane administracji podatkowej;
  • wiersz 12: metoda [calculate_tax] nie zwraca wyniku. Uzyskane dane (podatek, dopłata, ulga, obniżka, stawka) są zawarte w parametrze [taxpayer]: przed wywołaniem atrybuty te są puste, po wywołaniu zostały zainicjowane;

15.1.3.2. Klasa [ImpôtsMétier]

Klasa [ImpôtsMétier] implementuje interfejs [InterfaceImpôtsMétier] w następujący sposób:

Image

Metody tej klasy pochodzą z modułu [impôts_module_02] z sekcji |Moduł [impots.v02.modules.impôts_module_02]|. Ograniczono jedynie liczbę parametrów metod do dwóch:

  • taxpayer(id, stan cywilny, liczba dzieci, wynagrodzenie, podatek, ulga, dopłata, obniżka, stawka): obiekt reprezentujący podatnika i jego podatek;
  • admindata: obiekt zawierający dane administracji podatkowej;

Na przykładzie jednej metody pokazujemy wprowadzone zmiany;


    # obliczenie podatku – etap 1
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        # podatnik (identyfikator, stan cywilny, dzieci, wynagrodzenie, podatek, ulga, dopłata, obniżka, stawka)
        # dane administracyjne: dane urzędu skarbowego

        # obliczenie podatku z uwzględnieniem dzieci
        self.calculate_tax_2(taxpayer, admindata)
        # wyniki znajdują się w tabeli „podatnik”
        taux1 = taxpayer.taux
        surcôte1 = taxpayer.surcôte
        impot1 = taxpayer.impôt
        # obliczenie podatku bez uwzględnienia dzieci
        if taxpayer.enfants != 0:
            # obliczenie podatku dla tego samego podatnika bez dzieci
            taxpayer2 = TaxPayer().fromdict(
                {'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
            self.calculate_tax_2(taxpayer2, admindata)
            # wyniki znajdują się w taxpayer2
            taux2 = taxpayer2.taux
            surcôte2 = taxpayer2.surcôte
            impot2 = taxpayer2.impôt
            # zastosowanie limitu ilorazu rodzinnego
            if taxpayer.enfants < 3:
                # PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci
                impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
            else:
                # PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci, dwukrotnie więcej na kolejne
                impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
                         * 2 * admindata.plafond_qf_demi_part
        else:
            # jeśli podatnik nie ma dzieci, to podatek2 = podatek1
            impot2 = impot1

        # przyjmuje się najwyższy podatek wraz z odpowiednią stawką i dopłatą
        (impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
            impot2, impot2 - impot1 + surcôte2, taux2)

        # wyniki częściowe
        taxpayer.impôt = impot
        taxpayer.surcôte = surcôte
        taxpayer.taux = taux
        # obliczenie ewentualnej ulgi
        self.get_décôte(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.décôte
        # obliczenie ewentualnej ulgi podatkowej
        self.get_réduction(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.réduction
        # wynik
        taxpayer.impôt = math.floor(taxpayer.impôt)
  • wiersz 3: metoda [calculate_tax] jest jedyną metodą interfejsu [InterfaceImpôtsMétier]. Przyjmuje dwa parametry:
    • [tapPayer]: podatnik, dla którego obliczany jest podatek;
    • [admindata]: obiekt zawierający dane administracji podatkowej;
    • wyniki obliczeń są zawarte w parametrze [taxpayer] (wiersze 40–50). Zawartość tego obiektu nie jest zatem taka sama przed i po wywołaniu metody;

15.1.4. Testy warstw [dao] i [métier]

Image

  • [TestDaoMétier] to klasa testowa UnitTest dla warstw [dao] i [métier];
  • [config] to plik konfiguracyjny testów;

Konfiguracja [config] jest następująca:


def configure():
    import os

    # etap 1 ------
    # konfiguracja ścieżki Python

    # katalog tego pliku
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    # absolutne zależności aplikacji
    absolute_dependencies = [
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
        f"{root_dir}/02/entities",
    ]

    # ustawiamy ścieżkę systemową
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # krok 2 ------
    # konfiguracja aplikacji
    config = {
        # absolutne ścieżki do plików aplikacji
        "admindataFilename"f"{script_dir}/../data/input/admindata.json"
    }

    # instancjonowanie warstw aplikacji
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    métier = ImpôtsMétier()

    # umieszczamy instancje warstw w konfiguracji
    config["dao"] = dao
    config["métier"] = métier

    # generowanie konfiguracji
    return config
  • wiersze 4–23: konfiguracja ścieżki Python Path dla testów;
  • wiersze 32–41: instancjonuje się warstwy [dao] i [métier]. Ich odwołania umieszcza się w słowniku [config];
  • wiersz 44: zwracamy ten słownik;

Klasa testowa [TestDaoMétier] wygląda następująco:


import unittest


def get_config() -> dict:
    # konfiguracja aplikacji
    import config
    # zwracamy konfigurację
    return config.configure()


class TestDaoMétier(unittest.TestCase):

    # wykonywane przed każdą metodą test_
    def setUp(self) -> None:
        # pobieramy konfigurację testów
        config = get_config()
        # zapisujemy pewne informacje
        self.métier = config['métier']
        self.admindata = config['dao'].get_admindata()

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'żonaty': 'tak', 'dzieci': 2, 'wynagrodzenie': 55555,
        # 'podatek': 2814, 'dopłata': 0, 'ulga': 0, 'obniżka': 0, 'stawka': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacja
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_2(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'żonaty', 'dzieci': 2, 'wynagrodzenie': 50000,
        # 'podatek': 1384, 'dopłata': 0, 'ulga': 384, 'obniżka': 347, 'stawka': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 1384, delta=1)
        self.assertAlmostEqual(taxpayer.décôte, 384, delta=1)
        self.assertAlmostEqual(taxpayer.réduction, 347, delta=1)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_3(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'żonaty', 'dzieci': 3, 'wynagrodzenie': 50000,
        # 'podatek': 0, 'dopłata': 0, 'odliczenie': 720, 'ulga': 0, 'stawka': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertEqual(taxpayer.impôt, 0)
        self.assertAlmostEqual(taxpayer.décôte, 720, delta=1)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_4(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'nieżonaty/niezamężna', 'dzieci': 2, 'wynagrodzenie': 100000,
        # 'podatek': 19884, 'dopłata': 4480, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 19884, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 4480, delta=1)

    def test_5(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'nieżonaty/niezamężna', 'dzieci': 3, 'wynagrodzenie': 100000,
        # 'podatek': 16782, 'dopłata': 7176, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 16782, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 7176, delta=1)

    def test_6(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'żonaty', 'dzieci': 3, 'wynagrodzenie': 100000,
        # 'podatek': 9200, 'dopłata': 2180, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,3}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 9200, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.3, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 2180, delta=1)

    def test_7(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'żonaty', 'dzieci': 5, 'wynagrodzenie': 100000,
        # 'podatek': 4230, 'dopłata': 0, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 4230, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_8(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'nieżonaty/niezamężna', 'dzieci': 0, 'wynagrodzenie': 100000,
        # 'podatek': 22986, 'dopłata': 0, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 22986, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_9(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'żonaty', 'dzieci': 2, 'wynagrodzenie': 30000,
        # 'podatek': 0, 'dopłata': 0, 'ulga': 0, 'obniżka': 0, 'stawka': 0}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertEqual(taxpayer.impôt, 0)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.0, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_10(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'nie', 'dzieci': 0, 'wynagrodzenie': 200000,
        # 'podatek': 64210, 'dopłata': 7498, 'ulga': 0, 'obniżka': 0, 'stawka': 0,45}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 64210, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.45, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 7498, delta=1)

    def test_11(self) -> None:
        from TaxPayer import TaxPayer

        # {'stan cywilny': 'żonaty', 'dzieci': 3, 'wynagrodzenie': 200000,
        # 'podatek': 42842, 'dopłata': 17283, 'odliczenie': 0, 'ulga': 0, 'stawka': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


if __name__ == '__main__':
    unittest.main()

Komentarze

  • wiersz 11: klasa testowa rozszerza klasę [unittest.TestCase];
  • wiersze 13–19: w teście UnitTest metoda [setUp] jest wykonywana przed każdą z metod [test_];
  • wiersz 16: pobierana jest konfiguracja pochodząca ze skryptu [config] omówionego wcześniej;
  • wiersz 18: zapisuje się odwołanie do warstwy [métier];
  • wiersz 19: żądamy od warstwy [dao] obiektu [AdminData] zawierającego dane urzędu skarbowego i zapisujemy go;
  • wiersze 21–173: 11 testów, których wyniki zostały zweryfikowane na oficjalnej stronie urzędu skarbowego z 2019 r. |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
  • wiersze 21–33: wszystkie testy zostały opracowane według tego samego wzorca;
  • wiersz 22: importujemy klasę [TaxPayer];
  • wiersz 24: podatnik objęty testem;
  • wiersz 25: oczekiwane wyniki;
  • wiersz 26: utworzenie obiektu [TaxPayer] dla podatnika;
  • wiersz 27: obliczenie podatku. Wynik znajduje się w obiekcie [taxpayer];
  • wiersze 29–33: weryfikacja uzyskanych wyników;
  • wiersz 29: weryfikacja kwoty podatku z dokładnością do jednego euro. Testy wykazały bowiem, że wyniki uzyskane przez algorytm opisany w niniejszym dokumencie mogą różnić się od oficjalnych danych o kwotę nie większą niż 1 euro;

Przeprowadzenie testów daje następujące wyniki:

Image

Testing started at 16:08 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\tests



Ran 11 tests in 0.055s

OK

Process finished with exit code 0

15.1.5. Skrypt główny

Image

Skrypt główny jest konfigurowany przez następujący skrypt [config]:


def configure():
    import os

    # etap 1 ------
    # konfiguracja ścieżki Python

    # katalog tego pliku
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    # zależności aplikacji
    absolute_dependencies = [
        # zależności lokalne
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    # ustawiamy ścieżkę systemową
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # krok 2 ------
    # konfiguracja aplikacji
    config = {
        # absolutne ścieżki do plików aplikacji
        "taxpayersFilename"f"{script_dir}/../../data/input/taxpayersdata.txt",
        "resultsFilename"f"{script_dir}/../../data/output/résultats.json",
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json",
        "errorsFilename"f"{script_dir}/../../data/output/errors.txt"
    }

    # tworzenie instancji warstw aplikacji
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    métier = ImpôtsMétier()

    # umieszczamy instancje warstw w konfiguracji
    config["dao"] = dao
    config["métier"] = métier

    # generowanie konfiguracji
    return config

Jest on analogiczny do skryptu używanego do testowania warstw [métier] i [dao].

Główny skrypt [main.py] ma następującą postać:


# konfiguracja aplikacji
import config

config = config.configure()

# importy
from ImpôtsError import ImpôtsError

# pobieramy warstwy aplikacji (są one już zainicjowane)
dao = config["dao"]
métier = config["métier"]

try:
    # pobieranie przedziałów podatkowych
    admindata = dao.get_admindata()
    # odczyt danych podatników
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # podatników?
    if not taxpayers:
        raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # obliczanie podatku podatników
    for taxpayer in taxpayers:
        # „podatnik” jest zarówno parametrem wejściowym, jak i wyjściowym
        # „podatnik” zostanie zmodyfikowany
        métier.calculate_tax(taxpayer, admindata)
    # zapis wyników do pliku tekstowego
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    # wyświetlenie błędu
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # zakończono
    print("Travail terminé...")

Uwagi

  • wiersze 2–4: pobierana jest konfiguracja aplikacji. Wiadomo również, że ścieżka Python Path aplikacji została utworzona;
  • wiersze 9–11: pobierane są odwołania do warstw [métier] i [dao];
  • wiersz 15: pobierane są dane z urzędu skarbowego;
  • wiersz 17: pobierana jest lista podatników, dla których należy obliczyć podatek;
  • wiersze 19–20: jeśli lista jest pusta, zgłaszany jest wyjątek;
  • wiersze 22–25: obliczanie podatku dla poszczególnych obiektów [taxpayer] przy użyciu warstwy [métier];
  • wiersz 27: [taxpayers] jest teraz listą obiektów [TaxPayer], w których atrybuty (podatek, obniżka, podwyżka, ulga, stawka) otrzymały wartości. Lista ta jest zapisywana w pliku jSON;
  • wiersze 28–30: wykrycie ewentualnego błędu;
  • wiersze 31–33: wykonywane w każdym przypadku;

Uruchomienie skryptu daje takie same wyniki jak w poprzednich wersjach. Plik zawierający błędy podatników był nowością w tej wersji. Po uruchomieniu skryptu [main] jego zawartość wygląda następująco:


Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\main\01/../../data/input/taxpayersdata.txt

Ligne 17, not enough values to unpack (expected 4, got 2)
Ligne 19, too many values to unpack (expected 4)
Ligne 21, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]

Błędne wiersze to:

# dane poprawne: identyfikator, stan cywilny, dzieci, wynagrodzenie
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# mogą występować puste wiersze

# tworzymy błędne wiersze
# za mało wartości
11,12
# zbyt wiele wartości
12,oui,3,200000, x, y
# błędne wartości
x,x,x,x

15.2. Wersja 4 – aplikacja 2

W tej wersji to użytkownik, za pomocą klawiatury, podaje listę podatników. Architektura aplikacji będzie wyglądać następująco:

Image

Pojawia się nowy moduł: warstwa [ui] (interfejs użytkownika), która będzie komunikować się z użytkownikiem. Warstwa ta będzie posiadać interfejs i zostanie zaimplementowana przez klasę.

Image

15.2.1. Interfejs [InterfaceImpôtsUi]


# importy
from abc import ABC, abstractmethod


# interfejs InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
    # wywołanie klasy implementującej interfejs
    @abstractmethod
    def run(self):
        pass

Interfejs [InterfaceImpôtsUi] będzie miał tylko jedną metodę, tę z wierszy 8–10. Interfejs ten zostanie tutaj zaimplementowany za pomocą aplikacji konsolowej, ale można go również zaimplementować za pomocą interfejsu graficznego. Parametry przekazywane do metody [run] nie byłyby takie same w obu implementacjach. Aby obejść ten problem, standardową metodą jest:

  • nie podawanie żadnych parametrów do metody [run] (lub podawanie minimalnej liczby parametrów);
  • przekazywanie parametrów do konstruktora klasy implementującej interfejs. Mogą one różnić się w zależności od implementacji. Parametry te są zapisywane jako atrybuty klasy;
  • sprawić, by metoda [run] korzystała z tych atrybutów klasy (self.x);

Metoda ta pozwala na uzyskanie bardzo ogólnego interfejsu, który jest doprecyzowywany przez parametry konstruktorów każdej klasy implementującej. Metoda ta została już wykorzystana w wersji modułowej nr 1.

15.2.2. Klasa [ImpôtsConsole]

Klasa [ImpôtsConsole] implementuje interfejs [InterfaceImpôtsUi] w następujący sposób:


# importy
import re

from InterfaceImpôtsUi import InterfaceImpôtsUi
from TaxPayer import TaxPayer


# warstwa [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
    # konstruktor
    def __init__(self, config: dict):
        # zapisywanie parametrów
        self.admindata = config['dao'].get_admindata()
        self.métier = config['métier']

    def run(self):
        # interaktywny dialog z użytkownikiem
        fini = False
        while not fini:
            # czy podatnik jest w związku małżeńskim?
            marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # sprawdzenie poprawności wprowadzonych danych
            while marié != "oui" and marié != "non" and marié != "*":
                # komunikat o błędzie
                print("Tapez oui ou non ou *")
                # powtórz pytanie
                marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # Koniec?
            if marié == "*":
                # rozmowa zakończona
                return
            # liczba dzieci
            enfants = input("Nombre d'enfants : ").strip()
            # sprawdzanie poprawności wprowadzonych danych
            if not re.match(r"^\d+$", enfants):
                # komunikat o błędzie
                print("Tapez un nombre entier positif ou nul")
                # spróbujmy jeszcze raz
                enfants = input("Nombre d'enfants : ").strip()
            # roczne wynagrodzenie
            salaire = input("Salaire annuel : ").strip()
            # sprawdzenie poprawności wprowadzonych danych
            if not re.match(r"^\d+$", salaire):
                # komunikat o błędzie
                print("Tapez un nombre entier positif ou nul")
                # próbujemy ponownie
                salaire = input("Salaire annuel : ").strip()
            # obliczenie podatku
            taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
            self.métier.calculate_tax(taxpayer, self.admindata)
            # wyświetlenie
            print(f"Impôt du contribuable = {taxpayer}\n\n")
            # kolejny podatnik
  • wiersz 9: klasa [ImpôtsConsole] implementuje interfejs [InterfaceImpôtsUi];
  • wiersz 11: konstruktor klasy otrzymuje parametr – słownik [config] z konfiguracji aplikacji;
    • wiersz 13: pobierane są dane z urzędu skarbowego umożliwiające obliczenie podatku;
    • wiersz 14: zapisuje się odwołanie w warstwie [métier];
  • wiersz 16: implementacja metody [run] z interfejsu;
  • wiersze 19–53: interakcja z użytkownikiem. Polega ona
    • w zapytaniu podatnika o trzy informacje (stan cywilny, dzieci, wynagrodzenie);
    • obliczenie jego podatku;
    • wyświetlenie kwoty podatku;
    • dialog kończy się, gdy użytkownik odpowie * na pierwsze pytanie;
  • wiersze 20–27: zadaje się pytanie, czy podatnik jest żonaty, i sprawdza się poprawność odpowiedzi;
  • wiersze 29–31: jeśli użytkownik odpowiedział „*” na pytanie, dialog zostaje zakończony;
  • wiersze 32–39: pyta się o liczbę dzieci podatnika i sprawdza poprawność odpowiedzi;
  • wiersze 40–47: pyta się o roczne wynagrodzenie podatnika i sprawdza poprawność odpowiedzi;
  • wiersze 48–50: na podstawie tych informacji warstwa [métier] oblicza podatek podatnika;
  • wiersz 52: wyświetlana jest kwota podatku;

15.2.3. Skrypt główny

Główny skrypt [main] jest skonfigurowany przez następujący plik [config]:


def configure():
    import os

    # krok 1 ------
    # konfiguracja ścieżki Python

    # katalog tego pliku
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    # zależności aplikacji
    absolute_dependencies = [
        # zależności lokalne
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    # ustawiamy ścieżkę systemową
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # krok 2 ------
    # konfiguracja aplikacji
    config = {
        # absolutne ścieżki do plików aplikacji
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json",
    }

    # instancjonowanie warstw aplikacji
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier
    from ImpôtsConsole import ImpôtsConsole

    # warstwa DAO
    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    # warstwa biznesowa
    métier = ImpôtsMétier()
    # instancje warstw umieszczamy w konfiguracji
    config["dao"] = dao
    config["métier"] = métier
    # warstwa UI
    ui = ImpôtsConsole(config)
    config["ui"] = ui

    # generujemy konfigurację
    return config

Skrypt koordynujący ma następującą postać (main.py):


# konfigurujemy aplikację
import config

config = config.configure()

# importy
from ImpôtsError import ImpôtsError

# pobieramy warstwy aplikacji (są już zainicjowane)
ui = config["ui"]

# kod
try:
    # wykonanie warstwy [ui]
    ui.run()
except ImpôtsError as erreur:
    # wyświetlanie komunikatu o błędzie
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # wykonane w każdym przypadku
    print("Travail terminé...")
  • wiersze 1–4: pobierana jest konfiguracja aplikacji;
  • wiersz 10: pobieramy odwołanie do warstwy [ui];
  • wiersze 12–21: struktura kodu jest taka sama jak w poprzedniej aplikacji: kod otoczony blokiem try/catch w celu przechwycenia ewentualnych wyjątków;
  • wiersz 15: uruchamiamy warstwę [ui]: rozpoczyna się wówczas dialog z użytkownikiem;
  • wiersze 16–18: przechwycenie ewentualnego wyjątku;

Oto przykład wykonania:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/main/02/main.py
Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : oui
Nombre d'enfants : 3
Salaire annuel : 200000
Impôt du contribuable = {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}


Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : *
Travail terminé...

Process finished with exit code 0