Skip to content

15. Практичне завдання — версія 4

Image

Тут ми повертаємося до вправи, описаної в розділі |Версія 3|, і тепер розглядаємо її з використанням класів та інтерфейсів. Ми напишемо дві програми:

Програма 1 буде такою:

Image

Головний скрипт [main] створить екземпляри шару [dao] та шару [métier]:

  • шар [dao] буде відповідати за обробку даних, що зберігаються у текстових файлах, а згодом — у базі даних;
  • рівень [métier] буде виконувати обчислення податку;

У цьому додатку не передбачено жодних дій з боку користувача: дані платників податків будуть взяті з текстового файлу, ім’я якого буде передано модулю [main].

У додатку 2 користувач самостійно вводитиме дані платників податків за допомогою клавіатури. Тоді архітектура зміниться наступним чином:

Image

  • рівень [dao] (Data Access Object) відповідає за доступ до зовнішніх даних
  • рівень [métier] відповідає за бізнес-задачі, в даному випадку — за розрахунок податку. Він не оперує даними. Дані можуть надходити з двох джерел:
    • рівень [dao] — для постійних даних;
    • шар [ui] — для даних, що надаються користувачем.
  • шар [ui] (User Interface) відповідає за взаємодію з користувачем;
  • [main] є «диригентом»;

Далі шари [dao], [métier] та [ui] будуть реалізовані за допомогою окремих класів. Шари [métier] та [dao] будуть однаковими для обох додатків. Саме тому їх об’єднано в одній версії практичного завдання.

15.1. Версія 4 — додаток 1

Версія 4 обчислює податок для списку платників податків, розміщеного у текстовому файлі. Вона має таку архітектуру:

Image

15.1.1. Об’єкти

Image

Ентітети — це класи даних. Їхня роль полягає в інкапсуляції даних та наданні методів getter/setter, які дозволяють перевіряти їхню валідність. Ентітети передаються між рівнями. Один і той самий ентітет може переміщатися з рівня [ui] на рівень [dao] і навпаки.

15.1.1.1. Клас [ImpôtsError]

Ми будемо використовувати власний клас винятків:


# -------------------------------
# клас винятків
from MyException import MyException


class ImpôtsError(MyException):
    pass

Як тільки шари [métier] та [dao] зіткнуться з проблемою, вони викличуть це виключення. Воно походить від класу [MyException]. Отже, його використовують таким чином: [raise ImpôtsError(code_erreur, msg_erreur)].

15.1.1.2. Клас [AdminData]

Клас [AdminData] інкапсулює константи, що використовуються при розрахунку податку:


from BaseEntity import BaseEntity


# дані податкової адміністрації
class AdminData(BaseEntity):
    # ключі, виключені зі стану класу
    excluded_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"
        ]
  • рядок 5: клас [AdminData] успадковує клас [BaseEntity], описаний у параграфі |BaseEntity|. Нагадаємо, що класи, які успадковують клас [BaseEntity], повинні визначати:
    • атрибут класу [excluded_keys] (рядок 7), який перелічує властивості об’єкта, що виключаються під час перетворення об’єкта на словник;
    • статичний метод [get_allowed_keys] (рядки 10–26), який повертає список властивостей, що приймаються під час ініціалізації об’єкта за допомогою словника;

Ми не використовували сетери для перевірки правильності даних, що використовуються для ініціалізації об’єкта [AdminData]. Адже цей об’єкт є унікальним і визначається конфігурацією, тому він не може містити помилок.

15.1.1.3. Клас [TaxPayer]

Клас [TaxPayer] буде моделювати платника податків:


# імпортні дані
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError


# платник податків
class TaxPayer(BaseEntity):
    # моделює платника податків
    # id: ідентифікатор
    # одружений: так / ні
    # діти: кількість дітей
    # зарплата: його річна зарплата
    # податок: сума податку до сплати
    # надбавка: надбавка до суми податку, що підлягає сплаті
    # знижка: знижка з податку, що підлягає сплаті
    # знижка: знижка на податок, що підлягає сплаті
    # ставка: ставка оподаткування платника податків

    # ключі, виключені зі звіту про клас
    excluded_keys = []

    # дозволені ключі
    @staticmethod
    def get_allowed_keys() -> list:
        return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']

    # властивості
    @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

    # сетери
    @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 має бути цілим числом >=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):
        # зарплата має бути цілим числом >=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):
        # податок повинен бути цілим числом >=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):
        # знижка має бути цілим числом >=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):
        # надбавка повинна бути цілим числом >=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):
        # надбавка повинна бути цілим числом >=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):
        # ставка повинна бути дійсним числом >=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")

Примітки:

  • клас [TaxPayer] інкапсулює платника податків;
  • рядок 7: клас [TaxPayer] походить від класу [BaseEntity]. Отже, він має ідентифікатор [id];
  • рядок 20: жодна властивість не виключена зі стану об’єкта [AdminData];
  • рядки 22–25: властивості класу. Вони описані в рядках 9–17;
  • рядки 27–58: методи-геттери атрибутів класу;
  • рядки 60–161: методи-сеттери атрибутів класу. Нагадаємо, що перевага класу, який інкапсулює дані, перед простим словником полягає в тому, що клас може перевіряти правильність своїх властивостей за допомогою своїх методів-сеттерів;

15.1.2. Рівень [dao]

Image

Ми об’єднаємо реалізації шарів у папці [services]. Ці класи будуть реалізовувати інтерфейси, визначені в папці [interfaces].

Image

15.1.2.1. Інтерфейс [InterfaceImpôtsDao]

Шар [dao] реалізує наступний інтерфейс [InterfaceImpôtsDao] (файл InterfaceImpôtsDao.py):


# імпорт
from abc import ABC, abstractmethod


# інтерфейс IImpôtsDao
from AdminData import AdminData


class InterfaceImpôtsDao(ABC):
    # перелік податкових шкал
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

    # перелік даних про платників податків
    @abstractmethod
    def get_taxpayers_data(self) -> dict:
        pass

    # запис результатів розрахунку податку
    @abstractmethod
    def write_taxpayers_results(self, taxpayers_results: list):
        pass

Інтерфейс визначає три методи:

  • [get_admindata]: метод, що отримує масив податкових шкал. Слід зауважити, що не надається жодної інформації щодо способу отримання цих даних. Далі ці дані спочатку будуть знаходитися у текстовому файлі, а потім — у базі даних. Класи, що реалізують цей інтерфейс, повинні будуть адаптуватися до способу зберігання даних. Отже, ми матимемо один клас для отримання податкових шкал із текстового файлу та інший — для отримання їх із бази даних. Обидва вони реалізовуватимуть метод [get_admindata];
  • [get_taxpayers_data]: це метод, який отримує дані про платників податків. І тут ми також не вказуємо, де саме вони будуть знаходитися. Ми розглянемо лише той випадок, коли вони містяться у текстовому файлі;
  • [write_taxpayers_results]: метод, який зберігатиме результати розрахунку податку. Ми не вказуємо, де саме. Ми розглянемо лише випадок, коли результати зберігаються у текстовому файлі. Параметр [taxpayers_results] буде списком результатів, які потрібно зберегти;

15.1.2.2. Клас [AbstractImpôtsDao]

Рівень [dao] буде реалізовано двома класами:

  • один буде отримувати дані (платники податків, результати, податкові ставки) з текстових файлів;
  • інший — отримуватиме дані (платники податків, результати) з текстових файлів, а податкові шкали — з бази даних;

Ці два класи відрізнятимуться лише способом обробки податкових шкал. Дані про платників податків та результати розрахунків податку оброблятимуться однаково. З цієї причини ми будемо обробляти їх у батьківському класі [AbstractImpôtsDao]. Особливості обробки податкових шкал будуть реалізовані у двох дочірніх класах:

  • клас [ImpôtsDaoWithAdminDataInJsonFile] буде отримувати податкові шкали з текстового файлу у форматі jSON;
  • клас [ImpôtsDaoWithAdminDataInDatabase] буде отримувати податкові шкали з бази даних;

Батьківський клас [AbstractImpôtsDao] матиме такий вигляд:


# імпорт
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


# базовий клас для шару [dao]
class AbstractImpôtsDao(InterfaceImpôtsDao):
    # платники податків та їхні податки будуть у текстових файлах
    # конструктор
    def __init__(self, config: dict):
        # config[taxpayersFilename]: ім’я текстового файлу платників податків
        # config[resultsFilename] : ім’я файлу jSON з результатами
        # config[errorsFilename]: ім’я файлу помилок

        # зберігаються параметри
        self.taxpayers_filename = config.get("taxpayersFilename")
        self.taxpayers_results_filename = config.get("resultsFilename")
        self.errors_filename = config.get("errorsFilename")

    # ------------------
    # інтерфейс IImpôtsDao
    # ------------------

    # список даних про платників податків
    def get_taxpayers_data(self) -> dict:
        

    # запис податків платників податків
    def write_taxpayers_results(self, taxpayers: list):
        

    # зчитування податкових шкал
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass
  • рядок 13: клас [AbstractImpôtsDao] реалізує інтерфейс [InterfaceImpôtsDao]. Тому тут містяться три методи цього інтерфейсу:

    • [get_taxpayers_data]: рядок 31;
    • [write_taxpayers_results]: рядок 35;
    • [get_admindata]: рядок 40. Цей метод не буде реалізований класом [AbstractImpôtsDao], тому він оголошений абстрактним (рядок 39);
  • рядок 16: конструктор отримує словник [config], що містить таку інформацію:

    • [taxpayersFilename]: ім’я текстового файлу, що містить дані платників податків;
    • [resultsFilename]: ім’я текстового файлу, до якого будуть записані результати;
    • [errorsFilename]: ім’я текстового файлу, що містить перелік помилок, виявлених під час обробки файлу [taxpayersFilename];

Метод [get_taxpayers_data] є таким:


    # список даних про платників податків
    def get_taxpayers_data(self) -> dict:
        # ініціалізація
        taxpayers_data = []
        datafile = None
        erreurs = []
        try:
            # відкриття файлу даних
            datafile = open(self.taxpayers_filename, "r")
            # обробка поточного рядка файлу
            ligne = datafile.readline()
            # номер рядка
            numligne = 0
            while ligne != '':
                # рядок «+»
                numligne += 1
                # видалення пробілів
                ligne = ligne.strip()
                # пропускаються порожні рядки та коментарі
                if ligne != "" and ligne[0] != "#":
                    try:
                        # витягуються 4 поля id,marié,enfants,salaire, що утворюють рядок платника податків
                        (id, marié, enfants, salaire) = ligne.split(",")
                        # створюється новий TaxPayer
                        taxpayers_data.append(
                            TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
                    except BaseException as erreur:
                        # фіксується помилка
                        erreurs.append(f"Ligne {numligne}, {erreur}")
                # зчитується новий запис платника податків
                ligne = datafile.readline()
            # записуємо помилки, якщо вони є
            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)
            # повертається результат
            return {"taxpayers": taxpayers_data, "erreurs": erreurs}
        except BaseException as erreur:
            # викликається виняток ImpôtsError
            raise ImpôtsError(11, f"{erreur}")
        finally:
            # закривається файл
            if datafile:
                datafile.close()
  • рядок 4: дані про платників податків (сімейний стан, діти, заробітна плата) будуть розміщені у списку об’єктів типу [TaxPayer];
  • рядки 8–9: відкриваємо текстовий файл платників податків у режимі читання. Його вміст має такий вигляд:
# дані правильні: ідентифікатор, одружений, діти, зарплата
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
# можуть бути порожні рядки

# створюємо рядки з помилками
# недостатньо значень
11,12
# забагато значень
12,oui,3,200000, x, y
# неправильні значення
x,x,x,x

Порівняно з попередніми версіями:

  • кожен рядок файлу [taxpayersFilename] починається з ідентифікатора платника податків — простого номера;
  • дозволено вживати коментарі та порожні рядки;
  • ми будемо обробляти помилки. Таким чином, рядки 17, 19 та 21 мають бути визнані помилковими. Помилки записуються в окремий файл;

Продовжимо розгляд коду:

  • рядок 4: дані з текстового файлу переносяться до списку [taxPayersData];
  • рядки 14–31: файл платників податків зчитується рядок за рядком;
  • рядок 14: кінець файлу досягається, коли зчитується порожній рядок (нічого — навіть позначки кінця рядка \r\n немає);
  • рядок 20: порожні рядки та коментарі ігноруються. Рядок вважається коментарем, якщо після видалення пробілів перед і після тексту першим символом є символ #;
  • рядок 24: правильний рядок складається з чотирьох полів, розділених комою. Ці поля витягуються. Присвоєння даних кортежу з чотирьох елементів завершується з помилкою, якщо кількість присвоєних даних не дорівнює точно чотирьом;
  • рядок 25: якщо одне з чотирьох отриманих полів [id, marié, enfants, salaire] є недійсним, то метод [BaseEntity.fromdict] викличе виняток типу [MyException];
  • рядки 25–26: об’єкт [TaxPayer] додається до списку платників податків [taxpayers_data];
  • рядки 27–29: можливі помилки накопичуються у списку [erreurs]. Цей список було створено у рядку 6;
  • рядки 33–36: список виявлених помилок записується у текстовий файл [errorsFilename]. Помилки бувають двох типів:
    • у рядку не було правильної кількості очікуваних полів;
    • інформація в рядку була помилковою, і не вдалося створити об’єкт [TaxPayer];
  • рядки 39–41: перехоплюється будь-яка помилка (BaseException) і передається вище, інкапсулюючи її в тип [ImpôtsError];
  • рядки 42–45: у будь-якому випадку — успіху чи невдачі — текстовий файл платників податків закривається, якщо він був відкритий;

Метод [write_taxpayers_results] повинен створити файл jSON такого вигляду:


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

]

Метод [write_taxpayers_results] має такий вигляд:


    # запис податків платників податків
    def write_taxpayers_results(self, taxpayers: list):
        # запис результатів у файл jSON
        # платники податків: список об’єктів типу TaxPayer
        # (ідентифікатор, сімейний стан, діти, заробітна плата, податок, надбавка, знижка, зменшення, ставка)
        # список [taxpayers] записано у текстовий файл [self.taxpayers_results_filename]
        file = None
        try:
            # відкриття файлу результатів
            file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
            # створення списку для серіалізації у jSON
            mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
            # серіалізація jSON
            json.dump(list(mapping), file, ensure_ascii=False)
        except BaseException as erreur:
            # повторне генерування помилки під іншим типом
            raise ImpôtsError(12, f"{erreur}")
        finally:
            # закриття файлу, якщо він був відкритий
            if file:
                file.close()
  • рядок 2: метод отримує список платників податків [taxpayers], який він повинен записати у текстовий файл [self.taxpayers_results_filename] у форматі jSON;
  • рядок 10: створення файлу результатів UTF-8;
  • рядок 12: тут ми вводимо функцію [map], синтаксис якої в даному випадку має вигляд [map (fonction, liste1)]. Функція [fonction] застосовується до кожного елемента [liste1] і створює новий елемент, який поповнює список [liste2]. Нарешті, для кожного i:

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

Тут [liste1] — це список [taxPayers], список об’єктів типу [TaxPayer]. Функція [fonction] тут представлена у вигляді так званої функції [lambda], яка описує перетворення, що виконується над елементом [taxpayer] списку [taxpayers]: кожен елемент [taxpayer] замінюється на свій словник [taxpayer.asdict()]. Врешті-решт, отриманий список [liste2] є списком словників елементів списку [taxpayers];

  • рядок 12: результатом роботи функції [map] є не список [liste2], а об’єкт типу [map]. Щоб отримати [liste2], потрібно використати вираз [list(mapping)] (рядок 14);
  • рядок 14: список [liste2] записується у форматі jSON у файл [self.taxpayers_results_filename];
  • рядки 15–17: будь-який тип винятку перехоплюється та інкапсулюється в помилку типу [ImpôtsError] перед повторним запуском (рядок 17);
  • рядки 19–21: у будь-якому випадку — чи то успішному, чи то невдалому — файл результатів закривається, якщо він був відкритий;

15.1.2.3. Клас [ImpôtsDaoWithAdminDataInJsonFile]

Клас [ImpôtsDaoWithAdminDataInJsonFile] буде похідним від класу [AbstractImpôtsDao] і реалізує метод [getAdminData], який його батьківський клас не реалізував. Він буде отримувати дані податкової адміністрації з файлу 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
}

Клас [ImpôtsDaoWithAdminDataInJsonFile] має такий вигляд:


# імпорт
import codecs
import json

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


# реалізація шару [dao], де дані податкової адміністрації містяться у файлі jSON
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
    # конструктор
    def __init__(self, config: dict):
        # config[admindataFilename]: ім’я файлу jSON, що містить дані податкової адміністрації
        # config[taxpayersFilename] : ім’я текстового файлу з даними про платників податків
        # config[resultsFilename]: ім’я файлу jSON з результатами
        # config[errorsFilename]: ім’я файлу з помилками

        # ініціалізація класу Parent
        AbstractImpôtsDao.__init__(self, config)
        # читання даних податкової служби
        file = None
        try:
            # відкриття файлу jSON з податковими даними у режимі читання
            file = codecs.open(config["admindataFilename"], "r", "utf8")
            # перенесення вмісту файлу jSON в об’єкт [AdminData]
            self.admindata = AdminData().fromdict(json.load(file))
        except BaseException as erreur:
            # повторне формування помилки у вигляді типу [ImpôtsError]
            raise ImpôtsError(21, f"{erreur}")
        finally:
            # закриття файлу, якщо він був відкритий
            if file:
                file.close()

    # -------------
    # інтерфейс
    # -------------

    # отримання даних від податкової служби
    # метод повертає об’єкт [AdminData]
    def get_admindata(self) -> AdminData:
        return self.admindata
  • рядок 11: клас [ImpôtsDaoWithAdminDataInJsonFile] успадковує клас [AbstractImpôtsDao]. Таким чином, він реалізує інтерфейс [InterfaceImpôtsDao];
  • рядок 13: конструктор отримує як параметр словник, що містить інформацію з рядків 14–17;
  • рядок 20: ініціалізується батьківський клас;
  • рядок 24: відкриття файлу jSON з даними податкової адміністрації;
  • рядок 25: відкрито файл UTF-8 з даними податкової адміністрації;
  • рядок 27: вміст файлу зчитується та розміщується в об’єкті [self.admindata] типу [AdminData]. Ключі файлу jSON повинні відповідати властивостям, що допускаються для об’єкта [AdminData], інакше метод [fromdict] викличе виняток;
  • рядки 28–30: обробка винятків. Винятки, що можуть виникнути, інкапсулюються в тип [ImpôtsError] перед тим, як їх перекинути;
  • рядки 32–34: файл закривається, якщо він був відкритий;
  • рядки 42–43: реалізація методу [get_admindata] інтерфейсу [InterfaceImpôtsDao];

15.1.3. Рівень [métier]

Image

15.1.3.1. Інтерфейс [InterfaceImpôtsMétier]

Інтерфейс шару [métier] матиме такий вигляд:


# імпорт
from abc import ABC, abstractmethod

from AdminData import AdminData
from TaxPayer import TaxPayer


# інтерфейс IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
    # розрахунок податку для 1 платника податків
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        pass
  • інтерфейс [InterfaceImpôtsMétier] визначає єдиний метод:
  • рядок 12: метод [calculate_tax] дозволяє розрахувати податок для одного платника податків [taxpayer]. [admindata] — це об’єкт [AdminData], що інкапсулює дані податкової адміністрації;
  • рядок 12: метод [calculate_tax] не повертає результату. Отримані дані (податок, надбавка, знижка, пільга, ставка) містяться в параметрі [taxpayer]: до виклику ці атрибути порожні, після виклику вони ініціалізовані;

15.1.3.2. Клас [ImpôtsMétier]

Клас [ImpôtsMétier] реалізує інтерфейс [InterfaceImpôtsMétier] наступним чином:

Image

Методи класу походять із модуля [impôts_module_02], описаного в розділі |Модуль [impots.v02.modules.impôts_module_02]|. Кількість параметрів методів було обмежено до двох:

  • taxpayer(id, одружений, діти, зарплата, податок, знижка, надбавка, пільга, ставка): об’єкт, що представляє платника податків та його податок;
  • admindata: об’єкт, що інкапсулює дані податкової адміністрації;

На прикладі одного методу продемонструємо внесені зміни;


    # розрахунок податку — етап 1
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        # платник податків (ідентифікаційний номер, сімейний стан, діти, заробітна плата, податок, знижка, надбавка, пільга, ставка)
        # admindata: дані податкової адміністрації

        # розрахунок податку з урахуванням дітей
        self.calculate_tax_2(taxpayer, admindata)
        # результати містяться в taxpayer
        taux1 = taxpayer.taux
        surcôte1 = taxpayer.surcôte
        impot1 = taxpayer.impôt
        # розрахунок податку без урахування дітей
        if taxpayer.enfants != 0:
            # розрахунок податку для того самого платника податків без дітей
            taxpayer2 = TaxPayer().fromdict(
                {'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
            self.calculate_tax_2(taxpayer2, admindata)
            # результати знаходяться у taxpayer2
            taux2 = taxpayer2.taux
            surcôte2 = taxpayer2.surcôte
            impot2 = taxpayer2.impôt
            # застосування обмеження сімейного коефіцієнта
            if taxpayer.enfants < 3:
                # PLAFOND_QF_DEMI_PART євро за перших двох дітей
                impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
            else:
                # PLAFOND_QF_DEMI_PART євро за перших двох дітей, удвічі більше — за наступних
                impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
                         * 2 * admindata.plafond_qf_demi_part
        else:
            # якщо платник податків не має дітей, то податок2 = податок1
            impot2 = impot1

        # береться найбільший податок із відповідною ставкою та надбавкою
        (impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
            impot2, impot2 - impot1 + surcôte2, taux2)

        # проміжні результати
        taxpayer.impôt = impot
        taxpayer.surcôte = surcôte
        taxpayer.taux = taux
        # розрахунок можливої знижки
        self.get_décôte(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.décôte
        # розрахунок можливого зменшення податку
        self.get_réduction(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.réduction
        # результат
        taxpayer.impôt = math.floor(taxpayer.impôt)
  • рядок 3: метод [calculate_tax] є єдиним методом інтерфейсу [InterfaceImpôtsMétier]. Він приймає два параметри:
    • [tapPayer]: платник податків, для якого розраховується податок;
    • [admindata] — об’єкт, що інкапсулює дані податкової адміністрації;
    • результати розрахунку інкапсульовані в параметрі [taxpayer] (рядки 40–50). Отже, вміст цього об’єкта до та після виклику методу відрізняється;

15.1.4. Тестування шарів [dao] та [métier]

Image

  • [TestDaoMétier] — це тестовий клас UnitTest для шарів [dao] та [métier];
  • [config] — це файл конфігурації тестів;

Конфігурація [config] має такий вигляд:


def configure():
    import os

    # етап 1 ------
    # налаштування шляху до Python

    # папка, в якій знаходиться цей файл
    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_dependencies = [
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
        f"{root_dir}/02/entities",
    ]

    # встановлюємо syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # крок 2 ------
    # конфігурація додатка
    config = {
        # абсолютні шляхи до файлів додатка
        "admindataFilename"f"{script_dir}/../data/input/admindata.json"
    }

    # інстанціювання шарів додатка
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

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

    # додаємо екземпляри шарів до конфігурації
    config["dao"] = dao
    config["métier"] = métier

    # виконується конфігурація
    return config
  • рядки 4–23: налаштовується Python Path для тестів;
  • рядки 32–41: створюються екземпляри шарів [dao] та [métier]. Їхні посилання додаються до словника [config];
  • рядок 44: повертається цей словник;

Клас тесту [TestDaoMétier] має такий вигляд:


import unittest


def get_config() -> dict:
    # конфігурація додатка
    import config
    # виконується конфігурація
    return config.configure()


class TestDaoMétier(unittest.TestCase):

    # виконується перед кожним методом test_
    def setUp(self) -> None:
        # отримуємо конфігурацію тестів
        config = get_config()
        # зберігаємо деяку інформацію
        self.métier = config['métier']
        self.admindata = config['dao'].get_admindata()

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

        # {'одружений': 'так', 'діти': 2, 'зарплата': 55555,
        # 'податок': 2814, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірка
        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

        # {'одружений': 'так', 'діти': 2, 'зарплата': 50000,
        # 'податок': 1384, 'надбавка': 0, 'знижка': 384, 'зниження': 347, 'ставка': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'так', 'діти': 3, 'зарплата': 50000,
        # 'податок': 0, 'надбавка': 0, 'знижка': 720, 'зниження': 0, 'ставка': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'ні', 'діти': 2, 'зарплата': 100000,
        # 'податок': 19884, 'надбавка': 4480, 'знижка': 0, 'зниження': 0, 'ставка': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'ні', 'діти': 3, 'зарплата': 100000,
        # 'податок': 16782, 'надбавка': 7176, 'знижка': 0, 'зниження': 0, 'ставка': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'так', 'діти': 3, 'зарплата': 100000,
        # 'податок': 9200, 'надбавка': 2180, 'знижка': 0, 'зниження': 0, 'ставка': 0,3}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'так', 'діти': 5, 'зарплата': 100000,
        # 'податок': 4230, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0,14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'ні', 'діти': 0, 'зарплата': 100000,
        # 'податок': 22986, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'так', 'діти': 2, 'зарплата': 30000,
        # 'податок': 0, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'ні', 'діти': 0, 'зарплата': 200000,
        # 'податок': 64210, 'надбавка': 7498, 'знижка': 0, 'зниження': 0, 'ставка': 0,45}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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

        # {'одружений': 'так', 'діти': 3, 'зарплата': 200000,
        # 'податок': 42842, 'надбавка': 17283, 'знижка': 0, 'зниження': 0, 'ставка': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # перевірки
        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()

Коментарі

  • рядок 11: тестовий клас успадковує клас [unittest.TestCase];
  • рядки 13–19: у тесті UnitTest метод [setUp] виконується перед кожним із методів [test_];
  • рядок 16: отримується конфігурація зі скрипта [config], розглянутого раніше;
  • рядок 18: зберігається посилання на шар [métier];
  • рядок 19: у шару [dao] запитується об’єкт [AdminData], що інкапсулює дані податкової адміністрації, і він зберігається;
  • рядки 21–173: 11 тестів, результати яких було перевірено на офіційному сайті податкової служби за 2019 рік |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
  • рядки 21–33: усі тести побудовано за одним і тим самим зразком;
  • рядок 22: імпортується клас [TaxPayer];
  • рядок 24: платник податків, що тестується;
  • рядок 25: очікувані результати;
  • рядок 26: створення об’єкта [TaxPayer] платника податків;
  • рядок 27: розрахунок податку. Результат міститься в [taxpayer];
  • рядки 29–33: перевірка отриманих результатів;
  • рядок 29: перевіряється сума податку з точністю до євро. Тестування показало, що результати, отримані за допомогою алгоритму, описаного в цьому документі, можуть відрізнятися від офіційних цифр не більше ніж на 1 євро;

Виконання тестів дає такі результати:

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. Головний скрипт

Image

Головний скрипт налаштовується за допомогою наступного скрипта [config]:


def configure():
    import os

    # етап 1 ------
    # налаштування шляху до Python

    # папка цього файлу
    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_dependencies = [
        # локальні залежності
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    # встановлюємо syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # крок 2 ------
    # конфігурація додатка
    config = {
        # абсолютні шляхи до файлів додатка
        "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"
    }

    # інстанціювання шарів додатка
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

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

    # додаємо екземпляри шарів до конфігурації
    config["dao"] = dao
    config["métier"] = métier

    # завантажуємо конфігурацію
    return config

Він аналогічний тому, що використовується для тестування шарів [métier] та [dao].

Основний скрипт [main.py] має такий вигляд:


# налаштовуємо додаток
import config

config = config.configure()

# імпортуємо
from ImpôtsError import ImpôtsError

# отримуємо шари додатка (вони вже інстанційовані)
dao = config["dao"]
métier = config["métier"]

try:
    # отримання податкових шкал
    admindata = dao.get_admindata()
    # зчитування даних про платників податків
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # платників податків?
    if not taxpayers:
        raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # розрахунок податку платників податків
    for taxpayer in taxpayers:
        # «платник податків» є одночасно вхідним і вихідним параметром
        # «taxpayer» буде змінено
        métier.calculate_tax(taxpayer, admindata)
    # запис результатів у текстовий файл
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    # виведення повідомлення про помилку
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # завершено
    print("Travail terminé...")

Примітки

  • рядки 2–4: отримуємо конфігурацію додатка. Також відомо, що Python Path додатка вже сформовано;
  • рядки 9–11: отримуємо посилання на шари [métier] та [dao];
  • рядок 15: отримуємо дані від податкової адміністрації;
  • рядок 17: отримуємо список платників податків, для яких потрібно розрахувати податок;
  • рядки 19–20: якщо цей список порожній, генерується виняток;
  • рядки 22–25: обчислення податку для різних об’єктів [taxpayer] за допомогою шару [métier];
  • рядок 27: [taxpayers] тепер є списком об’єктів [TaxPayer], у яких атрибути (податок, знижка, надбавка, зменшення, ставка) отримали значення. Цей список записується у файл jSON;
  • рядки 28–30: виявлення можливої помилки;
  • рядки 31–33: виконуються в будь-якому випадку;

Виконання скрипта дає ті самі результати, що й у попередніх версіях. Файл з помилками платників податків був новим елементом у цій версії. Після виконання скрипта [main] його вміст виглядає так:


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]

Помилкові рядки були такими:

# дійсні дані: ідентифікатор, сімейний стан, діти, заробітна плата
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
# можуть бути порожні рядки

# створюються рядки з помилками
# недостатньо значень
11,12
# забагато значень
12,oui,3,200000, x, y
# неправильні значення
x,x,x,x

15.2. Версія 4 — додаток 2

У цій версії користувач самостійно вводить список платників податків за допомогою клавіатури. Архітектура додатка буде такою:

Image

З’являється новий модуль: рівень [ui] (User Interface), який буде взаємодіяти з користувачем. Цей рівень матиме інтерфейс і буде реалізований класом.

Image

15.2.1. Інтерфейс [InterfaceImpôtsUi]


# імпорт
from abc import ABC, abstractmethod


# інтерфейс InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
    # виконання класу, що реалізує інтерфейс
    @abstractmethod
    def run(self):
        pass

Інтерфейс [InterfaceImpôtsUi] матиме лише один метод, наведений у рядках 8–10. У цьому прикладі інтерфейс буде реалізовано за допомогою консольної програми, але його також можна реалізувати за допомогою графічного інтерфейсу. Параметри, що передаються методу [run], у цих двох реалізаціях будуть різними. Щоб обійти цю проблему, зазвичай застосовують такий підхід:

  • не передавати параметри методу [run] (або передавати мінімальну кількість параметрів);
  • передавати параметри конструктору класу, що реалізує інтерфейс. Вони можуть відрізнятися в різних реалізаціях. Ці параметри зберігаються як атрибути класу;
  • забезпечити, щоб метод [run] використовував ці атрибути класу (self.x);

Цей метод дозволяє отримати дуже загальний інтерфейс, який уточнюється параметрами конструкторів кожного класу реалізації. Цей метод вже використовувався для модульної версії № 1.

15.2.2. Клас [ImpôtsConsole]

Клас [ImpôtsConsole] реалізує інтерфейс [InterfaceImpôtsUi] наступним чином:


# імпорт
import re

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


# шар [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
    # конструктор
    def __init__(self, config: dict):
        # зберігання параметрів
        self.admindata = config['dao'].get_admindata()
        self.métier = config['métier']

    def run(self):
        # інтерактивний діалог з користувачем
        fini = False
        while not fini:
            # чи одружений платник податків?
            marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # перевірка правильності введених даних
            while marié != "oui" and marié != "non" and marié != "*":
                # повідомлення про помилку
                print("Tapez oui ou non ou *")
                # повторити запитання
                marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # Завершено?
            if marié == "*":
                # діалог завершено
                return
            # кількість дітей
            enfants = input("Nombre d'enfants : ").strip()
            # перевірка правильності введених даних
            if not re.match(r"^\d+$", enfants):
                # повідомлення про помилку
                print("Tapez un nombre entier positif ou nul")
                # почати спочатку
                enfants = input("Nombre d'enfants : ").strip()
            # річний оклад
            salaire = input("Salaire annuel : ").strip()
            # перевірка правильності введених даних
            if not re.match(r"^\d+$", salaire):
                # повідомлення про помилку
                print("Tapez un nombre entier positif ou nul")
                # повторюємо
                salaire = input("Salaire annuel : ").strip()
            # розрахунок податку
            taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
            self.métier.calculate_tax(taxpayer, self.admindata)
            # відображення
            print(f"Impôt du contribuable = {taxpayer}\n\n")
            # наступний платник податків
  • рядок 9: клас [ImpôtsConsole] реалізує інтерфейс [InterfaceImpôtsUi];
  • рядок 11: конструктор класу отримує параметр — словник [config] з конфігурації додатка;
    • рядок 13: отримуються дані від податкової служби, що дозволяють розрахувати податок;
    • рядок 14: зберігається посилання на шар [métier];
  • рядок 16: реалізація методу [run] інтерфейсу;
  • рядки 19–53: діалог з користувачем. Він полягає
    • у запиті трьох відомостей (сімейний стан, наявність дітей, заробітна плата) про платника податків;
    • у розрахунку його податку;
    • у його відображенні;
    • діалог завершується, коли користувач відповідає * на перше запитання;
  • рядки 20–27: запитується, чи одружений платник податків, і перевіряється правильність відповіді;
  • рядки 29–31: якщо користувач відповів «*» на запитання, діалог припиняється;
  • рядки 32–39: запитується кількість дітей платника податків і перевіряється правильність відповіді;
  • рядки 40–47: запитується річний дохід платника податків і перевіряється правильність відповіді;
  • рядки 48–50: на основі цієї інформації за допомогою шару [métier] розраховується податок платника податків;
  • рядок 52: відображається сума податку;

15.2.3. Головний скрипт

Головний скрипт [main] налаштовується за допомогою такого файлу [config]:


def configure():
    import os

    # крок 1 ------
    # налаштування шляху до Python

    # папка цього файлу
    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_dependencies = [
        # локальні залежності
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    # встановлюємо syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # крок 2 ------
    # конфігурація додатка
    config = {
        # абсолютні шляхи до файлів додатка
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json",
    }

    # інстанціювання шарів додатка
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier
    from ImpôtsConsole import ImpôtsConsole

    # рівень DAO
    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    # бізнес-шар
    métier = ImpôtsMétier()
    # додаємо екземпляри шарів у конфігурацію
    config["dao"] = dao
    config["métier"] = métier
    # шар UI
    ui = ImpôtsConsole(config)
    config["ui"] = ui

    # генеруємо конфігурацію
    return config

Координаційний скрипт має такий вигляд (main.py):


# налаштовуємо додаток
import config

config = config.configure()

# імпортуємо
from ImpôtsError import ImpôtsError

# отримуємо шари додатка (вони вже інстанційовані)
ui = config["ui"]

# код
try:
    # виконання шару [ui]
    ui.run()
except ImpôtsError as erreur:
    # виводиться повідомлення про помилку
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # виконується в будь-якому випадку
    print("Travail terminé...")
  • рядки 1–4: отримуємо конфігурацію додатка;
  • рядок 10: отримуємо посилання на шар [ui];
  • рядки 12–21: структура коду така сама, як і в попередньому додатку: код, оточений блоком try/catch для перехоплення можливих винятків;
  • рядок 15: запускається шар [ui]: після цього починається діалог з користувачем;
  • рядки 16–18: перехоплення можливого винятку;

Ось приклад виконання:


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