Skip to content

15. Toepassingsopgave - versie 4

Image

We nemen hier de oefening uit de paragraaf |Versie 3| opnieuw op en behandelen deze nu met klassen en interfaces. We zullen twee toepassingen schrijven:

Toepassing 1 ziet er als volgt uit:

Image

Een hoofdscript [main] zal een laag [dao] en een laag [métier] instantiëren:

  • de laag [dao] heeft als taak gegevens te beheren die zijn opgeslagen in tekstbestanden en later in een database;
  • de laag [métier] zal de belasting berekenen;

In deze applicatie zijn er geen handelingen door een gebruiker nodig: de gegevens van de belastingplichtigen worden opgehaald uit een tekstbestand waarvan de naam wordt doorgegeven aan de module [main].

In toepassing 2 voert de gebruiker de gegevens van de belastingplichtigen zelf in via het toetsenbord. De architectuur zal dan als volgt evolueren:

Image

  • de laag [dao] (Data Access Object) zorgt voor de toegang tot de externe gegevens
  • de laag [métier] houdt zich bezig met de bedrijfsprocessen, in dit geval de berekening van de belasting. Deze laag houdt zich niet bezig met de gegevens. Deze gegevens kunnen uit twee bronnen afkomstig zijn:
    • de laag [dao] voor persistente gegevens;
    • de laag [ui] voor door de gebruiker ingevoerde gegevens.
  • de laag [ui] (gebruikersinterface) zorgt voor de interacties met de gebruiker;
  • [main] is de regisseur;

Hierna zullen de lagen [dao], [métier] en [ui] elk met behulp van een klasse worden geïmplementeerd. De lagen [métier] en [dao] zullen voor beide applicaties hetzelfde zijn. Daarom zijn ze in één versie van de applicatieopdracht samengevoegd.

15.1. Versie 4 – toepassing 1

Versie 4 berekent de belasting van een lijst met belastingplichtigen die in een tekstbestand is opgeslagen. De architectuur is als volgt:

Image

15.1.1. De entiteiten

Image

Entiteiten zijn gegevensklassen. Hun rol is om gegevens in te kapselen en getters/setters aan te bieden waarmee de geldigheid ervan kan worden gecontroleerd. De entiteiten worden uitgewisseld tussen de lagen. Eenzelfde entiteit kan van de laag [ui] naar de laag [dao] gaan en vice versa.

15.1.1.1. De klasse [ImpôtsError]

We gebruiken een eigen uitzonderingsklasse:


# -------------------------------
# uitzonderingsklasse
from MyException import MyException


class ImpôtsError(MyException):
    pass

Zodra de lagen [métier] en [dao] een probleem tegenkomen, zullen ze deze uitzondering genereren. Deze is afgeleid van de klasse [MyException]. Ze wordt dus als volgt gebruikt: [raise ImpôtsError(code_erreur, msg_erreur)].

15.1.1.2. De klasse [AdminData]

De klasse [AdminData] omvat de constanten die een rol spelen bij de berekening van de belasting:


from BaseEntity import BaseEntity


# gegevens van de belastingdienst
class AdminData(BaseEntity):
    # sleutels die zijn uitgesloten van de klasseoverzicht
    excluded_keys = []

    # toegestane sleutels
    @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"
        ]
  • regel 5: de klasse [AdminData] is een uitbreiding van de klasse [BaseEntity] die wordt beschreven in de paragraaf |BaseEntity|. We herinneren ons dat klassen die de klasse [BaseEntity] uitbreiden, het volgende moeten definiëren:
    • een klasse-attribuut [excluded_keys] (regel 7) dat de eigenschappen van het object opsomt die worden uitgesloten wanneer het object wordt omgezet in een woordenboek;
    • een statische methode [get_allowed_keys] (regels 10-26) die de lijst met geaccepteerde eigenschappen retourneert wanneer het object wordt geïnitialiseerd met een woordenboek;

Er zijn geen setters gebruikt om de geldigheid te controleren van de gegevens die worden gebruikt om een object [AdminData] te initialiseren. Dit object is namelijk uniek en wordt bepaald door de configuratie, en kan dus geen fouten bevatten.

15.1.1.3. De klasse [TaxPayer]

De klasse [TaxPayer] zal een belastingplichtige modelleren:


# importen
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError


# een belastingplichtige
class TaxPayer(BaseEntity):
    # modelleert een belastingplichtige
    # id: identificatiecode
    # getrouwd: ja / nee
    # kinderen: het aantal kinderen
    # salaris: zijn jaarsalaris
    # belasting: bedrag van de te betalen belasting
    # toeslag: toeslag op de te betalen belasting
    # korting: korting op de te betalen belasting
    # korting: korting op de te betalen belasting
    # tarief: belastingtarief van de belastingplichtige

    # sleutels die zijn uitgesloten van de klasseoverzicht
    excluded_keys = []

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

    # eigenschappen
    @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

    # setters
    @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):
        # children moet een geheel getal zijn >=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):
        # salaris moet een geheel getal zijn >=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):
        # belasting moet een geheel getal zijn >=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):
        # korting moet een geheel getal zijn >= 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):
        # toeslag moet een geheel getal zijn >= 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):
        # de toeslag moet een geheel getal zijn >= 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):
        # tarief moet een reëel getal zijn >= 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")

Opmerkingen:

  • de klasse [TaxPayer] omvat een belastingplichtige;
  • regel 7: de klasse [TaxPayer] is afgeleid van de klasse [BaseEntity]. Ze heeft dus een identificatiecode [id];
  • regel 20: er wordt geen enkele eigenschap uitgesloten van de status van een object [AdminData];
  • regels 22-25: de eigenschappen van de klasse. Deze worden toegelicht in de regels 9-17;
  • regels 27-58: getters van de attributen van de klasse;
  • regels 60-161: de setters van de attributen van de klasse. Ter herinnering: het voordeel van een klasse die gegevens inkapselen ten opzichte van een eenvoudig woordenboek is dat de klasse de geldigheid van haar eigenschappen kan controleren dankzij haar setters;

15.1.2. De laag [dao]

Image

We gaan de implementaties van de lagen samenbrengen in een map [services]. Deze klassen zullen interfaces implementeren die zijn gedefinieerd in de map [interfaces].

Image

15.1.2.1. De interface [InterfaceImpôtsDao]

De laag [dao] zal de volgende interface [InterfaceImpôtsDao] implementeren (bestand InterfaceImpôtsDao.py):


# importen
from abc import ABC, abstractmethod


# interface IImpôtsDao
from AdminData import AdminData


class InterfaceImpôtsDao(ABC):
    # lijst met belastingschijven
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

    # lijst met belastingplichtigen
    @abstractmethod
    def get_taxpayers_data(self) -> dict:
        pass

    # boekingen van de resultaten van de belastingberekening
    @abstractmethod
    def write_taxpayers_results(self, taxpayers_results: list):
        pass

De interface definieert drie methoden:

  • [get_admindata]: dit is de methode die de tabel met belastingschijven ophaalt. Merk op dat er geen informatie wordt gegeven over de manier waarop deze gegevens moeten worden verkregen. Verderop zullen ze eerst in een tekstbestand en vervolgens in een database worden gevonden. Het is aan de klassen die de interface implementeren om zich aan te passen aan de wijze waarop de gegevens zijn opgeslagen. We zullen dus een klasse hebben om de belastingschijven uit een tekstbestand op te halen en een andere om ze uit een database op te halen. Beide zullen de methode [get_admindata] implementeren;
  • [get_taxpayers_data]: is de methode die de gegevens van de belastingplichtigen ophaalt. Ook hier specificeren we niet waar deze gegevens zich bevinden. We behandelen alleen het geval waarin ze in een tekstbestand staan;
  • [write_taxpayers_results]: is de methode die de resultaten van de belastingberekening opslaat. We geven niet aan waar. We behandelen alleen het geval waarin de resultaten in een tekstbestand worden opgeslagen. De parameter [taxpayers_results] is de lijst met resultaten die moeten worden opgeslagen;

15.1.2.2. De klasse [AbstractImpôtsDao]

De laag [dao] wordt geïmplementeerd door twee klassen:

  • de ene haalt de gegevens (belastingplichtigen, resultaten, belastingschijven) op uit tekstbestanden;
  • de andere haalt de gegevens (belastingplichtigen, resultaten) uit tekstbestanden en de belastingschijven uit een database;

De twee klassen zullen alleen verschillen in de verwerking van de belastingschijven. De gegevens van de belastingplichtigen en de resultaten van de belastingberekeningen zullen op dezelfde manier worden verwerkt. Daarom zullen we deze in een bovenliggende klasse [AbstractImpôtsDao] verwerken. De specifieke verwerking van de belastingschijven zal worden afgehandeld in twee onderliggende klassen:

  • de klasse [ImpôtsDaoWithAdminDataInJsonFile] haalt de belastingschijven op uit een tekstbestand in het formaat jSON;
  • de klasse [ImpôtsDaoWithAdminDataInDatabase] haalt de belastingschijven op uit een database;

De bovenliggende klasse [AbstractImpôtsDao] ziet er als volgt uit:


# importen
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


# basisklasse voor de laag [dao]
class AbstractImpôtsDao(InterfaceImpôtsDao):
    # de belastingplichtigen en hun belasting worden in tekstbestanden opgeslagen
    # constructor
    def __init__(self, config: dict):
        # config[taxpayersFilename]: de naam van het tekstbestand met de belastingplichtigen
        # config[resultsFilename]: de naam van het jSON-bestand met de resultaten
        # config[errorsFilename]: de naam van het foutbestand

        # de instellingen worden opgeslagen
        self.taxpayers_filename = config.get("taxpayersFilename")
        self.taxpayers_results_filename = config.get("resultsFilename")
        self.errors_filename = config.get("errorsFilename")

    # ------------------
    # interface IImpôtsDao
    # ------------------

    # lijst met gegevens van belastingplichtigen
    def get_taxpayers_data(self) -> dict:
        

    # het bijschrijven van de belasting van de belastingplichtigen
    def write_taxpayers_results(self, taxpayers: list):
        

    # belastingschijven uitlezen
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass
  • regel 13: de klasse [AbstractImpôtsDao] implementeert de interface [InterfaceImpôtsDao]. Daarom vinden we de drie methoden van deze interface:

    • [get_taxpayers_data]: regel 31;
    • [write_taxpayers_results]: regel 35;
    • [get_admindata]: regel 40. Deze methode wordt niet geïmplementeerd door de klasse [AbstractImpôtsDao] en is daarom als abstract gedeclareerd (regel 39);
  • regel 16: de constructor ontvangt een woordenboek [config] met de volgende informatie:

    • [taxpayersFilename]: de naam van het tekstbestand dat de gegevens van de belastingplichtigen bevat;
    • [resultsFilename]: de naam van het tekstbestand waarin de resultaten zullen worden opgeslagen;
    • [errorsFilename]: de naam van het tekstbestand met een overzicht van de fouten die zijn opgetreden bij de verwerking van het bestand [taxpayersFilename];

De methode [get_taxpayers_data] is als volgt:


    # lijst met belastingplichtigen
    def get_taxpayers_data(self) -> dict:
        # initialisaties
        taxpayers_data = []
        datafile = None
        erreurs = []
        try:
            # het gegevensbestand openen
            datafile = open(self.taxpayers_filename, "r")
            # de huidige regel van het bestand wordt verwerkt
            ligne = datafile.readline()
            # regelnummer
            numligne = 0
            while ligne != '':
                # een regel van +
                numligne += 1
                # spaties verwijderen
                ligne = ligne.strip()
                # lege regels en opmerkingen worden genegeerd
                if ligne != "" and ligne[0] != "#":
                    try:
                        # de 4 velden id,gehuwd,kinderen,salaris worden opgehaald die de belastingplichtige regel vormen
                        (id, marié, enfants, salaire) = ligne.split(",")
                        # er wordt een nieuwe regel aangemaakt: TaxPayer
                        taxpayers_data.append(
                            TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
                    except BaseException as erreur:
                        # de fout wordt genoteerd
                        erreurs.append(f"Ligne {numligne}, {erreur}")
                # er wordt een nieuwe belastingplichtige-regel ingelezen
                ligne = datafile.readline()
            # eventuele fouten worden vastgelegd
            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)
            # het resultaat wordt weergegeven
            return {"taxpayers": taxpayers_data, "erreurs": erreurs}
        except BaseException as erreur:
            # er wordt een uitzondering gegenereerd ImpôtsError
            raise ImpôtsError(11, f"{erreur}")
        finally:
            # het bestand wordt gesloten
            if datafile:
                datafile.close()
  • regel 4: de gegevens van de belastingplichtigen (gehuwd, kinderen, salaris) worden in een lijst met objecten van het type [TaxPayer] geplaatst;
  • regels 8-9: het tekstbestand met de belastingplichtigen wordt in leesmodus geopend. De inhoud ervan heeft de volgende vorm:
# geldige gegevens: id, getrouwd, kinderen, salaris
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
# er kunnen lege regels voorkomen

# er worden foutieve regels aangemaakt
# te weinig waarden
11,12
# te veel waarden
12,oui,3,200000, x, y
# foutieve waarden
x,x,x,x

In vergelijking met eerdere versies:

  • elke regel van het bestand [taxpayersFilename] begint met de identificatiecode van de belastingplichtige, een eenvoudig nummer;
  • opmerkingen en lege regels zijn toegestaan;
  • we gaan fouten afhandelen. Zo moeten de regels 17, 19 en 21 als foutief worden gemarkeerd. De fouten worden in een apart bestand vastgelegd;

Laten we verdergaan met het bekijken van de code:

  • regel 4: de gegevens uit het tekstbestand worden overgebracht naar de lijst [taxPayersData];
  • regels 14-31: het bestand met belastingplichtigen wordt regel voor regel gelezen;
  • regel 14: het einde van het bestand is bereikt wanneer een lege regel wordt gelezen (niets – zelfs geen regeleinde \r\n);
  • regel 20: lege regels en opmerkingen worden genegeerd. Een regel is een opmerking als, nadat de spaties voor en achter de tekst zijn verwijderd, het eerste teken het teken # is;
  • regel 24: een geldige regel bestaat uit vier velden, gescheiden door een komma. Deze worden opgehaald. Het toewijzen van gegevens aan een tuple met vier elementen mislukt als er niet precies vier gegevens worden toegewezen;
  • regel 25: als een van de vier opgehaalde velden [id, marié, enfants, salaire] ongeldig is, dan zal de methode [BaseEntity.fromdict] een uitzondering van het type [MyException] genereren;
  • regels 25-26: er wordt een object [TaxPayer] toegevoegd aan de lijst [taxpayers_data] van belastingplichtigen;
  • regels 27-29: eventuele fouten worden verzameld in een lijst [erreurs]. Deze lijst is aangemaakt in regel 6;
  • regels 33-36: de lijst met geconstateerde fouten wordt opgeslagen in het tekstbestand [errorsFilename]. Er zijn twee soorten fouten:
    • een regel had niet het juiste aantal verwachte velden;
    • de informatie in de regel was onjuist, waardoor het niet lukte om een object [TaxPayer] te construeren;
  • regels 39-41: elke fout (BaseException) wordt opgevangen en doorgegeven door deze in te kapselen in een type [ImpôtsError];
  • regels 42-45: in alle gevallen, of het nu lukt of mislukt, wordt het tekstbestand met de belastingplichtigen gesloten als het geopend was;

De methode [write_taxpayers_results] moet een bestand jSON opleveren in de vorm:


[
  {
    "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
  },

]

De methode [write_taxpayers_results] is als volgt:


    # het belastingbedrag van de belastingplichtigen wordt vastgelegd
    def write_taxpayers_results(self, taxpayers: list):
        # resultaten opslaan in een bestand jSON
        # belastingplichtigen: lijst met objecten van het type TaxPayer
        # (id, gehuwd, kinderen, salaris, belasting, toeslag, korting, vermindering, tarief)
        # de lijst [taxpayers] wordt opgeslagen in het tekstbestand [self.taxpayers_results_filename]
        file = None
        try:
            # het resultatenbestand wordt geopend
            file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
            # aanmaken van de lijst die moet worden geserialiseerd in jSON
            mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
            # serialisatie jSON
            json.dump(list(mapping), file, ensure_ascii=False)
        except BaseException as erreur:
            # de fout wordt opnieuw gegenereerd onder een ander type
            raise ImpôtsError(12, f"{erreur}")
        finally:
            # het bestand wordt gesloten als het geopend was
            if file:
                file.close()
  • regel 2: de methode ontvangt een lijst met belastingplichtigen [taxpayers] die zij moet opslaan in het tekstbestand [self.taxpayers_results_filename] in het formaat jSON;
  • regel 10: aanmaken van het resultatenbestand UTF-8;
  • regel 12: hier introduceren we de functie [map], waarvan de syntaxis hier [map (fonction, liste1)] is. De functie [fonction] wordt toegepast op elk element van [liste1] en produceert een nieuw element dat wordt toegevoegd aan een lijst [liste2]. Tot slot, voor elke i:

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

Hier is [liste1] de lijst [taxPayers], een lijst met objecten van het type [TaxPayer]. De functie [fonction] wordt hier uitgedrukt in de vorm van een zogenaamde functie [lambda], die de transformatie weergeeft die wordt uitgevoerd op een element [taxpayer] van de lijst [taxpayers]: elk element [taxpayer] wordt vervangen door zijn woordenboek [taxpayer.asdict()]. Uiteindelijk is de verkregen lijst [liste2] de lijst met woordenboeken van de elementen uit de lijst [taxpayers];

  • regel 12: het resultaat van de functie [map] is niet de lijst [liste2], maar een object van het type [map]. Om [liste2] te verkrijgen, moet de uitdrukking [list(mapping)] (regel 14) worden gebruikt;
  • regel 14: de lijst [liste2] wordt in het formaat jSON opgeslagen in het bestand [self.taxpayers_results_filename];
  • regels 15-17: elk type uitzondering wordt opgevangen en ingekapseld in een fout van het type [ImpôtsError] voordat deze opnieuw wordt uitgevoerd (regel 17);
  • regels 19-21: in alle gevallen, of het nu slaagt of mislukt, wordt het resultatenbestand gesloten als het geopend was;

15.1.2.3. Klasse [ImpôtsDaoWithAdminDataInJsonFile]

De klasse [ImpôtsDaoWithAdminDataInJsonFile] zal afleiden van de klasse [AbstractImpôtsDao] en de methode [getAdminData] implementeren die de bovenliggende klasse niet heeft geïmplementeerd. Deze klasse haalt de gegevens van de belastingdienst op uit een bestand 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
}

De klasse [ImpôtsDaoWithAdminDataInJsonFile] is als volgt:


# importen
import codecs
import json

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


# een implementatie van de laag [dao] waarbij de gegevens van de belastingdienst in een bestand jSON staan
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
    # constructor
    def __init__(self, config: dict):
        # config[admindataFilename]: de naam van het bestand jSON dat de gegevens van de belastingdienst bevat
        # config[taxpayersFilename]: de naam van het tekstbestand met de belastingplichtigen
        # config[resultsFilename]: de naam van het bestand jSON met de resultaten
        # config[errorsFilename]: de naam van het foutbestand

        # initialisatie van de klasse Parent
        AbstractImpôtsDao.__init__(self, config)
        # het inlezen van de gegevens van de belastingdienst
        file = None
        try:
            # het bestand jSON met belastinggegevens openen in leesmodus
            file = codecs.open(config["admindataFilename"], "r", "utf8")
            # de inhoud van het bestand jSON wordt overgebracht naar een object [AdminData]
            self.admindata = AdminData().fromdict(json.load(file))
        except BaseException as erreur:
            # de fout wordt opnieuw gegenereerd in de vorm van een type [ImpôtsError]
            raise ImpôtsError(21, f"{erreur}")
        finally:
            # het bestand wordt gesloten als het geopend was
            if file:
                file.close()

    # -------------
    # interface
    # -------------

    # gegevens ophalen bij de belastingdienst
    # de methode retourneert een object [AdminData]
    def get_admindata(self) -> AdminData:
        return self.admindata
  • regel 11: de klasse [ImpôtsDaoWithAdminDataInJsonFile] erft van de klasse [AbstractImpôtsDao]. Als zodanig implementeert deze de interface [InterfaceImpôtsDao];
  • regel 13: de constructor ontvangt als parameter een woordenboek met de informatie uit de regels 14-17;
  • regel 20: de bovenliggende klasse wordt geïnitialiseerd;
  • regel 24: het bestand jSON met de gegevens van de belastingdienst wordt geopend;
  • regel 25: het bestand UTF-8 met gegevens van de belastingdienst wordt geopend;
  • regel 27: de inhoud van het bestand wordt gelezen en in het object [self.admindata] van het type [AdminData] geplaatst. De sleutels van het bestand jSON moeten overeenkomen met de toegestane eigenschappen voor een object van het type [AdminData]; anders genereert de methode [fromdict] een uitzondering;
  • regels 28-30: uitzonderingsafhandeling. De uitzonderingen die kunnen optreden, worden ingekapseld in een type [ImpôtsError] voordat ze opnieuw worden gegenereerd;
  • regels 32-34: het bestand wordt gesloten als het geopend was;
  • regels 42-43: implementatie van de methode [get_admindata] van de interface [InterfaceImpôtsDao];

15.1.3. De laag [métier]

Image

15.1.3.1. De interface [InterfaceImpôtsMétier]

De interface van de laag [métier] zal als volgt zijn:


# imports
from abc import ABC, abstractmethod

from AdminData import AdminData
from TaxPayer import TaxPayer


# interface IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
    # belastingberekening voor 1 belastingplichtige
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        pass
  • De interface [InterfaceImpôtsMétier] definieert één enkele methode:
  • regel 12: met de methode [calculate_tax] kan de belasting van één enkele belastingplichtige [taxpayer] worden berekend. [admindata] is het object [AdminData] waarin de gegevens van de belastingdienst zijn ingekapseld;
  • regel 12: de methode [calculate_tax] levert geen resultaat op. De verkregen gegevens (belasting, toeslag, korting, vermindering, tarief) worden opgenomen in de parameter [taxpayer]: vóór de aanroep zijn deze attributen leeg, na de aanroep zijn ze geïnitialiseerd;

15.1.3.2. De klasse [ImpôtsMétier]

De klasse [ImpôtsMétier] implementeert de interface [InterfaceImpôtsMétier] als volgt:

Image

De methoden van de klasse zijn afkomstig uit de module [impôts_module_02] uit de paragraaf |De module [impots.v02.modules.impôts_module_02]|. We hebben het aantal parameters van de methoden beperkt tot twee:

  • taxpayer(id, gehuwd, kinderen, salaris, belasting, korting, toeslag, vermindering, tarief): het object dat een belastingplichtige en zijn belasting vertegenwoordigt;
  • admindata: het object dat de gegevens van de belastingdienst omvat;

We laten aan de hand van een methode zien welke wijzigingen er zijn aangebracht;


    # belastingberekening - fase 1
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        # belastingplichtige (id, gehuwd, kinderen, salaris, belasting, korting, toeslag, vermindering, tarief)
        # admindata: gegevens van de belastingdienst

        # belastingberekening met kinderen
        self.calculate_tax_2(taxpayer, admindata)
        # de resultaten staan in taxpayer
        taux1 = taxpayer.taux
        surcôte1 = taxpayer.surcôte
        impot1 = taxpayer.impôt
        # belastingberekening zonder kinderen
        if taxpayer.enfants != 0:
            # belastingberekening voor dezelfde belastingplichtige zonder kinderen
            taxpayer2 = TaxPayer().fromdict(
                {'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
            self.calculate_tax_2(taxpayer2, admindata)
            # de resultaten staan in taxpayer2
            taux2 = taxpayer2.taux
            surcôte2 = taxpayer2.surcôte
            impot2 = taxpayer2.impôt
            # toepassing van de maximering van het gezinsquotiënt
            if taxpayer.enfants < 3:
                # PLAFOND_QF_DEMI_PART euro voor de eerste twee kinderen
                impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
            else:
                # PLAFOND_QF_DEMI_PART euro voor de eerste twee kinderen, het dubbele voor de volgende
                impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
                         * 2 * admindata.plafond_qf_demi_part
        else:
            # als de belastingplichtige geen kinderen heeft, dan is belasting2 = belasting1
            impot2 = impot1

        # we nemen de hoogste belasting met het bijbehorende tarief en de bijbehorende toeslag
        (impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
            impot2, impot2 - impot1 + surcôte2, taux2)

        # tussentijdse resultaten
        taxpayer.impôt = impot
        taxpayer.surcôte = surcôte
        taxpayer.taux = taux
        # berekening van een eventuele korting
        self.get_décôte(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.décôte
        # berekening van een eventuele belastingvermindering
        self.get_réduction(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.réduction
        # resultaat
        taxpayer.impôt = math.floor(taxpayer.impôt)
  • regel 3: de methode [calculate_tax] is de enige methode van de interface [InterfaceImpôtsMétier]. Deze methode heeft twee parameters:
    • [tapPayer]: de belastingplichtige voor wie de belasting wordt berekend;
    • [admindata]: het object dat de gegevens van de belastingdienst bevat;
    • de resultaten van de berekening worden ingekapseld in de parameter [taxpayer] (regels 40-50). De inhoud van dit object is dus niet hetzelfde vóór en na het aanroepen van de methode;

15.1.4. Tests van de lagen [dao] en [métier]

Image

  • [TestDaoMétier] is de testklasse UnitTest voor de lagen [dao] en [métier];
  • [config] is het configuratiebestand voor de tests;

De configuratie [config] is als volgt:


def configure():
    import os

    # stap 1 ------
    # configuratie van het Python-pad

    # map van dit bestand
    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"

    # absolute afhankelijkheden van de applicatie
    absolute_dependencies = [
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
        f"{root_dir}/02/entities",
    ]

    # we stellen de syspath in
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # stap 2 ------
    # configuratie van de applicatie
    config = {
        # absolute paden naar de applicatiebestanden
        "admindataFilename"f"{script_dir}/../data/input/admindata.json"
    }

    # instantie van de applicatielagen
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

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

    # de laaginstanties worden in de configuratie opgenomen
    config["dao"] = dao
    config["métier"] = métier

    # de configuratie wordt teruggegeven
    return config
  • regels 4-23: hier wordt het Python-pad voor de tests geconfigureerd;
  • regels 32-41: de lagen [dao] en [métier] worden geïnstantieerd. Hun referenties worden in het woordenboek [config] geplaatst;
  • regel 44: dit woordenboek wordt teruggegeven;

De testklasse [TestDaoMétier] ziet er als volgt uit:


import unittest


def get_config() -> dict:
    # configuratie van de applicatie
    import config
    # de configuratie wordt teruggegeven
    return config.configure()


class TestDaoMétier(unittest.TestCase):

    # uitgevoerd vóór elke methode test_
    def setUp(self) -> None:
        # de testconfiguratie wordt opgehaald
        config = get_config()
        # we slaan wat informatie op
        self.métier = config['métier']
        self.admindata = config['dao'].get_admindata()

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

        # {'getrouwd': 'ja', 'kinderen': 2, 'salaris': 55555,
        # 'belasting': 2814, 'toeslag': 0, 'korting': 0, 'vermindering': 0, 'tarief': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controle
        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

        # {'getrouwd': 'ja', 'kinderen': 2, 'salaris': 50000,
        # 'belasting': 1384, 'toeslag': 0, 'korting': 384, 'vermindering': 347, 'tarief': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'ja', 'kinderen': 3, 'salaris': 50000,
        # 'belasting': 0, 'toeslag': 0, 'korting': 720, 'vermindering': 0, 'tarief': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'nee', 'kinderen': 2, 'salaris': 100000,
        # 'belasting': 19884, 'toeslag': 4480, 'korting': 0, 'vermindering': 0, 'tarief': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'nee', 'kinderen': 3, 'salaris': 100000,
        # 'belasting': 16782, 'toeslag': 7176, 'korting': 0, 'vermindering': 0, 'tarief': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'ja', 'kinderen': 3, 'salaris': 100000,
        # 'belasting': 9200, 'toeslag': 2180, 'korting': 0, 'vermindering': 0, 'tarief': 0,3}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'ja', 'kinderen': 5, 'salaris': 100000,
        # 'belasting': 4230, 'toeslag': 0, 'korting': 0, 'vermindering': 0, 'tarief': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'nee', 'kinderen': 0, 'salaris': 100000,
        # 'belasting': 22986, 'toeslag': 0, 'korting': 0, 'vermindering': 0, 'tarief': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'ja', 'kinderen': 2, 'salaris': 30000,
        # 'belasting': 0, 'toeslag': 0, 'korting': 0, 'vermindering': 0, 'tarief': 0}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'nee', 'kinderen': 0, 'salaris': 200000,
        # 'belasting': 64210, 'toeslag': 7498, 'korting': 0, 'vermindering': 0, 'tarief': 0,45}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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

        # {'getrouwd': 'ja', 'kinderen': 3, 'salaris': 200000,
        # 'belasting': 42842, 'toeslag': 17283, 'korting': 0, 'vermindering': 0, 'tarief': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # controles
        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()

Opmerkingen

  • regel 11: de testklasse is een uitbreiding van de klasse [unittest.TestCase];
  • regels 13-19: in een test UnitTest wordt de methode [setUp] uitgevoerd vóór elk van de methoden [test_];
  • regel 16: de configuratie uit het eerder besproken script [config] wordt opgehaald;
  • regel 18: er wordt een verwijzing naar de laag [métier] opgeslagen;
  • regel 19: bij de laag [dao] wordt het object [AdminData] opgevraagd, dat de gegevens van de belastingdienst bevat, en dit wordt opgeslagen;
  • regels 21-173: 11 tests waarvan de resultaten zijn geverifieerd op de officiële website van de belastingdienst 2019 |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
  • regels 21-33: alle tests zijn volgens hetzelfde model opgesteld;
  • regel 22: de klasse [TaxPayer] wordt geïmporteerd;
  • regel 24: geteste belastingplichtige;
  • regel 25: verwachte resultaten;
  • regel 26: het object [TaxPayer] van de belastingplichtige wordt aangemaakt;
  • regel 27: berekening van zijn belasting. Het resultaat staat in [taxpayer];
  • regels 29-33: controle van de verkregen resultaten;
  • regel 29: het belastingbedrag wordt tot op de euro nauwkeurig gecontroleerd. Uit de tests is namelijk gebleken dat de resultaten die door het algoritme in dit document worden verkregen, maximaal 1 euro kunnen afwijken van de officiële cijfers;

De uitvoering van de tests levert de volgende resultaten op:

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. Hoofdscript

Image

Het hoofdscript wordt geconfigureerd door het volgende script [config]:


def configure():
    import os

    # stap 1 ------
    # configuratie van het Python-pad

    # map van dit bestand
    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"

    # afhankelijkheden van de applicatie
    absolute_dependencies = [
        # lokale afhankelijkheden
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    # we stellen de syspath in
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # stap 2 ------
    # configuratie van de applicatie
    config = {
        # absolute paden naar de applicatiebestanden
        "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"
    }

    # instantiëren van de applicatielagen
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

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

    # de laaginstanties worden in de configuratie opgenomen
    config["dao"] = dao
    config["métier"] = métier

    # de configuratie wordt opgevraagd
    return config

Het is vergelijkbaar met het script dat wordt gebruikt voor het testen van de lagen [métier] en [dao].

Het hoofdscript [main.py] is als volgt:


# de applicatie configureren
import config

config = config.configure()

# imports
from ImpôtsError import ImpôtsError

# de lagen van de applicatie worden opgehaald (ze zijn al geïnstantieerd)
dao = config["dao"]
métier = config["métier"]

try:
    # het ophalen van de belastingschijven
    admindata = dao.get_admindata()
    # het inlezen van de gegevens van de belastingplichtigen
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # van de belastingplichtigen?
    if not taxpayers:
        raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # de belasting van de belastingplichtigen berekenen
    for taxpayer in taxpayers:
        # 'belastingplichtige' is zowel een invoer- als een uitvoerparameter
        # 'taxpayer' wordt gewijzigd
        métier.calculate_tax(taxpayer, admindata)
    # de resultaten worden naar een tekstbestand geschreven
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    # foutmelding weergeven
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # voltooid
    print("Travail terminé...")

Opmerkingen

  • regels 2-4: de configuratie van de applicatie wordt opgehaald. We weten ook dat het Python-pad van de applicatie is samengesteld;
  • regels 9-11: er worden verwijzingen opgehaald naar de lagen [métier] en [dao];
  • regel 15: de gegevens van de belastingdienst worden opgehaald;
  • regel 17: we halen de lijst op van belastingplichtigen voor wie de belasting moet worden berekend;
  • regels 19-20: als deze lijst leeg is, wordt er een uitzondering gegenereerd;
  • regels 22-25: berekening van de belasting voor de verschillende objecten [taxpayer] met behulp van de laag [métier];
  • regel 27: [taxpayers] is nu een lijst met objecten [TaxPayer] waarin de attributen (belasting, korting, toeslag, vermindering, tarief) een waarde hebben gekregen. Deze lijst wordt weggeschreven naar een bestand jSON;
  • regels 28-30: opvangen van een eventuele fout;
  • regels 31-33: worden in alle gevallen uitgevoerd;

Het uitvoeren van het script levert dezelfde resultaten op als in eerdere versies. Het bestand met fouten van belastingplichtigen was een nieuw onderdeel in deze versie. Na het uitvoeren van het script [main] is de inhoud ervan als volgt:


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]

De foutieve regels waren de volgende:

# geldige gegevens: id, gehuwd, kinderen, salaris
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
# er kunnen lege regels voorkomen

# er worden foutieve regels aangemaakt
# te weinig waarden
11,12
# te veel waarden
12,oui,3,200000, x, y
# foutieve waarden
x,x,x,x

15.2. Versie 4 – toepassing 2

In deze versie voert de gebruiker de lijst met belastingplichtigen via het toetsenbord in. De architectuur van de toepassing ziet er als volgt uit:

Image

Er verschijnt een nieuwe module: de laag [ui] (gebruikersinterface) die met de gebruiker zal communiceren. Deze laag zal een interface hebben en door een klasse worden geïmplementeerd.

Image

15.2.1. De interface [InterfaceImpôtsUi]


# importen
from abc import ABC, abstractmethod


# interface InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
    # uitvoering van de klasse die de interface implementeert
    @abstractmethod
    def run(self):
        pass

De interface [InterfaceImpôtsUi] zal slechts één methode hebben, namelijk die in de regels 8-10. De interface wordt hier geïmplementeerd met een console-applicatie, maar zou ook met een grafische gebruikersinterface kunnen worden geïmplementeerd. De parameters die aan de methode [run] worden doorgegeven, zouden in beide implementaties niet hetzelfde zijn. Om dit probleem te omzeilen, is de gebruikelijke werkwijze:

  • geen parameters (of zo min mogelijk parameters) door te geven aan de methode [run];
  • parameters doorgeven aan de constructor van de klasse die de interface implementeert. Deze kunnen per implementatie verschillen. Deze parameters worden opgeslagen als attributen van de klasse;
  • ervoor zorgen dat de methode [run] deze klasse-attributen (self.x) gebruikt;

Deze methode maakt het mogelijk om een zeer algemene interface te hebben die wordt gespecificeerd door de parameters van de constructors van elke implementatieklasse. Deze methode is al gebruikt voor de modulaire versie nr. 1.

15.2.2. De klasse [ImpôtsConsole]

De klasse [ImpôtsConsole] implementeert de interface [InterfaceImpôtsUi] als volgt:


# imports
import re

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


# laag [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
    # constructor
    def __init__(self, config: dict):
        # de parameters worden opgeslagen
        self.admindata = config['dao'].get_admindata()
        self.métier = config['métier']

    def run(self):
        # interactieve dialoog met de gebruiker
        fini = False
        while not fini:
            # is de belastingplichtige gehuwd?
            marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # controle van de geldigheid van de invoer
            while marié != "oui" and marié != "non" and marié != "*":
                # foutmelding
                print("Tapez oui ou non ou *")
                # vraag opnieuw
                marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # Klaar?
            if marié == "*":
                # dialoogvenster gesloten
                return
            # aantal kinderen
            enfants = input("Nombre d'enfants : ").strip()
            # controle van de geldigheid van de invoer
            if not re.match(r"^\d+$", enfants):
                # foutmelding
                print("Tapez un nombre entier positif ou nul")
                # opnieuw proberen
                enfants = input("Nombre d'enfants : ").strip()
            # jaarsalaris
            salaire = input("Salaire annuel : ").strip()
            # controle van de geldigheid van de invoer
            if not re.match(r"^\d+$", salaire):
                # foutmelding
                print("Tapez un nombre entier positif ou nul")
                # opnieuw proberen
                salaire = input("Salaire annuel : ").strip()
            # belastingberekening
            taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
            self.métier.calculate_tax(taxpayer, self.admindata)
            # weergave
            print(f"Impôt du contribuable = {taxpayer}\n\n")
            # volgende belastingplichtige
  • regel 9: de klasse [ImpôtsConsole] implementeert de interface [InterfaceImpôtsUi];
  • regel 11: de constructor van de klasse ontvangt een parameter, het woordenboek [config] uit de configuratie van de applicatie;
    • regel 13: de gegevens van de belastingdienst worden opgehaald om de belasting te berekenen;
    • regel 14: er wordt een referentie opgeslagen in de laag [métier];
  • regel 16: implementatie van de methode [run] van de interface;
  • regels 19-53: dialoog met de gebruiker. Hierbij
    • het opvragen van de drie gegevens (gehuwd, kinderen, salaris) van de belastingplichtige;
    • zijn belasting te berekenen;
    • deze weer te geven;
    • de dialoog eindigt wanneer de gebruiker * antwoordt op de eerste vraag;
  • regels 20-27: er wordt gevraagd of de belastingplichtige gehuwd is en de geldigheid van het antwoord wordt gecontroleerd;
  • regels 29-31: als de gebruiker ‘*’ op de vraag heeft geantwoord, wordt de dialoog beëindigd;
  • regels 32-39: er wordt gevraagd naar het aantal kinderen van de belastingplichtige en de geldigheid van het antwoord wordt gecontroleerd;
  • regels 40-47: er wordt gevraagd naar het jaarsalaris van de belastingplichtige en de geldigheid van het antwoord wordt gecontroleerd;
  • regels 48-50: met deze gegevens wordt via de laag [métier] de belasting van de belastingplichtige berekend;
  • regel 52: het belastingbedrag wordt weergegeven;

15.2.3. Het hoofdscript

Het hoofdscript [main] wordt geconfigureerd door het volgende bestand [config]:


def configure():
    import os

    # stap 1 ------
    # configuratie van het Python-pad

    # map van dit bestand
    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"

    # afhankelijkheden van de applicatie
    absolute_dependencies = [
        # lokale afhankelijkheden
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    # we stellen de syspath in
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # stap 2 ------
    # configuratie van de applicatie
    config = {
        # absolute paden naar de applicatiebestanden
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json",
    }

    # instantie van de applicatielagen
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier
    from ImpôtsConsole import ImpôtsConsole

    # DAO-laag
    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    # businesslaag
    métier = ImpôtsMétier()
    # de instanties van de lagen worden in de configuratie opgenomen
    config["dao"] = dao
    config["métier"] = métier
    # UI-laag
    ui = ImpôtsConsole(config)
    config["ui"] = ui

    # we genereren de configuratie
    return config

Het coördinerende script is als volgt (main.py):


# de applicatie configureren
import config

config = config.configure()

# imports
from ImpôtsError import ImpôtsError

# de lagen van de applicatie ophalen (deze zijn al geïnstantieerd)
ui = config["ui"]

# code
try:
    # uitvoering van de laag [ui]
    ui.run()
except ImpôtsError as erreur:
    # de foutmelding wordt weergegeven
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # wordt in alle gevallen uitgevoerd
    print("Travail terminé...")
  • regels 1-4: de configuratie van de applicatie wordt opgehaald;
  • regel 10: er wordt een verwijzing opgehaald naar de laag [ui];
  • regels 12-21: de structuur van de code is dezelfde als in de vorige applicatie: code omgeven door een try/catch om eventuele uitzonderingen op te vangen;
  • regel 15: de laag [ui] wordt gevraagd om uit te voeren: de dialoog met de gebruiker begint dan;
  • regels 16-18: opvangen van een eventuele uitzondering;

Hier volgt een voorbeeld van de uitvoering:


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