Skip to content

15. تمرين تطبيقي - الإصدار 4

Image

هنا نعيد النظر في التمرين الموصوف في قسم |الإصدار 3| ونقوم الآن بتنفيذه باستخدام الفئات والواجهات. سنكتب تطبيقين:

سيكون التطبيق 1 كما يلي:

Image

سيقوم البرنامج النصي الرئيسي [main] بإنشاء مثيل لطبقة [DAO] وطبقة [business]:

  • ستكون طبقة [DAO] مسؤولة عن إدارة البيانات المخزنة في ملفات نصية ثم في قاعدة بيانات لاحقًا؛
  • ستكون طبقة [business] مسؤولة عن حساب الضريبة؛

في هذا التطبيق، لن يكون هناك أي إدخال من المستخدم: ستوجد بيانات دافعي الضرائب في ملف نصي سيتم تمرير اسمه إلى الوحدة النمطية [main].

في التطبيق 2، سيقوم المستخدم بإدخال بيانات دافعي الضرائب عبر لوحة المفاتيح. وستتطور البنية بعد ذلك على النحو التالي:

Image

  • تتولى طبقة [DAO] (كائن الوصول إلى البيانات) الوصول إلى البيانات الخارجية
  • تتولى طبقة [business] معالجة منطق الأعمال، وفي هذه الحالة حساب الضرائب. ولا تتولى معالجة البيانات. ويمكن أن تأتي هذه البيانات من مصدرين:
    • طبقة [DAO] للبيانات الدائمة؛
    • طبقة [UI] للبيانات التي يقدمها المستخدم.
  • تتولى طبقة [ui] (واجهة المستخدم) التعامل مع المستخدم؛
  • [main] تعمل كمنسق؛

فيما يلي، سيتم تنفيذ كل من طبقات [dao] و[business] و[ui] باستخدام فئة. وستكون طبقتا [business] و[dao] متماثلتين في كلا التطبيقين. ولهذا السبب تم دمجهما في نسخة واحدة من تمرين التطبيق.

15.1. الإصدار 4 – التطبيق 1

يحسب الإصدار 4 الضريبة لقائمة من دافعي الضرائب المخزنة في ملف نصي. وله البنية التالية:

Image

15.1.1. الكيانات

Image

الكيانات هي فئات بيانات. وتتمثل مهمتها في تغليف البيانات وتوفير أدوات الحصول/التعيين التي تسمح بالتحقق من صحة البيانات. يتم تبادل الكيانات بين الطبقات. يمكن لكيان واحد الانتقال من طبقة [ui] إلى طبقة [dao] والعكس صحيح.

15.1.1.1. فئة [ImpôtsError]

سنستخدم فئة استثناء مخصصة:

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


class ImpôtsError(MyException):
    pass

بمجرد أن تواجه طبقات [business] و [DAO] مشكلة، فإنها ستثير هذا الاستثناء. وهو مشتق من فئة [MyException]. وبالتالي، يتم استخدامه على النحو التالي: [raise ImpôtsError(error_code, error_message)].

15.1.1.2. فئة [AdminData]

تغلف فئة [AdminData] الثوابت المستخدمة في حسابات الضرائب:

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"
        ]
  • السطر 5: تمتد فئة [AdminData] من فئة [BaseEntity] الموضحة في قسم |BaseEntity|. تذكر أن الفئات التي تمتد من فئة [BaseEntity] يجب أن تحدد:
    • سمة فئة [excluded_keys] (السطر 7) التي تسرد خصائص الكائن المستبعدة عند تحويل الكائن إلى قاموس؛
    • طريقة ثابتة [get_allowed_keys] (الأسطر 10–26) التي تُرجع قائمة بالخصائص المقبولة عند تهيئة الكائن باستخدام قاموس؛

لم نستخدم أدوات التعيين للتحقق من صحة البيانات المستخدمة لتهيئة كائن [AdminData]. وذلك لأن هذا الكائن فريد ومُعرَّف بواسطة التكوين، وبالتالي من غير المرجح أن يحتوي على أخطاء.

15.1.1.3. فئة [TaxPayer]

ستقوم فئة [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")

ملاحظات:

  • تغلف فئة [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):

#  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

تحدد الواجهة ثلاث طرق:

  • [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] على النحو التالي:

#  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
  • السطر 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] هي كما يلي:

    #  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()
  • السطر 4: سيتم وضع بيانات دافع الضرائب (متزوج، أطفال، راتب) في قائمة من الكائنات من النوع [TaxPayer]؛
  • السطران 8-9: نفتح ملف نصي للمكلف بالقراءة. محتواه بالصيغة التالية:
# 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

مقارنة بالإصدارات السابقة:

  • يبدأ كل سطر في ملف [taxpayersFilename] برقم تعريف دافع الضرائب، وهو رقم واحد؛
  • يُسمح بالتعليقات والأسطر الفارغة؛
  • سنقوم بمعالجة الأخطاء. وبالتالي، يجب وضع علامة "غير صالح" على الأسطر 17 و19 و21. يتم تسجيل الأخطاء في ملف منفصل؛

لنواصل مراجعة الكود:

  • السطر 4: يتم نقل البيانات من الملف النصي إلى قائمة [taxPayersData]؛
  • الأسطر 14-31: يتم قراءة ملف دافعي الضرائب سطراً سطراً؛
  • السطر 14: يتم الوصول إلى نهاية الملف عند قراءة سطر فارغ (لا شيء — ولا حتى حرف نهاية السطر \r\n)؛
  • السطر 20: يتم تجاهل الأسطر الفارغة والتعليقات. يعتبر السطر تعليقًا إذا كان الحرف الأول هو الحرف # بعد إزالة المسافات البيضاء قبل النص وبعده؛
  • السطر 24: يتكون السطر الصحيح من أربعة حقول مفصولة بفواصل. يتم استرداد هذه الحقول. يفشل تعيين البيانات إلى تابع مكون من أربعة عناصر إذا لم يتم تعيين أربع نقاط بيانات بالضبط؛
  • السطر 25: إذا كان أي من الحقول الأربعة المسترجعة [id, married, children, salary] غير صالح، فإن طريقة [BaseEntity.fromdict] ستثير استثناء [MyException]؛
  • السطران 25-26: تتم إضافة كائن [TaxPayer] إلى قائمة دافعي الضرائب [taxpayers_data]؛
  • الأسطر 27-29: يتم تجميع أي أخطاء في قائمة [errors]. تم إنشاء هذه القائمة في السطر 6؛
  • الأسطر 33–36: يتم حفظ قائمة الأخطاء التي تمت مواجهتها في ملف نصي [errorsFilename]. هناك نوعان من الأخطاء:
    • لم يكن للصف العدد الصحيح من الحقول المتوقعة؛
    • المعلومات الموجودة في الصف غير صحيحة وفشل إنشاء كائن [TaxPayer]؛
  • الأسطر 39–41: يتم اكتشاف أي خطأ (BaseException) ونقله عن طريق تغليفه في نوع [TaxPayerError]؛
  • الأسطر 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] هي كما يلي:

    #  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()
  • السطر 2: تتلقى الطريقة قائمة بالمكلفين [taxpayers] التي يجب حفظها في ملف نصي [self.taxpayers_results_filename] بتنسيق JSON؛
  • السطر 10: إنشاء ملف النتائج بتنسيق UTF-8؛
  • السطر 12: نقدم هنا دالة [map]، التي تكون صيغتها هنا [map (function, list1)]. تُطبق الدالة [function] على كل عنصر من عناصر [list1] وتُنتج عنصرًا جديدًا يُضاف إلى قائمة [list2]. وأخيرًا، لكل i:

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

هنا، [list1] هي القائمة [taxPayers]، وهي قائمة من الكائنات من النوع [TaxPayer]. يتم التعبير عن الدالة [function] هنا على أنها ما يُسمى بدالة [lambda] التي تصف التحويل المطبق على عنصر [taxpayer] من القائمة [taxpayers]: يتم استبدال كل عنصر [taxpayer] بقاموسه [taxpayer.asdict()]. وأخيرًا، فإن القائمة الناتجة [list2] هي قائمة القواميس الخاصة بالعناصر الموجودة في قائمة [taxpayers]؛

  • السطر 12: النتيجة التي تعيدها الدالة [map] ليست القائمة [list2] بل كائن من النوع [map]. للحصول على [list2]، يجب استخدام التعبير [list(mapping)] (السطر 14)؛
  • السطر 14: يتم حفظ القائمة [list2] بتنسيق 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] هي كما يلي:

#  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
  • السطر 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. طبقة [business]

Image

15.1.3.1. واجهة [InterfaceImpôtsMétier]

ستكون واجهة طبقة [الأعمال] كما يلي:

#  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
  • تحدد واجهة [BusinessTaxInterface] طريقة واحدة:
  • السطر 12: تحسب الطريقة [calculate_tax] الضريبة لمكلف واحد [taxpayer]. [admindata] هو كائن [AdminData] الذي يغلف بيانات إدارة الضرائب؛
  • السطر 12: لا تُرجع طريقة [calculate_tax] أي نتيجة. يتم تضمين البيانات التي تم الحصول عليها (الضريبة، الرسوم الإضافية، الخصم، التخفيض، المعدل) في المعلمة [taxpayer]: قبل الاستدعاء، تكون هذه السمات فارغة؛ وبعد الاستدعاء، يتم تهيئتها؛

15.1.3.2. فئة [BusinessTaxes]

تنفذ فئة [ImpôtsMétier] واجهة [InterfaceImpôtsMétier] على النحو التالي:

Image

طرق الفئة مشتقة من الوحدة النمطية [impôts_module_02] في القسم |الوحدة النمطية [impots.v02.modules.impôts_module_02]|. وقد قمنا بتحديد معلمات الطريقة في اثنتين فقط:

  • taxpayer(id, married, children, salary, tax, discount, surcharge, reduction, rate): الكائن الذي يمثل دافع الضرائب وضرائبه؛
  • admindata: الكائن الذي يغلف بيانات إدارة الضرائب؛

نوضح التغييرات التي تم إجراؤها باستخدام طريقة؛

    #  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)
  • السطر 3: طريقة [calculate_tax] هي الطريقة الوحيدة في واجهة [InterfaceImpôtsMétier]. وهي تأخذ معلمتين:
    • [tapPayer]: دافع الضرائب الذي يتم حساب الضريبة له؛
    • [admindata]: الكائن الذي يغلف بيانات إدارة الضرائب؛
    • يتم تغليف نتائج الحساب في المعلمة [taxpayer] (الأسطر 40–50). وبالتالي، فإن محتوى هذا الكائن يختلف قبل وبعد استدعاء الطريقة؛

15.1.4. اختبارات طبقات [dao] و [business]

Image

  • [TestDaoMétier] هي فئة UnitTest لاختبار طبقات [dao] و [business]؛
  • [config] هو ملف تكوين الاختبار؛

تكوين [config] هو كما يلي:

def configure():
    import os

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

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    #  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
  • الأسطر 4–23: نقوم بتكوين مسار Python للاختبارات؛
  • الأسطر 32–41: إنشاء مثيلات لطبقتي [dao] و[business]. تخزين مراجعهما في قاموس [config]؛
  • السطر 44: إرجاع هذا القاموس؛

فئة الاختبار [TestDaoMétier] هي كما يلي:

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

تعليقات

  • السطر 11: فئة الاختبار تمتد من فئة [unittest.TestCase]؛
  • الأسطر 13–19: في UnitTest، يتم تنفيذ الأسلوب [setUp] قبل كل أسلوب من أساليب [test_]؛
  • السطر 16: يتم استرداد التكوين من البرنامج النصي [config] الذي تمت مناقشته سابقًا؛
  • السطر 18: يتم تخزين مرجع إلى طبقة [business]؛
  • السطر 19: نطلب كائن [AdminData] — الذي يغلف بيانات إدارة الضرائب — من طبقة [DAO] ونخزنه؛
  • الأسطر 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

    #  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

وهو مشابه للذي يستخدم لاختبار طبقات [business] و [dao].

النص البرمجي الرئيسي [main.py] هو كما يلي:

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

ملاحظات

  • الأسطر 2–4: نسترد تكوين التطبيق. ونعلم أيضًا أن مسار Python الخاص بالتطبيق قد تم إنشاؤه؛
  • الأسطر 9–11: نسترد الإشارات إلى طبقتي [business] و[DAO]؛
  • السطر 15: نسترد البيانات من إدارة الضرائب؛
  • السطر 17: نسترد قائمة دافعي الضرائب الذين يجب حساب ضرائبهم؛
  • الأسطر 19-20: إذا كانت هذه القائمة فارغة، يتم إثارة استثناء؛
  • الأسطر 22-25: نحسب الضريبة لمختلف كائنات [taxpayer] باستخدام طبقة [business]؛
  • السطر 27: أصبحت [taxpayers] الآن قائمة بكائنات [TaxPayer] حيث تم تعيين قيم للسمات (tax، discount، surcharge، reduction، rate). يتم كتابة هذه القائمة في ملف 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]

كانت الأسطر الخاطئة كما يلي:

# 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. الإصدار 4 – التطبيق 2

في هذا الإصدار، يقوم المستخدم بإدخال قائمة دافعي الضرائب عبر لوحة المفاتيح. ستكون بنية التطبيق على النحو التالي:

Image

تظهر وحدة جديدة: طبقة [ui] (واجهة المستخدم)، والتي ستتفاعل مع المستخدم. ستحتوي هذه الطبقة على واجهة وسيتم تنفيذها بواسطة فئة.

Image

15.2.1. واجهة [InterfaceImpôtsUi]

#  imports
from abc import ABC, abstractmethod


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

ستحتوي واجهة [InterfaceImpôtsUi] على طريقة واحدة فقط، وهي تلك الموجودة في الأسطر 8–10. سيتم تنفيذ الواجهة هنا باستخدام تطبيق وحدة التحكم، ولكن يمكن أيضًا تنفيذها باستخدام واجهة مستخدم رسومية. لن تكون المعلمات التي يتم تمريرها إلى طريقة [run] هي نفسها في كلا التنفيذين. للتغلب على هذه المشكلة، فإن النهج المعتاد هو:

  • عدم تمرير أي معلمات إلى طريقة [run] (أو تمرير الحد الأدنى من المعلمات)؛
  • تمرير المعلمات إلى منشئ الفئة التي تنفذ الواجهة. قد تختلف هذه المعلمات من تنفيذ لآخر. يتم تخزين هذه المعلمات كسمات للفئة؛
  • التأكد من أن طريقة [run] تستخدم سمات الفئة هذه (self.x

تسمح هذه الطريقة بواجهة عامة جدًا يتم تحديدها بواسطة معلمات منشئات كل فئة تنفيذ. وقد تم استخدام هذه الطريقة بالفعل في الإصدار المعياري رقم 1.

15.2.2. فئة [ImpôtsConsole]

تنفذ فئة [ImpôtsConsole] واجهة [InterfaceImpôtsUi] على النحو التالي:

#  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
  • السطر 9: تنفذ فئة [TaxConsole] واجهة [TaxUiInterface]؛
  • السطر 11: يتلقى منشئ الفئة معلمة، وهي قاموس [config] الذي يحتوي على تكوين التطبيق؛
    • السطر 13: يتم استرداد البيانات من سلطة الضرائب لحساب الضريبة؛
    • السطر 14: يتم تخزين مرجع إلى طبقة [business]؛
  • السطر 16: تنفيذ طريقة [run] للواجهة؛
  • الأسطر 19–53: تفاعل المستخدم. يتضمن ذلك
    • طلب ثلاث معلومات من دافع الضرائب (الحالة الاجتماعية، والأطفال، والراتب)؛
    • حساب الضريبة المستحقة عليه؛
    • عرض النتيجة؛
    • ينتهي الحوار عندما يجيب المستخدم بـ * على السؤال الأول؛
  • الأسطر 20–27: يسأل البرنامج عما إذا كان دافع الضرائب متزوجًا ويتحقق من صحة الإجابة؛
  • الأسطر 29-31: إذا أجاب المستخدم بـ "*" على السؤال، ينتهي الحوار؛
  • الأسطر 32-39: يُسأل دافع الضرائب عن عدد أطفاله، ويتم التحقق من صحة الإجابة؛
  • الأسطر 40-47: يُطلب من دافع الضرائب ذكر راتبه السنوي، ويتم التحقق من صحة الإجابة؛
  • الأسطر 48-50: باستخدام هذه المعلومات، تحسب طبقة [الأعمال] ضريبة دافع الضرائب؛
  • السطر 52: يتم عرض مبلغ الضريبة؛

15.2.3. النص البرمجي الرئيسي

يتم تكوين البرنامج النصي الرئيسي [main] بواسطة ملف [config] التالي:

def configure():
    import os

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

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

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

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

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

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

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

    #  return the config
    return config

النص البرمجي الرئيسي هو كما يلي (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é...")
  • الأسطر 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