15. Toepassingsopgave - versie 4

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:

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:

- 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:

15.1.1. De entiteiten

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]

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

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:
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]

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:

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]

- [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:

15.1.5. Hoofdscript

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:
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:

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.

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