Skip to content

15. Application Exercise - Version 4

Image

Here we revisit the exercise described in the |Version 3| section and now implement it using classes and interfaces. We will write two applications:

Application 1 will be as follows:

Image

A main script [main] will instantiate a [DAO] layer and a [business] layer:

  • the [DAO] layer will be responsible for managing data stored in text files and later in a database;
  • the [business] layer will be responsible for calculating the tax;

In this application, there will be no user input: taxpayer data will be found in a text file whose name will be passed to the [main] module.

In Application 2, the user will enter taxpayer data via the keyboard. The architecture will then evolve as follows:

Image

  • The [DAO] (Data Access Object) layer handles access to external data
  • the [business] layer handles business logic, in this case tax calculation. It does not handle the data. This data can come from two sources:
    • the [DAO] layer for persistent data;
    • the [UI] layer for data provided by the user.
  • the [ui] (User Interface) layer handles interactions with the user;
  • [main] acts as the orchestrator;

In the following, the [dao], [business], and [ui] layers will each be implemented using a class. The [business] and [dao] layers will be the same for both applications. This is why they have been combined into a single version of the application exercise.

15.1. Version 4 – Application 1

Version 4 calculates the tax for a list of taxpayers stored in a text file. It has the following architecture:

Image

15.1.1. Entities

Image

Entities are data classes. Their role is to encapsulate data and provide getters/setters that allow the data’s validity to be verified. Entities are exchanged between layers. A single entity can travel from the [ui] layer to the [dao] layer and vice versa.

15.1.1.1. The [ImpôtsError] class

We will use a custom exception class:

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


class ImpôtsError(MyException):
    pass

As soon as the [business] and [DAO] layers encounter a problem, they will raise this exception. It derives from the [MyException] class. It is therefore used as follows: [raise ImpôtsError(error_code, error_message)].

15.1.1.2. The [AdminData] class

The [AdminData] class encapsulates the constants used in tax calculations:

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"
        ]
  • Line 5: The [AdminData] class extends the [BaseEntity] class described in the |BaseEntity| section. Recall that classes extending the [BaseEntity] class must define:
    • a class attribute [excluded_keys] (line 7) that lists the object’s properties excluded when the object is converted to a dictionary;
    • a static method [get_allowed_keys] (lines 10–26) that returns the list of properties accepted when the object is initialized with a dictionary;

We did not use setters to validate the data used to initialize an [AdminData] object. This is because this object is unique and defined by configuration, and therefore unlikely to contain errors.

15.1.1.3. The [TaxPayer] class

The [TaxPayer] class will model a taxpayer:

#  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")

Notes:

  • The [TaxPayer] class encapsulates a taxpayer;
  • line 7: the [TaxPayer] class derives from the [BaseEntity] class. It therefore has an identifier [id];
  • line 20: no properties are excluded from the state of an [AdminData] object;
  • lines 22–25: the class properties. These are explained in lines 9–17;
  • lines 27–58: getters for the class attributes;
  • lines 60–161: the setters for the class’s attributes. Recall that the advantage of a class encapsulating data over a simple dictionary is that the class can verify the validity of its properties using its setters;

15.1.2. The [dao] layer

Image

We will group the layer implementations in a [services] folder. These classes will implement interfaces defined in the [interfaces] folder.

Image

15.1.2.1. The [InterfaceImpôtsDao] interface

The [dao] layer will implement the following [InterfaceImpôtsDao] interface (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

The interface defines three methods:

  • [get_admindata]: is the method that retrieves the tax bracket table. Note that no information is provided on how to obtain this data. Later on, it will first be found in a text file and then in a database. It will be up to the classes that implement the interface to adapt to the data storage method. We will therefore have one class to retrieve the tax brackets from a text file and another to retrieve them from a database. Both will implement the [get_admindata] method;
  • [get_taxpayers_data]: is the method that retrieves taxpayer data. Again, we do not specify where it will be found. We will only handle the case where it is in a text file;
  • [write_taxpayers_results]: is the method that will persist the tax calculation results. We do not specify where. We will only handle the case where the results are persisted in a text file. The [taxpayers_results] parameter will be the list of results to be persisted;

15.1.2.2. The [AbstractImpôtsDao] class

The [dao] layer will be implemented by two classes:

  • one will retrieve the data (taxpayers, results, tax brackets) from text files;
  • the other will retrieve data (taxpayers, results) from text files and tax brackets from a database;

The two classes will differ only in how they handle tax brackets. Taxpayer data and tax calculation results will be managed in the same way. For this reason, we will manage them in a parent class [AbstractImpôtsDao]. The specific handling of tax brackets will be managed in two child classes:

  • the [ImpôtsDaoWithAdminDataInJsonFile] class will retrieve the tax brackets from a text file in JSON format;
  • the [ImpôtsDaoWithAdminDataInDatabase] class will retrieve the tax brackets from a database;

The parent class [AbstractImpôtsDao] will be as follows:

#  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
  • Line 13: The class [AbstractImpôtsDao] implements the interface [InterfaceImpôtsDao]. Therefore, it contains the three methods of this interface:
    • [get_taxpayers_data]: line 31;
    • [write_taxpayers_results]: line 35;
    • [get_admindata]: line 40. This method will not be implemented by the [AbstractImpôtsDao] class, so it is declared abstract (line 39);
  • line 16: the constructor receives a [config] dictionary containing the following information:
    • [taxpayersFilename]: the name of the text file containing taxpayer data;
    • [resultsFilename]: the name of the text file in which the results will be stored;
    • [errorsFilename]: the name of the text file listing the errors encountered while processing the [taxpayersFilename] file;

The [get_taxpayers_data] method is as follows:

    #  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()
  • line 4: taxpayer data (married, children, salary) will be placed in a list of objects of type [TaxPayer];
  • lines 8-9: we open the taxpayer text file for reading. Its content has the following format:
# 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

Compared to previous versions:

  • Each line in the [taxpayersFilename] file begins with the taxpayer ID, a single number;
  • comments and empty lines are allowed;
  • we will handle errors. Thus, lines 17, 19, and 21 must be marked as invalid. Errors are logged in a separate file;

Let’s continue reviewing the code:

  • line 4: the data from the text file is transferred to the [taxPayersData] list;
  • lines 14–31: The taxpayer file is read line by line;
  • line 14: the end of the file is reached when an empty line is read (nothing—not even the end-of-line character \r\n);
  • line 20: empty lines and comments are ignored. A line is a comment if, after removing whitespace before and after the text, the first character is the # character;
  • line 24: a valid line consists of four fields separated by commas. These are retrieved. Assigning data to a four-element tuple fails if there are not exactly four data points assigned;
  • line 25: if any of the four retrieved fields [id, married, children, salary] is invalid, then the [BaseEntity.fromdict] method will raise a [MyException] exception;
  • lines 25–26: a [TaxPayer] object is added to the [taxpayers_data] list of taxpayers;
  • lines 27–29: any errors are collected in a list [errors]. This list was created on line 6;
  • lines 33–36: the list of encountered errors is saved to the text file [errorsFilename]. There are two types of errors:
    • a row did not have the correct number of expected fields;
    • the information in the row was incorrect and failed to construct a [TaxPayer] object;
  • lines 39–41: any error (BaseException) is caught and propagated by wrapping it in a [TaxPayerError] type;
  • lines 42–45: in all cases, whether successful or not, the taxpayer text file is closed if it was opened;

The [write_taxpayers_results] method must produce a JSON file in the following format:


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

]

The [write_taxpayers_results] method is as follows:

    #  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()
  • line 2: the method receives a list of taxpayers [taxpayers] that it must save to the text file [self.taxpayers_results_filename] in JSON format;
  • line 10: creation of the UTF-8 results file;
  • line 12: here we introduce the [map] function, whose syntax here is [map (function, list1)]. The [function] is applied to each element of [list1] and produces a new element that populates a list [list2]. Finally, for each i:

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

Here, [list1] is the list [taxPayers], a list of objects of type [TaxPayer]. The function [function] is expressed here as a so-called [lambda] function that describes the transformation applied to an element [taxpayer] of the list [taxpayers]: each [taxpayer] element is replaced by its dictionary [taxpayer.asdict()]. Finally, the resulting list [list2] is the list of dictionaries of the elements in the [taxpayers] list;

  • line 12: the result returned by the [map] function is not the list [list2] but an object of type [map]. To obtain [list2], you must use the expression [list(mapping)] (line 14);
  • line 14: the list [list2] is saved in JSON format to the file [self.taxpayers_results_filename];
  • lines 15–17: any type of exception is caught and wrapped in an [ImpôtsError] before being rethrown (line 17);
  • lines 19–21: in all cases, whether successful or not, the results file is closed if it was opened;

15.1.2.3. Class [ImpôtsDaoWithAdminDataInJsonFile]

The [ImpôtsDaoWithAdminDataInJsonFile] class will derive from the [AbstractImpôtsDao] class and implement the [getAdminData] method that its parent class has not implemented. It will retrieve tax administration data from a JSON file:


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

The [ImpôtsDaoWithAdminDataInJsonFile] class is as follows:

#  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
  • Line 11: The [ImpôtsDaoWithAdminDataInJsonFile] class inherits from the [AbstractImpôtsDao] class. As such, it implements the [InterfaceImpôtsDao] interface;
  • line 13: the constructor receives as a parameter a dictionary containing the information from lines 14–17;
  • line 20: the parent class is initialized;
  • line 24: the JSON file containing the tax administration data is opened;
  • line 25: the UTF-8 file containing the tax authority data is opened;
  • line 27: the file’s contents are read and placed in the [self.admindata] object of type [AdminData]. The keys in the JSON file must match the properties accepted for an [AdminData] object; otherwise, the [fromdict] method will throw an exception;
  • lines 28–30: exception handling. Any exceptions that may occur are wrapped in an [ImpôtsError] type before being rethrown;
  • lines 32–34: the file is closed if it was opened;
  • lines 42–43: implementation of the [get_admindata] method of the [InterfaceImpôtsDao] interface;

15.1.3. The [business] layer

Image

15.1.3.1. The [InterfaceImpôtsMétier] interface

The interface for the [business] layer will be as follows:

#  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
  • The [BusinessTaxInterface] interface defines a single method:
  • line 12: the [calculate_tax] method calculates the tax for a single taxpayer [taxpayer]. [admindata] is the [AdminData] object encapsulating the tax administration data;
  • Line 12: The [calculate_tax] method does not return a result. The data obtained (tax, surcharge, discount, reduction, rate) are included in the [taxpayer] parameter: before the call, these attributes are empty; after the call, they have been initialized;

15.1.3.2. The [BusinessTaxes] class

The [ImpôtsMétier] class implements the [InterfaceImpôtsMétier] interface as follows:

Image

The class methods are derived from the [impôts_module_02] module in the section |The [impots.v02.modules.impôts_module_02] module|. We have limited the method parameters to just two:

  • taxpayer(id, married, children, salary, tax, discount, surcharge, reduction, rate): the object representing a taxpayer and their tax;
  • admindata: the object encapsulating the tax administration data;

We demonstrate the changes made using a method;

    #  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)
  • line 3: The [calculate_tax] method is the only method in the [InterfaceImpôtsMétier] interface. It takes two parameters:
    • [tapPayer]: the taxpayer for whom the tax is being calculated;
    • [admindata]: the object encapsulating the tax administration data;
    • the results of the calculation are encapsulated in the [taxpayer] parameter (lines 40–50). The content of this object is therefore not the same before and after the method is called;

15.1.4. Tests for the [dao] and [business] layers

Image

  • [TestDaoMétier] is the UnitTest class for testing the [dao] and [business] layers;
  • [config] is the test configuration file;

The [config] configuration is as follows:

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
  • lines 4–23: We configure the Python path for the tests;
  • lines 32–41: instantiate the [dao] and [business] layers. Store their references in the [config] dictionary;
  • line 44: return this dictionary;

The [TestDaoMétier] test class is as follows:

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()

Comments

  • line 11: the test class extends the [unittest.TestCase] class;
  • lines 13–19: In a UnitTest, the [setUp] method is executed before each of the [test_] methods;
  • line 16: the configuration from the [config] script discussed earlier is retrieved;
  • line 18: a reference to the [business] layer is stored;
  • line 19: we request the [AdminData] object—which encapsulates tax administration data—from the [DAO] layer and store it;
  • lines 21–173: 11 tests whose results were verified on the official 2019 tax website |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
  • lines 21–33: all tests were built using the same template;
  • line 22: import the [TaxPayer] class;
  • line 24: taxpayer being tested;
  • line 25: expected results;
  • line 26: creation of the taxpayer’s [TaxPayer] object;
  • line 27: calculation of their tax. The result is in [taxpayer];
  • lines 29–33: verification of the results obtained;
  • line 29: we verify the tax amount to the nearest euro. Tests have indeed shown that the results obtained by the algorithm in this document may differ from official figures by up to 1 euro;

Running the tests yields the following results:

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. Main script

Image

The main script is configured by the following [config] script:

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

It is similar to the one used for testing the [business] and [dao] layers.

The main script [main.py] is as follows:

#  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é...")

Notes

  • lines 2–4: we retrieve the application configuration. We also know that the application’s Python Path has been built;
  • lines 9–11: we retrieve references to the [business] and [DAO] layers;
  • line 15: we retrieve data from the tax administration;
  • line 17: we retrieve the list of taxpayers for whom taxes must be calculated;
  • lines 19–20: if this list is empty, an exception is raised;
  • lines 22–25: calculate the tax for the various [taxpayer] objects using the [business] layer;
  • line 27: [taxpayers] is now a list of [TaxPayer] objects where the attributes (tax, discount, surcharge, reduction, rate) have been assigned values. This list is written to a JSON file;
  • lines 28–30: catch any potential errors;
  • lines 31–33: executed in all cases;

Running the script yields the same results as in previous versions. The taxpayer error file was a new feature in this version. After running the [main] script, its contents are as follows:


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]

The erroneous lines were as follows:


# 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. Version 4 – Application 2

In this version, the user enters the list of taxpayers via the keyboard. The application architecture will be as follows:

Image

A new module appears: the [ui] (User Interface) layer, which will interact with the user. This layer will have an interface and will be implemented by a class.

Image

15.2.1. The [InterfaceImpôtsUi] interface

#  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

The [InterfaceImpôtsUi] interface will have only one method, the one in lines 8–10. The interface will be implemented here with a console application, but it could also be implemented with a graphical user interface. The parameters passed to the [run] method would not be the same in both implementations. To work around this problem, the usual approach is to:

  • do not pass any parameters to the [run] method (or pass the minimum number of parameters);
  • pass parameters to the constructor of the class implementing the interface. These may differ from one implementation to another. These parameters are stored as class attributes;
  • ensure that the [run] method uses these class attributes (self.x);

This method allows for a very general interface that is specified by the parameters of the constructors of each implementation class. This method was already used for modular version #1.

15.2.2. The [ImpôtsConsole] class

The [ImpôtsConsole] class implements the [InterfaceImpôtsUi] interface as follows:

#  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
  • Line 9: The [TaxConsole] class implements the [TaxUiInterface] interface;
  • line 11: the class constructor receives a parameter, the [config] dictionary containing the application configuration;
    • line 13: data from the tax authority is retrieved to calculate the tax;
    • line 14: a reference to the [business] layer is stored;
  • line 16: implementation of the [run] method of the interface;
  • lines 19–53: user interaction. This involves
    • asking the taxpayer for three pieces of information (marital status, children, salary);
    • calculating their tax;
    • displaying the result;
    • the dialogue ends when the user answers * to the first question;
  • lines 20–27: the program asks if the taxpayer is married and checks the validity of the response;
  • lines 29–31: if the user answered ‘*’ to the question, the dialogue ends;
  • lines 32–39: the taxpayer is asked how many children they have, and the validity of the response is checked;
  • lines 40–47: the taxpayer’s annual salary is requested, and the validity of the response is verified;
  • lines 48–50: using this information, the [business] layer calculates the taxpayer’s tax;
  • line 52: the tax amount is displayed;

15.2.3. The main script

The main script [main] is configured by the following [config] file:

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

The main script is as follows (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é...")
  • lines 1-4: retrieve the application configuration;
  • line 10: retrieves a reference to the [ui] layer;
  • lines 12-21: the code structure is the same as in the previous application: code wrapped in a try/catch block to catch any potential exceptions;
  • line 15: we ask the [ui] layer to execute: the user interaction then begins;
  • lines 16–18: catch any potential exceptions;

Here is an example of execution:


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