Skip to content

15. Esercizio pratico - Versione 4

Image

Qui riprendiamo l'esercizio descritto nella sezione |Versione 3| e lo implementiamo utilizzando classi e interfacce. Scriveremo due applicazioni:

L'applicazione 1 sarà la seguente:

Image

Uno script principale [main] istanzierà un livello [DAO] e un livello [business]:

  • il livello [DAO] sarà responsabile della gestione dei dati memorizzati in file di testo e successivamente in un database;
  • il livello [business] sarà responsabile del calcolo dell'imposta;

In questa applicazione non ci saranno input da parte dell'utente: i dati del contribuente si troveranno in un file di testo il cui nome verrà passato al modulo [main].

Nell'Applicazione 2, l'utente inserirà i dati del contribuente tramite la tastiera. L'architettura si evolverà quindi come segue:

Image

  • Il livello [DAO] (Data Access Object) gestisce l'accesso ai dati esterni
  • il livello [business] gestisce la logica di business, in questo caso il calcolo delle imposte. Non gestisce i dati. Questi dati possono provenire da due fonti:
    • il livello [DAO] per i dati persistenti;
    • il livello [UI] per i dati forniti dall'utente.
  • il livello [ui] (User Interface) gestisce le interazioni con l'utente;
  • [main] funge da orchestratore;

Di seguito, i livelli [dao], [business] e [ui] saranno implementati ciascuno utilizzando una classe. I livelli [business] e [dao] saranno gli stessi per entrambe le applicazioni. Questo è il motivo per cui sono stati combinati in un'unica versione dell'esercizio dell'applicazione.

15.1. Versione 4 – Applicazione 1

La versione 4 calcola l'imposta per un elenco di contribuenti memorizzato in un file di testo. Ha la seguente struttura:

Image

15.1.1. Entità

Image

Le entità sono classi di dati. Il loro ruolo è quello di incapsulare i dati e fornire metodi getter/setter che consentono di verificare la validità dei dati. Le entità vengono scambiate tra i livelli. Una singola entità può passare dal livello [ui] al livello [dao] e viceversa.

15.1.1.1. La classe [ImpôtsError]

Useremo una classe di eccezione personalizzata:

1
2
3
4
5
6
7
# -------------------------------
#  exceptional class
from MyException import MyException


class ImpôtsError(MyException):
    pass

Non appena i livelli [business] e [DAO] incontrano un problema, generano questa eccezione. Essa deriva dalla classe [MyException]. Viene quindi utilizzata come segue: [raise ImpôtsError(error_code, error_message)].

15.1.1.2. La classe [AdminData]

La classe [AdminData] incapsula le costanti utilizzate nei calcoli fiscali:

from BaseEntity import BaseEntity


#  tax administration data
class AdminData(BaseEntity):
    #  keys excluded from class state
    excluded_keys = []

    #  auroralized keys
    @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"
        ]
  • Riga 5: La classe [AdminData] estende la classe [BaseEntity] descritta nella sezione |BaseEntity|. Ricordiamo che le classi che estendono la classe [BaseEntity] devono definire:
    • un attributo di classe [excluded_keys] (riga 7) che elenca le proprietà dell'oggetto escluse quando l'oggetto viene convertito in un dizionario;
    • un metodo statico [get_allowed_keys] (righe 10–26) che restituisce l'elenco delle proprietà accettate quando l'oggetto viene inizializzato con un dizionario;

Non abbiamo utilizzato i setter per convalidare i dati utilizzati per inizializzare un oggetto [AdminData]. Questo perché tale oggetto è unico e definito dalla configurazione, e quindi è improbabile che contenga errori.

15.1.1.3. La classe [TaxPayer]

La classe [TaxPayer] modellerà un contribuente:

#  imports
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError


#  a taxpayer
class TaxPayer(BaseEntity):
    #  models a taxpayer
    #  id: identifier
    #  married: yes / no
    #  children: number of children
    #  salary: annual salary
    #  tax: amount of tax payable
    #  surcôte: additional tax to pay
    #  discount: discount on tax payable
    #  reduction: reduction in tax payable
    #  rate: the taxpayer's tax rate

    #  keys excluded from class state
    excluded_keys = []

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

    #  properties
    @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 must be an integer >=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):
        #  salary must be an integer >=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):
        #  tax must be an integer >=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):
        #  discount must be an integer >=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):
        #  surcharge must be an integer >=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):
        #  surcharge must be an integer >=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):
        #  rate must be real >=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")

Note:

  • La classe [TaxPayer] incapsula un contribuente;
  • riga 7: la classe [TaxPayer] deriva dalla classe [BaseEntity]. Ha quindi un identificatore [id];
  • riga 20: nessuna proprietà è esclusa dallo stato di un oggetto [AdminData];
  • righe 22–25: le proprietà della classe. Queste sono spiegate nelle righe 9–17;
  • righe 27–58: i getter per gli attributi della classe;
  • righe 60–161: i setter per gli attributi della classe. Ricordiamo che il vantaggio di una classe che incapsula i dati rispetto a un semplice dizionario è che la classe può verificare la validità delle sue proprietà utilizzando i propri setter;

15.1.2. Il livello [dao]

Image

Raggrupperemo le implementazioni del livello in una cartella [services]. Queste classi implementeranno le interfacce definite nella cartella [interfaces].

Image

15.1.2.1. L'interfaccia [InterfaceImpôtsDao]

Il livello [dao] implementerà la seguente interfaccia [InterfaceImpôtsDao] (file InterfaceImpôtsDao.py):

#  imports
from abc import ABC, abstractmethod


#  interface IImpôtsDao
from AdminData import AdminData


class InterfaceImpôtsDao(ABC):
    #  list of tax brackets
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

    #  list of taxpayer data
    @abstractmethod
    def get_taxpayers_data(self) -> dict:
        pass

    #  entering tax calculation results
    @abstractmethod
    def write_taxpayers_results(self, taxpayers_results: list):
        pass

L'interfaccia definisce tre metodi:

  • [get_admindata]: è il metodo che recupera la tabella delle fasce di imposta. Si noti che non vengono fornite informazioni su come ottenere questi dati. In seguito, essi saranno inizialmente disponibili in un file di testo e successivamente in un database. Spetterà alle classi che implementano l'interfaccia adattarsi al metodo di archiviazione dei dati. Avremo quindi una classe per recuperare le fasce di imposta da un file di testo e un'altra per recuperarle da un database. Entrambe implementeranno il metodo [get_admindata];
  • [get_taxpayers_data]: è il metodo che recupera i dati dei contribuenti. Anche in questo caso, non specifichiamo dove si troveranno. Tratteremo solo il caso in cui si trovino in un file di testo;
  • [write_taxpayers_results]: è il metodo che salverà i risultati del calcolo delle imposte. Non specifichiamo dove. Gestiremo solo il caso in cui i risultati vengano salvati in un file di testo. Il parametro [taxpayers_results] sarà l'elenco dei risultati da salvare;

15.1.2.2. La classe [AbstractImpôtsDao]

Il livello [dao] sarà implementato da due classi:

  • una recupererà i dati (contribuenti, risultati, scaglioni fiscali) dai file di testo;
  • l'altra recupererà i dati (contribuenti, risultati) dai file di testo e le fasce di imposta da un database;

Le due classi differiranno solo per il modo in cui gestiscono le fasce di imposta. I dati dei contribuenti e i risultati del calcolo delle imposte saranno gestiti allo stesso modo. Per questo motivo, li gestiremo in una classe padre [AbstractImpôtsDao]. La gestione specifica delle fasce di imposta sarà gestita in due classi figlie:

  • la classe [ImpôtsDaoWithAdminDataInJsonFile] recupererà le fasce di imposta da un file di testo in formato JSON;
  • la classe [ImpôtsDaoWithAdminDataInDatabase] recupererà le fasce di imposta da un database;

La classe padre [AbstractImpôtsDao] sarà la seguente:

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


#  base class for the [dao] layer
class AbstractImpôtsDao(InterfaceImpôtsDao):
    #  taxpayers and their taxes will be stored in text files
    #  manufacturer
    def __init__(self, config: dict):
        #  config[taxpayersFilename]: name of the taxpayer text file
        #  config[resultsFilename]: the name of the jSON results file
        #  config[errorsFilename]: the name of the error file

        #  save parameters
        self.taxpayers_filename = config.get("taxpayersFilename")
        self.taxpayers_results_filename = config.get("resultsFilename")
        self.errors_filename = config.get("errorsFilename")

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

    #  list of taxpayer data
    def get_taxpayers_data(self) -> dict:
        

    #  tax entry for taxpayers
    def write_taxpayers_results(self, taxpayers: list):
        

    #  reading tax brackets
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass
  • Riga 13: La classe [AbstractImpôtsDao] implementa l'interfaccia [InterfaceImpôtsDao]. Pertanto, contiene i tre metodi di questa interfaccia:
    • [get_taxpayers_data]: riga 31;
    • [write_taxpayers_results]: riga 35;
    • [get_admindata]: riga 40. Questo metodo non verrà implementato dalla classe [AbstractImpôtsDao], quindi è dichiarato astratto (riga 39);
  • riga 16: il costruttore riceve un dizionario [config] contenente le seguenti informazioni:
    • [taxpayersFilename]: il nome del file di testo contenente i dati dei contribuenti;
    • [resultsFilename]: il nome del file di testo in cui verranno memorizzati i risultati;
    • [errorsFilename]: il nome del file di testo che elenca gli errori riscontrati durante l'elaborazione del file [taxpayersFilename];

Il metodo [get_taxpayers_data] è il seguente:

    #  list of taxpayer data
    def get_taxpayers_data(self) -> dict:
        #  initializations
        taxpayers_data = []
        datafile = None
        erreurs = []
        try:
            #  open data file
            datafile = open(self.taxpayers_filename, "r")
            #  the current line of the
            ligne = datafile.readline()
            #  line no
            numligne = 0
            while ligne != '':
                #  a + line
                numligne += 1
                #  remove the whites
                ligne = ligne.strip()
                #  ignore empty lines and comments
                if ligne != "" and ligne[0] != "#":
                    try:
                        #  we retrieve the 4 fields id,married,children,salary which form the taxpayer line
                        (id, marié, enfants, salaire) = ligne.split(",")
                        #  create a new TaxPayer
                        taxpayers_data.append(
                            TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
                    except BaseException as erreur:
                        #  we note the error
                        erreurs.append(f"Ligne {numligne}, {erreur}")
                #  a new taxpayer line reads
                ligne = datafile.readline()
            #  record errors if any
            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)
            #  we return the result
            return {"taxpayers": taxpayers_data, "erreurs": erreurs}
        except BaseException as erreur:
            #  throw a ImpôtsError exception
            raise ImpôtsError(11, f"{erreur}")
        finally:
            #  close the file
            if datafile:
                datafile.close()
  • riga 4: i dati del contribuente (stato civile, figli, stipendio) saranno inseriti in un elenco di oggetti di tipo [TaxPayer];
  • righe 8-9: apriamo il file di testo del contribuente per la lettura. Il suo contenuto ha il seguente formato:
# données valides : id, marié, enfants, salaire
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
# on peut avoir des lignes vides

# on crée des lignes erronées
# pas assez de valeurs
11,12
# trop de valeurs
12,oui,3,200000, x, y
# des valeurs erronées
x,x,x,x

Rispetto alle versioni precedenti:

  • Ogni riga nel file [taxpayersFilename] inizia con l'ID del contribuente, un singolo numero;
  • sono ammessi commenti e righe vuote;
  • gestiremo gli errori. Pertanto, le righe 17, 19 e 21 devono essere contrassegnate come non valide. Gli errori vengono registrati in un file separato;

Continuiamo a esaminare il codice:

  • riga 4: i dati dal file di testo vengono trasferiti alla lista [taxPayersData];
  • righe 14–31: il file dei contribuenti viene letto riga per riga;
  • riga 14: la fine del file viene raggiunta quando viene letta una riga vuota (nulla, nemmeno il carattere di fine riga \r\n);
  • riga 20: le righe vuote e i commenti vengono ignorati. Una riga è un commento se, dopo aver rimosso gli spazi bianchi prima e dopo il testo, il primo carattere è il carattere #;
  • riga 24: una riga valida è composta da quattro campi separati da virgole. Questi vengono recuperati. L'assegnazione dei dati a una tupla di quattro elementi fallisce se non ci sono esattamente quattro punti dati assegnati;
  • riga 25: se uno qualsiasi dei quattro campi recuperati [id, married, children, salary] non è valido, il metodo [BaseEntity.fromdict] genererà un'eccezione [MyException];
  • righe 25–26: un oggetto [TaxPayer] viene aggiunto alla lista [taxpayers_data] dei contribuenti;
  • righe 27–29: eventuali errori vengono raccolti in un elenco [errors]. Questo elenco è stato creato alla riga 6;
  • righe 33–36: l'elenco degli errori riscontrati viene salvato nel file di testo [errorsFilename]. Esistono due tipi di errori:
    • una riga non aveva il numero corretto di campi previsti;
    • le informazioni nella riga erano errate e non è stato possibile costruire un oggetto [TaxPayer];
  • righe 39–41: qualsiasi errore (BaseException) viene intercettato e propagato avvolgendo lo in un tipo [TaxPayerError];
  • righe 42–45: in tutti i casi, indipendentemente dal successo o meno, il file di testo del contribuente viene chiuso se era stato aperto;

Il metodo [write_taxpayers_results] deve produrre un file JSON nel seguente formato:


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

]

Il metodo [write_taxpayers_results] è il seguente:

    #  tax entry for taxpayers
    def write_taxpayers_results(self, taxpayers: list):
        #  writing results to a jSON file
        #  taxpayers: list of objects of type TaxPayer
        #  (id, married, children, salary, tax, surcharge, discount, reduction, rate)
        #  the [taxpayers] list is saved in text file [self.taxpayers_results_filename]
        file = None
        try:
            #  opening the results file
            file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
            #  creation of the list to be serialized in jSON
            mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
            #  serialization jSON
            json.dump(list(mapping), file, ensure_ascii=False)
        except BaseException as erreur:
            #  restart the error with another type
            raise ImpôtsError(12, f"{erreur}")
        finally:
            #  close the file if it has been opened
            if file:
                file.close()
  • riga 2: il metodo riceve un elenco di contribuenti [taxpayers] che deve salvare nel file di testo [self.taxpayers_results_filename] in formato JSON;
  • riga 10: creazione del file dei risultati in UTF-8;
  • riga 12: qui introduciamo la funzione [map], la cui sintassi in questo caso è [map (funzione, lista1)]. La [funzione] viene applicata a ciascun elemento di [lista1] e produce un nuovo elemento che va ad arricchire una lista [lista2]. Infine, per ogni i:

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

Qui, [list1] è la lista [taxPayers], una lista di oggetti di tipo [TaxPayer]. La funzione [funzione] è qui espressa come una cosiddetta funzione [lambda] che descrive la trasformazione applicata a un elemento [contribuente] della lista [contribuenti]: ogni elemento [contribuente] viene sostituito dal suo dizionario [contribuente.asdict()]. Infine, la lista risultante [lista2] è la lista dei dizionari degli elementi della lista [contribuenti];

  • riga 12: il risultato restituito dalla funzione [map] non è la lista [list2] ma un oggetto di tipo [map]. Per ottenere [list2], è necessario utilizzare l'espressione [list(mapping)] (riga 14);
  • riga 14: la lista [list2] viene salvata in formato JSON nel file [self.taxpayers_results_filename];
  • righe 15–17: qualsiasi tipo di eccezione viene intercettata e avvolta in un [ImpôtsError] prima di essere rilanciata (riga 17);
  • righe 19–21: in tutti i casi, indipendentemente dal successo o meno, il file dei risultati viene chiuso se era stato aperto;

15.1.2.3. Classe [ImpôtsDaoWithAdminDataInJsonFile]

La classe [ImpôtsDaoWithAdminDataInJsonFile] deriverà dalla classe [AbstractImpôtsDao] e implementerà il metodo [getAdminData] che la sua classe padre non ha implementato. Recupererà i dati dell'amministrazione fiscale da un file 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
}

La classe [ImpôtsDaoWithAdminDataInJsonFile] è la seguente:

#  imports
import codecs
import json

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


#  an implementation of the [dao] layer, where tax administration data is stored in a jSON file
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
    #  manufacturer
    def __init__(self, config: dict):
        #  config[admindataFilename]: name of jSON file containing tax administration data
        #  config[taxpayersFilename]: name of the taxpayer text file
        #  config[resultsFilename]: the name of the jSON results file
        #  config[errorsFilename]: the name of the error file

        #  parent class initialization
        AbstractImpôtsDao.__init__(self, config)
        #  reading tax administration data
        file = None
        try:
            #  open the jSON tax data file in read mode
            file = codecs.open(config["admindataFilename"], "r", "utf8")
            #  transfer the contents of file jSON to object [AdminData]
            self.admindata = AdminData().fromdict(json.load(file))
        except BaseException as erreur:
            #  we relaunch the error as type [ImpôtsError]
            raise ImpôtsError(21, f"{erreur}")
        finally:
            #  close the file if it has been opened
            if file:
                file.close()

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

    #  data recovery from tax authorities
    #  the method returns an object [AdminData]
    def get_admindata(self) -> AdminData:
        return self.admindata
  • Riga 11: La classe [ImpôtsDaoWithAdminDataInJsonFile] eredita dalla classe [AbstractImpôtsDao]. In quanto tale, implementa l'interfaccia [InterfaceImpôtsDao];
  • riga 13: il costruttore riceve come parametro un dizionario contenente le informazioni delle righe 14–17;
  • riga 20: la classe padre viene inizializzata;
  • riga 24: viene aperto il file JSON contenente i dati dell'amministrazione fiscale;
  • riga 25: viene aperto il file UTF-8 contenente i dati dell'autorità fiscale;
  • riga 27: il contenuto del file viene letto e inserito nell'oggetto [self.admindata] di tipo [AdminData]. Le chiavi nel file JSON devono corrispondere alle proprietà accettate per un oggetto [AdminData]; in caso contrario, il metodo [fromdict] genererà un'eccezione;
  • righe 28–30: gestione delle eccezioni. Qualsiasi eccezione che possa verificarsi viene avvolta in un tipo [ImpôtsError] prima di essere rilanciata;
  • righe 32–34: il file viene chiuso se era stato aperto;
  • righe 42–43: implementazione del metodo [get_admindata] dell'interfaccia [InterfaceImpôtsDao];

15.1.3. Il livello [business]

Image

15.1.3.1. L'interfaccia [InterfaceImpôtsMétier]

L'interfaccia per il livello [business] sarà la seguente:

#  imports
from abc import ABC, abstractmethod

from AdminData import AdminData
from TaxPayer import TaxPayer


#  interface IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
    #  tax calculation for 1 taxpayer
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        pass
  • L'interfaccia [BusinessTaxInterface] definisce un unico metodo:
  • riga 12: il metodo [calculate_tax] calcola l'imposta per un singolo contribuente [taxpayer]. [admindata] è l'oggetto [AdminData] che incapsula i dati dell'amministrazione fiscale;
  • Riga 12: il metodo [calculate_tax] non restituisce un risultato. I dati ottenuti (imposta, sovrattassa, sconto, riduzione, aliquota) sono inclusi nel parametro [taxpayer]: prima della chiamata, questi attributi sono vuoti; dopo la chiamata, sono stati inizializzati;

15.1.3.2. La classe [BusinessTaxes]

La classe [ImpôtsMétier] implementa l'interfaccia [InterfaceImpôtsMétier] come segue:

Image

I metodi della classe derivano dal modulo [impôts_module_02] nella sezione |Il modulo [impots.v02.modules.impôts_module_02]|. Abbiamo limitato i parametri del metodo a soli due:

  • contribuente(id, coniugato, figli, stipendio, imposta, sconto, sovrattassa, riduzione, aliquota): l'oggetto che rappresenta un contribuente e la sua imposta;
  • admindata: l'oggetto che incapsula i dati dell'amministrazione fiscale;

Dimostriamo le modifiche apportate utilizzando un metodo;

    #  tax calculation - phase 1
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        #  taxpayer(id, married, children, salary, tax, discount, surcharge, reduction, rate)
        #  admindata: tax administration data

        #  tax calculation with children
        self.calculate_tax_2(taxpayer, admindata)
        #  results are in taxpayer
        taux1 = taxpayer.taux
        surcôte1 = taxpayer.surcôte
        impot1 = taxpayer.impôt
        #  tax calculation without children
        if taxpayer.enfants != 0:
            #  tax calculation for the same taxpayer without children
            taxpayer2 = TaxPayer().fromdict(
                {'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
            self.calculate_tax_2(taxpayer2, admindata)
            #  the results are in taxpayer2
            taux2 = taxpayer2.taux
            surcôte2 = taxpayer2.surcôte
            impot2 = taxpayer2.impôt
            #  application of the family allowance ceiling
            if taxpayer.enfants < 3:
                #  PLAFOND_QF_DEMI_PART euros for the first 2 children
                impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
            else:
                #  PLAFOND_QF_DEMI_PART euros for the first 2 children, double for subsequent children
                impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
                         * 2 * admindata.plafond_qf_demi_part
        else:
            #  if the taxpayer has no children, then impot2=impot1
            impot2 = impot1

        #  we take the highest tax with the corresponding rate and surcharge
        (impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
            impot2, impot2 - impot1 + surcôte2, taux2)

        #  partial results
        taxpayer.impôt = impot
        taxpayer.surcôte = surcôte
        taxpayer.taux = taux
        #  calculation of any discount
        self.get_décôte(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.décôte
        #  calculation of any tax reduction
        self.get_réduction(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.réduction
        #  result
        taxpayer.impôt = math.floor(taxpayer.impôt)
  • riga 3: Il metodo [calculate_tax] è l'unico metodo nell'interfaccia [InterfaceImpôtsMétier]. Accetta due parametri:
    • [tapPayer]: il contribuente per il quale viene calcolata l'imposta;
    • [admindata]: l'oggetto che incapsula i dati dell'amministrazione fiscale;
    • i risultati del calcolo sono incapsulati nel parametro [taxpayer] (righe 40–50). Il contenuto di questo oggetto non è quindi lo stesso prima e dopo la chiamata al metodo;

15.1.4. Test per i livelli [dao] e [business]

Image

  • [TestDaoMétier] è la classe UnitTest per testare i livelli [dao] e [business];
  • [config] è il file di configurazione del test;

La configurazione [config] è la seguente:

def configure():
    import os

    #  step 1 ------
    #  path python configuration

    #  folder of this file
    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 application dependencies
    absolute_dependencies = [
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
        f"{root_dir}/02/entities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config = {
        #  absolute paths for application files
        "admindataFilename": f"{script_dir}/../data/input/admindata.json"
    }

    #  instantiation of application layers
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

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

    #  put the layer instances in the config
    config["dao"] = dao
    config["métier"] = métier

    #  return the config
    return config
  • righe 4–23: configuriamo il percorso Python per i test;
  • righe 32–41: istanziare i livelli [dao] e [business]. Memorizzare i relativi riferimenti nel dizionario [config];
  • riga 44: restituisci questo dizionario;

La classe di test [TestDaoMétier] è la seguente:

import unittest


def get_config() -> dict:
    #  application configuration
    import config
    #  we return the configuration
    return config.configure()


class TestDaoMétier(unittest.TestCase):

    #  executed before each test_ method
    def setUp(self) -> None:
        #  retrieve the test configuration
        config = get_config()
        #  memorize some information
        self.métier = config['métier']
        self.admindata = config['dao'].get_admindata()

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

        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  check
        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

        #  { 'married': 'yes', 'children': 2, 'salary': 50000,
        #  tax': 1384, 'surcôte': 0, 'décôte': 384, 'réduction': 347, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'yes', 'children': 3, 'salary': 50000,
        #  tax': 0, 'surcôte': 0, 'décôte': 720, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'no', 'children': 2, 'salary': 100000,
        #  tax': 19884, 'surcôte': 4480, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'no', 'children': 3, 'salary': 100000,
        #  tax': 16782, 'surcôte': 7176, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'yes', 'children': 3, 'salary': 100000,
        #  tax': 9200, 'surcôte': 2180, 'décôte': 0, 'réduction': 0, 'taux': 0.3}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'yes', 'children': 5, 'salary': 100000,
        #  tax': 4230, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'no', 'children': 0, 'salary': 100000,
        #  tax': 22986, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'yes', 'children': 2, 'salary': 30000,
        #  tax': 0, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'no', 'children': 0, 'salary': 200000,
        #  tax': 64210, 'surcôte': 7498, 'décôte': 0, 'réduction': 0, 'taux': 0.45}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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

        #  { 'married': 'yes', 'children': 3, 'salary': 200000,
        #  tax': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        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()

Commenti

  • riga 11: la classe di test estende la classe [unittest.TestCase];
  • righe 13–19: in un UnitTest, il metodo [setUp] viene eseguito prima di ciascuno dei metodi [test_];
  • riga 16: viene recuperata la configurazione dallo script [config] discusso in precedenza;
  • riga 18: viene memorizzato un riferimento al livello [business];
  • riga 19: richiediamo l'oggetto [AdminData] — che incapsula i dati dell'amministrazione fiscale — dal livello [DAO] e lo memorizziamo;
  • righe 21–173: 11 test i cui risultati sono stati verificati sul sito web ufficiale delle imposte 2019 |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
  • righe 21–33: tutti i test sono stati creati utilizzando lo stesso modello;
  • riga 22: importazione della classe [TaxPayer];
  • riga 24: contribuente sottoposto al test;
  • riga 25: risultati attesi;
  • riga 26: creazione dell'oggetto [TaxPayer] del contribuente;
  • riga 27: calcolo della sua imposta. Il risultato si trova in [taxpayer];
  • righe 29–33: verifica dei risultati ottenuti;
  • riga 29: verifichiamo l'importo dell'imposta arrotondato all'euro più vicino. I test hanno infatti dimostrato che i risultati ottenuti dall'algoritmo in questo documento possono differire dai dati ufficiali fino a 1 euro;

L'esecuzione dei test produce i seguenti risultati:

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. Script principale

Image

Lo script principale è configurato dal seguente script [config]:

def configure():
    import os

    #  step 1 ------
    #  path python configuration

    #  folder of this file
    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"

    #  application dependencies
    absolute_dependencies = [
        #  local dependencies
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config = {
        #  absolute paths for application files
        "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"
    }

    #  instantiation of application layers
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

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

    #  put the layer instances in the config
    config["dao"] = dao
    config["métier"] = métier

    #  return the config
    return config

È simile a quella utilizzata per testare i livelli [business] e [dao].

Lo script principale [main.py] è il seguente:

#  configure the application
import config

config = config.configure()

#  imports
from ImpôtsError import ImpôtsError

#  retrieve application layers (already instantiated)
dao = config["dao"]
métier = config["métier"]

try:
    #  tax bracket recovery
    admindata = dao.get_admindata()
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  tax calculation
    for taxpayer in taxpayers:
        #  taxpayer is both an input and output parameter
        #  taxpayer will be modified
        métier.calculate_tax(taxpayer, admindata)
    #  writing results to a text file
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  completed
    print("Travail terminé...")

Note

  • righe 2–4: recuperiamo la configurazione dell'applicazione. Sappiamo anche che il Python Path dell'applicazione è stato creato;
  • righe 9–11: recuperiamo i riferimenti ai livelli [business] e [DAO];
  • riga 15: recuperiamo i dati dall'amministrazione fiscale;
  • riga 17: recuperiamo l'elenco dei contribuenti per i quali devono essere calcolate le imposte;
  • righe 19–20: se questo elenco è vuoto, viene generata un'eccezione;
  • righe 22–25: calcoliamo l'imposta per i vari oggetti [contribuente] utilizzando il livello [business];
  • riga 27: [contribuenti] è ora un elenco di oggetti [TaxPayer] in cui agli attributi (imposta, sconto, sovrattassa, riduzione, aliquota) sono stati assegnati dei valori. Questo elenco viene scritto in un file JSON;
  • righe 28–30: intercettano eventuali errori;
  • righe 31–33: eseguite in tutti i casi;

L'esecuzione dello script produce gli stessi risultati delle versioni precedenti. Il file degli errori dei contribuenti era una nuova funzionalità di questa versione. Dopo aver eseguito lo script [main], il suo contenuto è il seguente:


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]

Le righe errate erano le seguenti:

# données valides : id, marié, enfants, salaire
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
# on peut avoir des lignes vides

# on crée des lignes erronées
# pas assez de valeurs
11,12
# trop de valeurs
12,oui,3,200000, x, y
# des valeurs erronées
x,x,x,x

15.2. Versione 4 – Applicazione 2

In questa versione, l'utente inserisce l'elenco dei contribuenti tramite la tastiera. L'architettura dell'applicazione sarà la seguente:

Image

Viene introdotto un nuovo modulo: il livello [ui] (User Interface), che interagirà con l'utente. Questo livello avrà un'interfaccia e sarà implementato da una classe.

Image

15.2.1. L'interfaccia [InterfaceImpôtsUi]

#  imports
from abc import ABC, abstractmethod


#  interface InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
    #  execution of the class implementing the interface
    @abstractmethod
    def run(self):
        pass

L'interfaccia [InterfaceImpôtsUi] avrà un solo metodo, quello alle righe 8–10. L'interfaccia verrà implementata qui con un'applicazione console, ma potrebbe anche essere implementata con un'interfaccia utente grafica. I parametri passati al metodo [run] non sarebbero gli stessi in entrambe le implementazioni. Per ovviare a questo problema, l'approccio usuale è:

  • non passare alcun parametro al metodo [run] (o passare il numero minimo di parametri);
  • passare i parametri al costruttore della classe che implementa l'interfaccia. Questi possono variare da un'implementazione all'altra. Tali parametri vengono memorizzati come attributi di classe;
  • assicurarsi che il metodo [run] utilizzi questi attributi di classe (self.x);

Questo metodo consente un'interfaccia molto generica, specificata dai parametri dei costruttori di ciascuna classe di implementazione. Questo metodo era già stato utilizzato per la versione modulare n. 1.

15.2.2. La classe [ImpôtsConsole]

La classe [ImpôtsConsole] implementa l'interfaccia [InterfaceImpôtsUi] come segue:

#  imports
import re

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


#  layer [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
    #  manufacturer
    def __init__(self, config: dict):
        #  save parameters
        self.admindata = config['dao'].get_admindata()
        self.métier = config['métier']

    def run(self):
        #  interactive dialogue with the user
        fini = False
        while not fini:
            #  is the taxpayer married?
            marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            #  check validity of input
            while marié != "oui" and marié != "non" and marié != "*":
                #  error msg
                print("Tapez oui ou non ou *")
                #  question again
                marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            #  finished?
            if marié == "*":
                #  dialogue over
                return
            #  number of children
            enfants = input("Nombre d'enfants : ").strip()
            #  check validity of input
            if not re.match(r"^\d+$", enfants):
                #  error msg
                print("Tapez un nombre entier positif ou nul")
                #  here we go again
                enfants = input("Nombre d'enfants : ").strip()
            #  annual salary
            salaire = input("Salaire annuel : ").strip()
            #  check validity of input
            if not re.match(r"^\d+$", salaire):
                #  error msg
                print("Tapez un nombre entier positif ou nul")
                #  here we go again
                salaire = input("Salaire annuel : ").strip()
            #  tAX CALCULATION
            taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
            self.métier.calculate_tax(taxpayer, self.admindata)
            #  display
            print(f"Impôt du contribuable = {taxpayer}\n\n")
            #  next taxpayer
  • Riga 9: la classe [TaxConsole] implementa l'interfaccia [TaxUiInterface];
  • riga 11: il costruttore della classe riceve un parametro, il dizionario [config] contenente la configurazione dell'applicazione;
    • riga 13: vengono recuperati i dati dall'autorità fiscale per calcolare l'imposta;
    • riga 14: viene memorizzato un riferimento al livello [business];
  • riga 16: implementazione del metodo [run] dell'interfaccia;
  • righe 19–53: interazione con l'utente. Ciò comporta
    • richiedere al contribuente tre informazioni (stato civile, figli, stipendio);
    • il calcolo delle imposte;
    • visualizzazione del risultato;
    • il dialogo termina quando l'utente risponde * alla prima domanda;
  • righe 20–27: il programma chiede se il contribuente è sposato e verifica la validità della risposta;
  • righe 29–31: se l'utente ha risposto ‘*’ alla domanda, il dialogo termina;
  • righe 32–39: al contribuente viene chiesto quanti figli ha e viene verificata la validità della risposta;
  • righe 40–47: viene richiesto lo stipendio annuale del contribuente e viene verificata la validità della risposta;
  • righe 48–50: utilizzando queste informazioni, il livello [aziendale] calcola l'imposta del contribuente;
  • riga 52: viene visualizzato l'importo dell'imposta;

15.2.3. Lo script principale

Lo script principale [main] è configurato dal seguente file [config]:

def configure():
    import os

    #  step 1 ------
    #  path python configuration

    #  folder of this file
    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"

    #  application dependencies
    absolute_dependencies = [
        #  local dependencies
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config = {
        #  absolute paths for application files
        "admindataFilename": f"{script_dir}/../../data/input/admindata.json",
    }

    #  instantiation of application layers
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier
    from ImpôtsConsole import ImpôtsConsole

    #  dao layer
    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    #  business layer
    métier = ImpôtsMétier()
    #  put the layer instances in the config
    config["dao"] = dao
    config["métier"] = métier
    #  layer ui
    ui = ImpôtsConsole(config)
    config["ui"] = ui

    #  return the config
    return config

Lo script principale è il seguente (main.py):

#  configure the application
import config

config = config.configure()

#  imports
from ImpôtsError import ImpôtsError

#  retrieve application layers (already instantiated)
ui = config["ui"]

#  code
try:
    #  execution of the [ui] layer
    ui.run()
except ImpôtsError as erreur:
    #  the error message is displayed
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  executed in all cases
    print("Travail terminé...")
  • righe 1-4: recupera la configurazione dell'applicazione;
  • riga 10: recupera un riferimento al livello [ui];
  • righe 12-21: la struttura del codice è la stessa dell'applicazione precedente: codice racchiuso in un blocco try/catch per intercettare eventuali eccezioni;
  • riga 15: chiediamo al livello [ui] di eseguire: inizia quindi l'interazione con l'utente;
  • righe 16–18: intercettazione di eventuali eccezioni;

Ecco un esempio di esecuzione:


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