Skip to content

15. تمرین عملی – نسخهٔ ۴

Image

در اینجا تمرین توصیف‌شده در بخش |نسخه ۳| را مجدداً بررسی می‌کنیم و اکنون با استفاده از کلاس‌ها و رابط‌ها به آن می‌پردازیم. ما دو برنامه خواهیم نوشت:

برنامهٔ ۱ به شرح زیر خواهد بود:

Image

یک اسکریپت اصلی، [main یک لایه [dao] و یک لایه [métier] را نمونه سازی خواهد کرد:

  • لایه [dao] مسئول مدیریت داده‌های ذخیره‌شده در فایل‌های متنی و متعاقباً در یک پایگاه داده خواهد بود؛
  • لایه [métier] مسئول محاسبه مالیات خواهد بود؛

در این برنامه، هیچ ورودی کاربری وجود نخواهد داشت: داده‌های مالیات‌دهندگان از یک فایل متنی بازیابی خواهد شد که نام آن به ماژول [main] ارائه می‌شود.

در برنامهٔ ۲، کاربر داده‌های مالیات‌دهندگان را از طریق صفحه‌کلید وارد خواهد کرد. سپس معماری به شرح زیر تکامل خواهد یافت:

Image

  • لایه [dao] (Data Access Object) دسترسی به داده‌های خارجی را مدیریت می‌کند
  • لایه [métier] منطق کسب‌وکار را مدیریت می‌کند، در این مورد محاسبه مالیات. این لایه خود داده‌ها را مدیریت نمی‌کند. این داده‌ها می‌توانند از دو منبع تأمین شوند:
    • لایه [dao] برای داده‌های پایدار؛
    • لایه [ui] برای داده‌های ارائه‌شده توسط کاربر.
  • لایه [ui] (رابط کاربری) تعاملات کاربر را مدیریت می‌کند؛
  • لایه [main] به‌عنوان هماهنگ‌کننده عمل می‌کند؛

در ادامه، لایه‌های [dao]، [métier] و [ui] هر یک با استفاده از یک کلاس پیاده‌سازی خواهند شد. لایه‌های [métier] و [dao] برای هر دو برنامه یکسان خواهند بود. به همین دلیل آن‌ها در یک نسخه واحد از تمرین برنامه ترکیب شده‌اند.

15.1. نسخهٔ ۴ – برنامهٔ ۱

نسخهٔ ۴ مالیات را برای فهرستی از مودیان مالیاتی که در یک فایل متنی قرار دارند، محاسبه می‌کند. معماری آن به شرح زیر است:

Image

15.1.1. نهادها

Image

اشیاء کلاس‌های داده هستند. نقش آن‌ها محصورسازی داده‌ها و فراهم کردن گترها و سترهایی است که امکان بررسی اعتبار آن داده‌ها را فراهم می‌کند. اشیاء بین لایه‌ها مبادله می‌شوند. یک شیء واحد می‌تواند از لایه [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"
        ]
  • خط ۵: کلاس [AdminData] از کلاس [BaseEntity] که در بخش |BaseEntity| توصیف شده است، ارث می‌برد. به یاد داشته باشید که کلاس‌هایی که از کلاس [BaseEntity] ارث می‌برند باید تعریف کنند:
    • یک ویژگی کلاس [excluded_keys] (خط ۷) که فهرست ویژگی‌های شیء را که هنگام تبدیل شیء به یک دیکشنری حذف می‌شوند، لیست می‌کند؛
    • یک متد استاتیک [get_allowed_keys] (خطوط ۱۰–۲۶) که لیست ویژگی‌های پذیرفته‌شده هنگام مقداردهی اولیه شیء با یک دیکشنری را بازمی‌گرداند؛

هیچ متد setter برای بررسی اعتبار داده‌های مورد استفاده برای инициалиزه‌سازی یک شی [AdminData] استفاده نشد. این به این دلیل است که این شی یکتا است و توسط پیکربندی تعریف شده، و بنابراین بعید است که حاوی خطا باشد.

15.1.1.3. کلاس [TaxPayer]

کلاس [TaxPayer] یک مؤدی مالیاتی را مدل‌سازی خواهد کرد:


# واردات
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError


# یک مؤدی
class TaxPayer(BaseEntity):
    #نماینده یک مؤدی مالیاتی است
    #شناسه: شناسگر
    # متأهل: بله / خیر
    # فرزندان: تعداد فرزندان
    # حقوق: حقوق سالانهٔ آنها
    # مالیات: مبلغ مالیات پرداختنی
    # عوارض اضافی: عوارض اضافی قابل پرداخت
    # معافیت مالیاتی: معافیت مالیاتی از مالیات پرداختنی
    # کاهش: کاهش در مالیات پرداختنی
    #نرخ: نرخ مالیاتی مودی

    #کلیدهای مستثنی‌شده از عبارت کلاس
    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):
        # فرزندان باید یک عدد صحیح ≥ 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] یک مؤدی مالیاتی را در بر می‌گیرد؛
  • خط ۷: کلاس [TaxPayer] از کلاس [BaseEntity] ارث می‌برد. بنابراین شناسه [id] را دارد؛
  • خط ۲۰: هیچ ویژگی‌ای از وضعیت یک شیء [AdminData] مستثنی نشده است؛
  • خطوط ۲۲–۲۵: ویژگی‌های کلاس. این ویژگی‌ها در خطوط ۹–۱۷ مشخص شده‌اند؛
  • خطوط ۲۷–۵۸: گترها برای ویژگی‌های کلاس؛
  • خطوط 60–161: متدهای setter برای ویژگی‌های کلاس. شایان ذکر است که مزیت یک کلاس که داده‌ها را در بر می‌گیرد نسبت به یک دیکشنری ساده این است که کلاس می‌تواند ویژگی‌های خود را با استفاده از متدهای setter اعتبارسنجی کند؛

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]: نام فایل متنی حاوی مالیات‌دهندگان
        # پیکربندی [resultsFilename]: نام فایل نتایج jSON
        # config[errorsFilename]: نام فایل خطا

        # تنظیمات ذخیره شدند
        self.taxpayers_filename = config.get("taxpayersFilename")
        self.taxpayers_results_filename = config.get("resultsFilename")
        self.errors_filename = config.get("errorsFilename")

    # ------------------
    # رابط QZXW2HTML CSUltcMO0dHNEYW8ZQX
    # ------------------

    # فهرست داده‌های مودی
    def get_taxpayers_data(self) -> dict:
        

    #ثبت مالیات مودیان
    def write_taxpayers_results(self, taxpayers: list):
        

    # خواندن مقاطع مالیاتی
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass
  • خط ۱۳: کلاس [AbstractImpôtsDao] رابط [InterfaceImpôtsDao] را پیاده‌سازی می‌کند. در نتیجه، سه متد این رابط موجود هستند:

    • [get_taxpayers_data]: خط ۳۱؛
    • [write_taxpayers_results]: خط ۳۵؛
    • [get_admindata]: خط ۴۰. این متد توسط کلاس [AbstractImpôtsDao] پیاده‌سازی نخواهد شد، بنابراین به عنوان انتزاعی (abstract) اعلام شده است (خط ۳۹);
  • خط 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:
                        #بازیابی چهار فیلد id، married، children و salary که خط مودی را تشکیل می‌دهند
                        (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()
  • خط ۴: داده‌های مودی (وضعیت تأهل، فرزندان، حقوق) در یک لیست از اشیاء از نوع [TaxPayer] قرار داده خواهد شد؛
  • خطوط ۸–۹: فایل متنی مودیان برای خواندن باز می‌شود. محتوای آن در قالب زیر است:
#داده‌های معتبر: شناسه، متأهل، فرزندان، حقوق
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 باید به‌عنوان نادرست علامت‌گذاری شوند. خطاها در یک فایل جداگانه ثبت می‌شوند؛

بیایید بررسی کد را ادامه دهیم:

  • خط ۴: داده‌ها از فایل متنی به لیست [taxPayersData] منتقل می‌شوند؛
  • خطوط ۱۴–۳۱: فایل مالیات‌دهندگان خط به خط خوانده می‌شود؛
  • خط ۱۴: انتهای فایل زمانی فرا می‌رسد که یک خط خالی خوانده شود (هیچ چیز – حتی کاراکتر پایان خط \r\n)؛
  • خط ۲۰: خطوط خالی و نظرات نادیده گرفته می‌شوند. یک خط زمانی نظر محسوب می‌شود که پس از حذف فاصله‌های خالی قبل و بعد از متن، اولین کاراکتر آن # باشد؛
  • خط 24: یک خط معتبر از چهار فیلد جداشده با ویرگول تشکیل شده است. ما این‌ها را بازیابی می‌کنیم. تخصیص داده‌ها به یک تپلی چهارعنصری زمانی ناموفق است که دقیقاً چهار آیتم داده تخصیص داده نشده باشد؛
  • خط ۲۵: اگر هر یک از چهار فیلد بازیابی‌شده [id, marié, enfants, salaire] نامعتبر باشد، آنگاه متد [BaseEntity.fromdict] یک استثنا از نوع [MyException] پرتاب خواهد کرد؛
  • خطوط ۲۵–۲۶: یک شیء [TaxPayer] به لیست [taxpayers_data] مودیان مالیاتی اضافه می‌شود؛
  • خطوط 27–29: هرگونه خطا در لیستی به نام [erreurs] انباشته می‌شود. این لیست در خط 6 ایجاد شده است؛
  • خطوط ۳۳–۳۶: فهرست خطاهای مشاهده‌شده در فایل متنی [errorsFilename] ذخیره می‌شود. دو نوع خطا وجود دارد:
    • یک سطر تعداد صحیح فیلدهای مورد انتظار را نداشت؛
    • اطلاعات در سطر نادرست بود و ساخت یک شیء [TaxPayer] با شکست مواجه شد؛
  • خطوط ۳۹–۴۱: هر خطا (BaseException) با قرار دادن آن در یک نوع [ImpôtsError] رهگیری و منتقل می‌شود؛
  • خطوط ۴۲–۴۵: در همه موارد، چه با موفقیت و چه با خطا، فایل متنی مالیات‌دهندگان در صورت باز بودن بسته می‌شود؛

متد [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()
  • خط ۲: متد یک لیست از مودیان [taxpayers] را دریافت می‌کند که باید آن را در فایل متنی [self.taxpayers_results_filename] با فرمت jSON ذخیره کند؛
  • خط ۱۰: ایجاد فایل نتایج UTF-8;
  • خط ۱۲: در اینجا تابع [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] است؛

  • خط ۱۲: نتیجه بازگردانده شده توسط تابع [map] نه لیست [liste2 بلکه یک شی از نوع [map] است. برای به‌دست آوردن [liste2]، باید از عبارت [list(mapping)] (خط 14) استفاده کنید؛
  • خط ۱۴: لیست [liste2] در قالب jSON در فایل [self.taxpayers_results_filename] ذخیره می‌شود؛
  • خطوط ۱۵–۱۷: هر نوع استثنا قبل از اینکه دوباره پرتاب شود، رهگیری و در یک خطای از نوع [ImpôtsError] بسته‌بندی می‌شود (خط ۱۷);
  • خطوط ۱۹–۲۱: در همه موارد، چه با موفقیت و چه با شکست، اگر فایل نتایج باز شده باشد، بسته می‌شود؛

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
  • خط ۱۱: کلاس [ImpôtsDaoWithAdminDataInJsonFile] از کلاس [AbstractImpôtsDao] ارث می‌برد. بنابراین، این کلاس رابط [InterfaceImpôtsDao] را پیاده‌سازی می‌کند؛
  • خط ۱۳: سازنده به عنوان پارامتر یک دیکشنری حاوی اطلاعات خطوط ۱۴–۱۷ را می‌پذیرد؛
  • خط ۲۰: کلاس والد مقداردهی اولیه می‌شود؛
  • خط ۲۴: فایل jSON حاوی داده‌های مقامات مالیاتی باز می‌شود؛
  • خط ۲۵: فایل UTF-8 حاوی داده‌های مقامات مالیاتی باز می‌شود؛
  • خط ۲۷: محتویات فایل خوانده شده و در شیء [self.admindata] از نوع [AdminData] قرار داده می‌شوند. کلیدها در فایل jSON باید با ویژگی‌های پذیرفته‌شده برای یک شیء [AdminData] مطابقت داشته باشند؛ در غیر این صورت، متد [fromdict] یک استثنا پرتاب خواهد کرد؛
  • خطوط ۲۸–۳۰: مدیریت استثنا. هر استثنایی که ممکن است رخ دهد، قبل از پرتاب مجدد در یک نوع [ImpôtsError] محصور می‌شود؛
  • خطوط ۳۲–۳۴: اگر فایل باز شده باشد، بسته می‌شود؛
  • خطوط ۴۲–۴۳: پیاده‌سازی متد [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):
    #محاسبه مالیات برای یک مودی مالیاتی
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        pass
  • رابط [InterfaceImpôtsMétier] یک متد واحد را تعریف می‌کند:
  • خط ۱۲: متد [calculate_tax] برای محاسبه مالیات یک مودی واحد، [taxpayer استفاده می‌شود. [admindata] شیء [AdminData] است که داده‌های مرجع مالیاتی را در بر می‌گیرد؛
  • خط ۱۲: متد [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, married, children, salary, tax, discount, surcharge, reduction, rate): ابجکتی که نماینده یک مودی و مالیات اوست؛
  • admindata: ابجکتی که داده‌های مرجع مالیاتی را در بر می‌گیرد؛

ما تغییرات انجام‌شده را با استفاده از یک متد به‌عنوان مثال نشان می‌دهیم؛


    #محاسبه مالیات – فاز ۱
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        # مودی (شناسه، وضعیت تأهل، فرزندان، حقوق، مالیات، معافیت مالیاتی، عوارض، کاهش، نرخ مالیاتی)
        #داده‌های اداری: اطلاعات سازمان مالیاتی

        #محاسبه مالیات با فرزندان
        self.calculate_tax_2(taxpayer, admindata)
        #نتایج در مالیات‌دهنده
        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)
            #نتایج در مالیات‌دهندهٔ دوم است
            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:
            # اگر مودی فرزند نداشته باشد، آنگاه tax2 = tax1
            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)
  • خط ۳: متد [calculate_tax] تنها متد در رابط [InterfaceImpôtsMétier] است. این متد دو پارامتر می‌گیرد:
    • [tapPayer]: مودی مالیاتی که مالیات برای او محاسبه می‌شود؛
    • [admindata]: ابجکتی که داده‌های مرجع مالیاتی را در خود جای می‌دهد؛
    • نتایج محاسبه در پارامتر [taxpayer] (خطوط 40–50) گنجانده شده‌اند. بنابراین محتوای این شیء قبل و بعد از فراخوانی متد یکسان نیست؛

15.1.4. آزمایش لایه‌های [dao] و [métier]

Image

  • [TestDaoMétier] کلاس آزمایشی برای لایه‌های [dao] و [métier] است؛
  • [config] فایل پیکربندی آزمایشی است؛

پیکربندی برای [config] به شرح زیر است:


def configure():
    import os

    # مرحله ۱ ------
    #پیکربندی مسیر پایتون

    # پوشهٔ این فایل
    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)

    # مرحله ۲ ------
    # پیکربندی برنامه
    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
  • خطوط ۴–۲۳: مسیر پایتون برای تست‌ها پیکربندی شده است؛
  • خطوط ۳۲–۴۱: لایه‌های [dao] و [métier] نمونه سازی می‌شوند. ارجاعات آن‌ها در دیکشنری [config] قرار داده می‌شوند؛
  • خط ۴۴: این دیکشنری بازگردانده می‌شود؛

کلاس تست [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, 'حقوق': 100,000,
        #'مالیات': 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, 'حقوق': 100,000,
        # 'مالیات': 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, 'حقوق': 200,000,
        # 'مالیات': 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()

توضیحات

  • خط ۱۱: کلاس تست از کلاس [unittest.TestCase] ارث می‌برد؛
  • خطوط ۱۳–۱۹: در یک تست UnitTest، متد [setUp] قبل از هر یک از متدهای [test_] اجرا می‌شود؛
  • خط ۱۶: پیکربندی از اسکریپت [config] که قبلاً مورد بحث قرار گرفت، بازیابی می‌شود؛
  • خط ۱۸: مرجعی به لایه [métier] ذخیره می‌شود؛
  • خط ۱۹: لایه [dao] برای شی [AdminData] که داده‌های مرجع مالیاتی را در بر می‌گیرد، پرس‌وجو می‌شود و این شی ذخیره می‌شود؛
  • خطوط ۲۱–۱۷۳: ۱۱ آزمون که نتایج آن‌ها در وب‌سایت رسمی مالیات ۲۰۱۹ تأیید شده است |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm|;
  • خطوط ۲۱–۳۳: تمام تست‌ها با استفاده از یک قالب یکسان ساخته شده‌اند؛
  • خط ۲۲: کلاس [TaxPayer] وارد شده است؛
  • خط 24: مالیات‌دهنده آزمایش شد؛
  • خط ۲۵: نتایج مورد انتظار؛
  • خط ۲۶: ایجاد شیء مودی مالیاتی [TaxPayer];
  • خط ۲۷: محاسبه مالیات آنها. نتیجه در [taxpayer] است؛
  • خطوط ۲۹–۳۳: تأیید نتایج به‌دست‌آمده؛
  • خط ۲۹: مبلغ مالیات تا نزدیک‌ترین یورو بررسی می‌شود. آزمایش‌ها نشان داده‌اند که نتایج حاصل از الگوریتم در این سند ممکن است تا ۱ یورو با ارقام رسمی متفاوت باشد؛

اجرای تست‌ها نتایج زیر را می‌دهد:

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

    # مرحله ۱ ------
    #پیکربندی مسیر پایتون

    # پوشهٔ این فایل
    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)

    # مرحله ۲ ------
    # پیکربندی برنامه
    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:
        # 'مالیات‌دهنده' هم پارامتر ورودی و هم پارامتر خروجی است
        # 'مالیات‌دهنده' اصلاح خواهد شد
        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é...")

یادداشت‌ها

  • خطوط ۲–۴: پیکربندی برنامه بازیابی می‌شود. همچنین می‌دانیم که مسیر پایتون (Python Path) برنامه تنظیم شده است؛
  • خطوط ۹–۱۱: ما ارجاعات به لایه‌های [métier] و [dao] را بازیابی می‌کنیم؛
  • خط ۱۵: داده‌ها را از مراجع مالیاتی بازیابی می‌کنیم؛
  • خط ۱۷: ما فهرست مودیان مالیاتی را که باید مالیات برای آن‌ها محاسبه شود، بازیابی می‌کنیم؛
  • خطوط ۱۹–۲۰: اگر این لیست خالی باشد، یک استثنا ایجاد می‌شود؛
  • خطوط 22–25: مالیات برای اشیاء مختلف [taxpayer] با استفاده از لایه [métier] محاسبه می‌شود؛
  • خط ۲۷: [taxpayers] اکنون فهرستی از اشیاء [TaxPayer] است که در آن‌ها ویژگی‌ها (مالیات، تخفیف، عوارض، کاهش، نرخ) مقداردهی شده‌اند. این فهرست در یک فایل jSON نوشته می‌شود؛
  • خطوط ۲۸–۳۰: رسیدگی به هرگونه خطا؛
  • خطوط ۳۱–۳۳: در همه موارد اجرا می‌شود؛

اجرای اسکریپت نتایج مشابهی با نسخه‌های قبلی تولید می‌کند. فایلی که شامل خطاهای مالیات‌دهنده بود، ویژگی جدیدی در این نسخه بود. پس از اجرای اسکریپت [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. نسخه ۴ – برنامه ۲

در این نسخه، کاربر فهرست مودیان را از طریق صفحه‌کلید وارد می‌کند. معماری برنامه به شرح زیر خواهد بود:

Image

یک ماژول جدید معرفی شده است: لایه [ui] (رابط کاربری)، که با کاربر تعامل خواهد داشت. این لایه دارای یک رابط خواهد بود و توسط یک کلاس پیاده‌سازی می‌شود.

Image

15.2.1. رابط [InterfaceImpôtsUi]


# واردات‌ها
from abc import ABC, abstractmethod


# رابط InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
    #اجرای کلاسی که رابط را پیاده‌سازی می‌کند
    @abstractmethod
    def run(self):
        pass

رابط [InterfaceImpôtsUi] تنها یک متد خواهد داشت، همانطور که در خطوط ۸–۱۰ نشان داده شده است. این رابط در اینجا با استفاده از یک کنسول اپلیکیشن پیاده‌سازی می‌شود، اما می‌توان آن را با استفاده از یک رابط کاربری گرافیکی نیز پیاده‌سازی کرد. پارامترهای ارسال‌شده به متد [run] در دو پیاده‌سازی یکسان نخواهند بود. برای حل این مشکل، رویکرد معمول این است که:

  • هیچ پارامتری (یا حداقل تعداد پارامترها) را به متد [run] ارسال کنید؛
  • پارامترها را به سازنده کلاسی که رابط را پیاده‌سازی می‌کند، ارسال کنید. این پارامترها ممکن است از یک پیاده‌سازی به پیاده‌سازی دیگر متفاوت باشند. این پارامترها به عنوان ویژگی‌های کلاس ذخیره می‌شوند؛
  • اطمینان حاصل کنید که متد [run] از این ویژگی‌های کلاس (self.x) استفاده می‌کند؛

این متد یک رابط بسیار کلی را فراهم می‌کند که توسط پارامترهای سازنده‌های هر کلاس پیاده‌ساز مشخص می‌شود. این متد قبلاً برای نسخه ماژولار شماره ۱ استفاده شده است.

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")
            # مودی بعدی
  • خط ۹: کلاس [ImpôtsConsole] رابط [InterfaceImpôtsUi] را پیاده‌سازی می‌کند؛
  • خط ۱۱: سازنده کلاس یک پارامتر می‌گیرد، فرهنگ لغت [config] حاوی پیکربندی برنامه؛
    • خط ۱۳: داده‌ها از مراجع مالیاتی بازیابی می‌شوند تا امکان محاسبه مالیات فراهم شود؛
    • خط ۱۴: یک مرجع به لایه [métier] ذخیره می‌شود؛
  • خط ۱۶: پیاده‌سازی متد [run] از رابط؛
  • خطوط ۱۹–۵۳: تعامل کاربر. این شامل
    • درخواست سه مورد اطلاعات از مؤدی (وضعیت تأهل، تعداد فرزندان، حقوق)؛
    • محاسبه مالیات آنها؛
    • نمایش مبلغ مالیات؛
    • گفت‌وگو زمانی پایان می‌یابد که کاربر به سؤال اول پاسخ * می‌دهد؛
  • خطوط ۲۰–۲۷: از مودی پرسیده می‌شود که آیا متأهل است یا خیر و اعتبار پاسخ بررسی می‌شود؛
  • خطوط ۲۹–۳۱: اگر کاربر به سؤال با «*» پاسخ داده باشد، دیالوگ خاتمه می‌یابد؛
  • خطوط ۳۲–۳۹: از مودی پرسیده می‌شود که چند فرزند دارد و اعتبار پاسخ بررسی می‌شود؛
  • خطوط ۴۰–۴۷: حقوق سالانه مالیات‌دهنده درخواست می‌شود و اعتبار پاسخ بررسی می‌شود؛
  • خطوط ۴۸–۵۰: با استفاده از این اطلاعات، از لایه [métier] برای محاسبه مالیات مودی استفاده می‌شود؛
  • خط ۵۲: مبلغ مالیات نمایش داده می‌شود؛

15.2.3. اسکریپت اصلی

اسکریپت اصلی [main] توسط فایل زیر [config] پیکربندی می‌شود:


def configure():
    import os

    # مرحله ۱ ------
    #پیکربندی مسیر پایتون

    # پوشهٔ این فایل
    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)

    # مرحله ۲ ------
    # پیکربندی برنامه
    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 = 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é...")
  • خطوط ۱–۴: پیکربندی برنامه بازیابی می‌شود؛
  • خط ۱۰: مرجعی به لایه [ui] بازیابی می‌شود؛
  • خطوط ۱۲–۲۱: ساختار کد مشابه برنامه قبلی است: کد در یک بلوک try/catch قرار گرفته است تا هرگونه خطای احتمالی را مدیریت کند؛
  • خط ۱۵: لایه [ui] دستور اجرا دریافت می‌کند: سپس دیالوگ با کاربر آغاز می‌شود؛
  • خطوط ۱۶–۱۸: هرگونه استثنا گرفته می‌شود؛

در اینجا نمونه‌ای از خروجی آورده شده است:


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