From a4bd5c58a97924285573978b798a76354b5a4c70 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Sat, 16 Nov 2024 17:10:46 +0100 Subject: [PATCH] Use script_ast generator --- CHANGELOG.md | 2 +- openfisca_tunisia_pension/__init__.py | 37 ++- openfisca_tunisia_pension/regimes/__init__.py | 0 openfisca_tunisia_pension/regimes/regime.py | 249 ++++++++++++++++++ openfisca_tunisia_pension/regimes/rsa.py | 88 +++++++ openfisca_tunisia_pension/regimes/rsna.py | 91 +++++++ .../scripts_ast/__init__.py | 0 .../scripts_ast/script_ast.py | 237 +++++++++++++++++ .../tunisia_pension_taxbenefitsystem.py | 25 -- openfisca_tunisia_pension/variables/cnrps.py | 6 +- openfisca_tunisia_pension/variables/data.py | 2 +- .../variables/helpers.py | 8 +- openfisca_tunisia_pension/variables/rsa.py | 4 +- openfisca_tunisia_pension/variables/rsna.py | 204 ++++++++++---- .../{pension_rsna.yaml => rsna_pension.yaml} | 3 +- ..._rsna.yaml => rsna_salaire_reference.yaml} | 3 +- 16 files changed, 873 insertions(+), 86 deletions(-) create mode 100644 openfisca_tunisia_pension/regimes/__init__.py create mode 100644 openfisca_tunisia_pension/regimes/regime.py create mode 100644 openfisca_tunisia_pension/regimes/rsa.py create mode 100644 openfisca_tunisia_pension/regimes/rsna.py create mode 100644 openfisca_tunisia_pension/scripts_ast/__init__.py create mode 100644 openfisca_tunisia_pension/scripts_ast/script_ast.py delete mode 100644 openfisca_tunisia_pension/tunisia_pension_taxbenefitsystem.py rename tests/formulas/{pension_rsna.yaml => rsna_pension.yaml} (94%) rename tests/formulas/{salaire_reference_rsna.yaml => rsna_salaire_reference.yaml} (94%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49313a6..940e78b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ * Move parameters from xml format to yaml files tree ### 1.0.0 -* Renomme `nb_trim_val` en `trimestres_valides` +* Renomme `nb_trim_val` en `duree_assurance` * Utilisation de noms longs pour différent paramètres ## 0.9.2 diff --git a/openfisca_tunisia_pension/__init__.py b/openfisca_tunisia_pension/__init__.py index 6b04e0a..93adb49 100644 --- a/openfisca_tunisia_pension/__init__.py +++ b/openfisca_tunisia_pension/__init__.py @@ -1,3 +1,38 @@ -from openfisca_tunisia_pension.tunisia_pension_taxbenefitsystem import TunisiaPensionTaxBenefitSystem +'''OpenFisca Tunisia Pension tax-benefit system.''' + + +import logging +import os + +from openfisca_core.taxbenefitsystems import TaxBenefitSystem + +from openfisca_tunisia_pension import entities +from openfisca_tunisia_pension.scripts_ast import script_ast + + +COUNTRY_DIR = os.path.dirname(os.path.abspath(__file__)) + +logging.getLogger('numba.core.ssa').disabled = True +logging.getLogger('numba.core.byteflow').disabled = True +logging.getLogger('numba.core.interpreter').disabled = True + +# Convert regimes classes to OpenFisca variables. +script_ast.main(verbose = False) + + +class TunisiaPensionTaxBenefitSystem(TaxBenefitSystem): + '''Tunisian pensions tax benefit system''' + CURRENCY = 'DT' + + def __init__(self): + super(TunisiaPensionTaxBenefitSystem, self).__init__(entities.entities) + + # We add to our tax and benefit system all the variables + self.add_variables_from_directory(os.path.join(COUNTRY_DIR, 'variables')) + + # We add to our tax and benefit system all the legislation parameters defined in the parameters files + parameters_path = os.path.join(COUNTRY_DIR, 'parameters') + self.load_parameters(parameters_path) + CountryTaxBenefitSystem = TunisiaPensionTaxBenefitSystem diff --git a/openfisca_tunisia_pension/regimes/__init__.py b/openfisca_tunisia_pension/regimes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openfisca_tunisia_pension/regimes/regime.py b/openfisca_tunisia_pension/regimes/regime.py new file mode 100644 index 0000000..a2e1109 --- /dev/null +++ b/openfisca_tunisia_pension/regimes/regime.py @@ -0,0 +1,249 @@ +'''Abstract regimes definition.''' + + +from openfisca_core.model_api import * +# from openfisca_core.errors.variable_not_found_error import VariableNotFoundError + +# Import the Entities specifically defined for this tax and benefit system +from openfisca_tunisia_pension.entities import Individu + + +class AbstractRegime(object): + name = None + variable_prefix = None + parameters = None + + class cotisation(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'cotisation retraite employeur' + + def formula(individu, period, parameters): + NotImplementedError + + class duree_assurance(Variable): + value_type = int + entity = Individu + definition_period = YEAR + label = "Durée d'assurance (trimestres validés)" + + # def formula(individu, period, parameters): + # duree_assurance_validee = individu("regime_name_duree_assurance_validee", period) + # annee_de_liquidation = individu('regime_name_liquidation_date', period).astype('datetime64[Y]').astype(int) + 1970 + # majoration_duree_assurance = individu('regime_name_majoration_duree_assurance', period) + # return where( + # annee_de_liquidation == period.start.year, + # round_(duree_assurance_validee + majoration_duree_assurance), # On arrondi l'année de la liquidation + # duree_assurance_validee + # ) + + class liquidation_date(Variable): + value_type = date + entity = Individu + definition_period = ETERNITY + label = 'Date de liquidation' + default_value = date(2250, 12, 31) + + class majoration_pension(Variable): + value_type = int + entity = Individu + definition_period = MONTH + label = 'Majoration de pension' + + def formula(individu, period, parameters): + NotImplementedError + + class pension(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension' + + def formula(individu, period, parameters): + NotImplementedError + + class pension_brute(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension brute' + + def formula(individu, period, parameters): + NotImplementedError + + class pension_servie(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension servie' + + def formula(individu, period, parameters): + NotImplementedError + + +class AbstractRegimeEnAnnuites(AbstractRegime): + name = 'Régime en annuités' + variable_prefix = 'regime_en_annuites' + parameters = 'regime_en_annuites' + + class duree_assurance_annuelle(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = "Durée d'assurance (en trimestres validés l'année considérée)" + + class majoration_duree_assurance(Variable): + value_type = float + entity = Individu + definition_period = ETERNITY + label = "Majoration de durée d'assurance" + + def formula(individu, period): + return ( + individu('regime_name_majoration_duree_assurance_enfant', period) + + individu('regime_name_majoration_duree_assurance_autre', period) + ) + + class majoration_duree_assurance_autre(Variable): + value_type = float + entity = Individu + definition_period = ETERNITY + label = "Majoration de durée d'assurance autre que celle attribuée au motif des enfants" + + class majoration_pension(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Majoration de pension' + + class majoration_pension_au_31_decembre(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Majoration de pension au 31 décembre' + + def formula(individu, period, parameters): + annee_de_liquidation = individu('regime_name_liquidation_date', period).astype('datetime64[Y]').astype(int) + 1970 + # Raccouci pour arrêter les calculs dans le passé quand toutes les liquidations ont lieu dans le futur + if all(annee_de_liquidation > period.start.year): + return individu.empty_array() + last_year = period.last_year + majoration_pension_au_31_decembre_annee_precedente = individu('regime_name_majoration_pension_au_31_decembre', last_year) + revalorisation = parameters(period).regime_name.revalorisation_pension_au_31_decembre + majoration_pension = individu('regime_name_majoration_pension', period) + return revalorise( + majoration_pension_au_31_decembre_annee_precedente, + majoration_pension, + annee_de_liquidation, + revalorisation, + period, + ) + + class pension(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension' + + def formula(individu, period): + pension_brute = individu('regime_name_pension_brute', period) + majoration_pension = individu('regime_name_majoration_pension', period) + return pension_brute + majoration_pension + + class pension_au_31_decembre(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension' + + def formula(individu, period): + pension_brute_au_31_decembre = individu('regime_name_pension_brute_au_31_decembre', period) + majoration_pension_au_31_decembre = individu('regime_name_majoration_pension_au_31_decembre', period) + return pension_brute_au_31_decembre + majoration_pension_au_31_decembre + + class pension_brute_au_31_decembre(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension brute au 31 décembre' + + def formula(individu, period, parameters): + annee_de_liquidation = individu('regime_name_liquidation_date', period).astype('datetime64[Y]').astype(int) + 1970 + # Raccouci pour arrêter les calculs dans le passé quand toutes les liquidations ont lieu dans le futur + if all(period.start.year < annee_de_liquidation): + return individu.empty_array() + last_year = period.last_year + pension_brute_au_31_decembre_annee_precedente = individu('regime_name_pension_brute_au_31_decembre', last_year) + revalorisation = parameters(period).regime_name.revalorisation_pension_au_31_decembre + pension_brute = individu('regime_name_pension_brute', period) + return revalorise( + pension_brute_au_31_decembre_annee_precedente, + pension_brute, + annee_de_liquidation, + revalorisation, + period, + ) + + class pension_servie(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension servie' + + def formula(individu, period, parameters): + annee_de_liquidation = individu('regime_name_liquidation_date', period).astype('datetime64[Y]').astype(int) + 1970 + # Raccouci pour arrêter les calculs dans le passé quand toutes les liquidations ont lieu dans le futur + if all(annee_de_liquidation > period.start.year): + return individu.empty_array() + last_year = period.last_year + pension_au_31_decembre_annee_precedente = individu('regime_name_pension_au_31_decembre', last_year) + revalorisation = parameters(period).regime_name.revalarisation_pension_servie + pension = individu('regime_name_pension_au_31_decembre', period) + return revalorise( + pension_au_31_decembre_annee_precedente, + pension, + annee_de_liquidation, + revalorisation, + period, + ) + + class salaire_de_base(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Salaire de base (salaire brut)' + set_input = set_input_divide_by_period + + class salaire_de_reference(Variable): + value_type = float + entity = Individu + definition_period = ETERNITY + label = 'Salaire de référence' + + class taux_de_liquidation(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Taux de liquidation de la pension' + + def formula(individu, period, parameters): + decote = individu('regime_name_decote', period) + surcote = individu('regime_name_surcote', period) + taux_plein = parameters(period).regime_name.taux_plein.taux_plein + return taux_plein * (1 - decote + surcote) + + +# def revalorise(variable_31_decembre_annee_precedente, variable_originale, annee_de_liquidation, revalorisation, period): +# return select( +# [ +# annee_de_liquidation > period.start.year, +# annee_de_liquidation == period.start.year, +# annee_de_liquidation < period.start.year, +# ], +# [ +# 0, +# variable_originale, +# variable_31_decembre_annee_precedente * revalorisation +# ] +# ) diff --git a/openfisca_tunisia_pension/regimes/rsa.py b/openfisca_tunisia_pension/regimes/rsa.py new file mode 100644 index 0000000..c0b3b91 --- /dev/null +++ b/openfisca_tunisia_pension/regimes/rsa.py @@ -0,0 +1,88 @@ +'''Régime des salariés agricoles.''' + + +from openfisca_core.model_api import * +from openfisca_core import periods + +from openfisca_tunisia_pension.entities import Individu +from openfisca_tunisia_pension.regimes.regime import AbstractRegimeEnAnnuites +# from openfisca_tunisia_pension.tools import add_vectorial_timedelta, year_ + + +import functools +from numpy import ( + apply_along_axis, + logical_not as not_, + maximum as max_, + vstack, + ) + +from openfisca_tunisia_pension.variables.helpers import mean_over_k_largest, pension_generique + + +class RegimeRSA(AbstractRegimeEnAnnuites): + name = 'Régime des salariés agricoles' + variable_prefix = 'rsa' + parameters_prefix = 'rsa' + + class salaire_reference(Variable): + value_type = float + entity = Individu + label = 'Salaires de référence du régime des salariés agricoles' + definition_period = YEAR + + def formula(individu, period): + # TODO: gérer le nombre d'année + # TODO: plafonner les salaires à 2 fois le smag de l'année d'encaissement + base_declaration_rsa = 180 + base_liquidation_rsa = 300 + + n = 3 + mean_over_largest = functools.partial(mean_over_k_largest, k = n) + salaire = apply_along_axis( + mean_over_largest, + axis = 0, + arr = vstack([ + individu('salaire', period = periods.period('year', year)) + for year in range(period.start.year, period.start.year - n, -1) + ]), + ) + salaire_refererence = salaire * base_liquidation_rsa / base_declaration_rsa + return salaire_refererence + + + class pension(Variable): + value_type = float + entity = Individu + label = 'Salaires de référence du régime des salariés agricoles' + definition_period = YEAR + + def formula(individu, period, parameters): + rsa = parameters(period).retraite.regime_name + taux_annuite_base = rsa.taux_annuite_base + taux_annuite_supplementaire = rsa.taux_annuite_supplementaire + duree_stage = rsa.stage_requis + age_elig = rsa.age_legal + periode_remplacement_base = rsa.periode_remplacement_base + plaf_taux_pension = rsa.plaf_taux_pension + smag = parameters(period).marche_travail.smag * 25 + duree_stage_validee = duree_assurance > 4 * duree_stage + pension_min = rsa.pension_min + salaire_reference = individu('regime_name_salaire_reference', period) + + montant = pension_generique( + duree_assurance, + sal_ref, + taux_annuite_base, + taux_annuite_supplementaire, + duree_stage, + age_elig, + periode_remplacement_base, + plaf_taux_pension + ) + + elig_age = age > age_elig + elig = duree_stage_validee * elig_age * (salaire_reference > 0) + montant_percu = max_(montant, pension_min * smag) + pension = elig * montant_percu + return pension diff --git a/openfisca_tunisia_pension/regimes/rsna.py b/openfisca_tunisia_pension/regimes/rsna.py new file mode 100644 index 0000000..00bc078 --- /dev/null +++ b/openfisca_tunisia_pension/regimes/rsna.py @@ -0,0 +1,91 @@ +'''Régime des salariés non agricoles.''' + + +from openfisca_core.model_api import * + + +from openfisca_tunisia_pension.entities import Individu +from openfisca_tunisia_pension.regimes.regime import AbstractRegimeEnAnnuites +# from openfisca_tunisia_pension.tools import add_vectorial_timedelta, year_ + + +import functools +from numpy import ( + apply_along_axis, + logical_not as not_, + maximum as max_, + vstack, + ) + +from openfisca_tunisia_pension.variables.helpers import mean_over_k_largest, pension_generique + + +class RegimeRSNA(AbstractRegimeEnAnnuites): + name = 'Régime des salariés non agricoles' + variable_prefix = 'rsna' + parameters_prefix = 'rsna' + + class salaire_reference(Variable): + value_type = float + entity = Individu + label = 'Salaires de référence du régime des salariés non agricoles' + definition_period = YEAR + + def formula(individu, period): + # TODO: gérer le nombre d'année n + # TODO: plafonner les salaires à 6 fois le smig de l'année d'encaissement + n = 10 + mean_over_largest = functools.partial(mean_over_k_largest, k = n) + salaire_refererence = apply_along_axis( + mean_over_largest, + axis = 0, + arr = vstack([ + individu('salaire', period = year) + for year in range(period.start.year, period.start.year - n, -1) + ]), + ) + return salaire_refererence + + class pension(Variable): + value_type = float + entity = Individu + label = 'Pension des affiliés au régime des salariés non agricoles' + definition_period = YEAR + + def formula(individu, period, parameters): + duree_assurance = individu('regime_name_duree_assurance', period = period) + salaire_reference = individu('regime_name_salaire_reference', period = period) + age = individu('age', period = period) + + rsna = parameters(period).retraite.regime_name + taux_annuite_base = rsna.taux_annuite_base + taux_annuite_supplementaire = rsna.taux_annuite_supplementaire + duree_stage = rsna.stage_derog + age_eligible = rsna.age_dep_anticip + periode_remplacement_base = rsna.periode_remplacement_base + plaf_taux_pension = rsna.plaf_taux_pension + smig = parameters(period).marche_travail.smig_48h + + pension_min_sup = rsna.pension_minimale.sup + pension_min_inf = rsna.pension_minimale.inf + + stage = duree_assurance > 4 * duree_stage + pension_minimale = ( + stage * pension_min_sup + not_(stage) * pension_min_inf + ) + montant = pension_generique( + duree_assurance, + salaire_reference, + taux_annuite_base, + taux_annuite_supplementaire, + duree_stage, + age_eligible, + periode_remplacement_base, + plaf_taux_pension, + ) + # eligibilite + eligibilite_age = age > age_eligible + eligibilite = stage * eligibilite_age * (salaire_reference > 0) + # plafonnement + montant_pension_percu = max_(montant, pension_minimale * smig) + return eligibilite * montant_pension_percu diff --git a/openfisca_tunisia_pension/scripts_ast/__init__.py b/openfisca_tunisia_pension/scripts_ast/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openfisca_tunisia_pension/scripts_ast/script_ast.py b/openfisca_tunisia_pension/scripts_ast/script_ast.py new file mode 100644 index 0000000..843d3f9 --- /dev/null +++ b/openfisca_tunisia_pension/scripts_ast/script_ast.py @@ -0,0 +1,237 @@ +'''Generate openfisca variables of pension scheme.''' + +import ast +import copy +import logging +import os +import sys +from pathlib import Path + + +import openfisca_tunisia_pension +tunisia_pension_root = str(Path(openfisca_tunisia_pension.__file__).parent.parent) + + +log = logging.getLogger(__name__) + + +# tunisia_pension_root = pkg_resources.get_distribution('openfisca-tunisia-pension').location + +class RewriteRegimeFormula(ast.NodeTransformer): + parameters_prefix = None + variable_prefix = None + + def __init__(self, parameters_prefix, variable_prefix): + self.parameters_prefix = parameters_prefix + self.variable_prefix = variable_prefix + + def visit_Attribute(self, node): + node = self.generic_visit(node) + if type(node.attr) is str and node.attr.startswith('regime_name'): + new_attr = node.attr.replace('regime_name', self.parameters_prefix) + log.debug(f'Found attribute to replace: {node.attr} => {new_attr}') + node.attr = new_attr + return node + + def visit_Constant(self, node): + # Useless: node = self.generic_visit(node), because a constant has no + # sub-tree to visit. + if type(node.value) is str and node.value.startswith('regime_name'): + new_value = node.value.replace('regime_name', self.variable_prefix) + log.debug(f'Found parameter to replace: {node.value} => {new_value}') + node.value = new_value + return node + + +class RewriteRegimeVariableClass(ast.NodeTransformer): + parameters_prefix = None + variable_prefix = None + + def __init__(self, parameters_prefix, variable_prefix): + self.parameters_prefix = parameters_prefix + self.variable_prefix = variable_prefix + + def visit_ClassDef(self, node): + node.name = self.variable_prefix + '_' + node.name + node = self.generic_visit(node) + return node + + def visit_FunctionDef(self, node): + if node.name.startswith('formula'): + log.debug(f'Found formula: {node.name}') + node = RewriteRegimeFormula(self.parameters_prefix, self.variable_prefix).visit(node) + else: + node = self.generic_visit(node) + return node + + +def flatten_regime( + regime_class_node_by_name, + regime_class_node, + parameters_prefix, + variable_prefix, + existing_variables_name, + variables_node, + ): + new_variables_name = set() + for node in regime_class_node.body: + if type(node) is ast.ClassDef: + # Node is a variable. + if node.name not in existing_variables_name: + # Variable is not overriden by a subclass variable. + existing_variables_name.add(node.name) + new_variables_name.add(node.name) + + # Aplatit récursivement les variables la classe de base de cette classe Regime. + if regime_class_node.bases[0].id != 'object': + flatten_regime( + regime_class_node_by_name, + regime_class_node_by_name[regime_class_node.bases[0].id], + parameters_prefix, + variable_prefix, + existing_variables_name, + variables_node, + ) + + # Aplatit les variables de cette classe Regime. + for node in regime_class_node.body: + if type(node) is ast.ClassDef: + # Node is a variable. + if node.name in new_variables_name: + node = copy.deepcopy(node) + node = ast.fix_missing_locations(RewriteRegimeVariableClass( + parameters_prefix, + variable_prefix, + ).visit(node)) + variables_node.append(node) + + +def flatten_regimes(input_string, output_filename): + '''Creates regime variables. + + Args: + input_string (str): String to be processed by ast + output_filename (path): Destination of created variables code + ''' + # parser le texte du fichier en structure logique de type AST + input_node = ast.parse(input_string) + + # pour afficher un arbre AST faire une commande + # print(ast.dump(input_node, indent=4)) + + # créer un nouveau arbre AST vide qui va contenir le nouveau code + output_node = ast.Module(body=[], type_ignores=[]) + + # Crée un dictionnaire de toutes les classes contenant 'Regime' dans le nom. + regime_class_node_by_name = {} + for node in input_node.body: + if type(node) is ast.ClassDef and 'Regime' in node.name: + regime_class_node_by_name[node.name] = node + + # Recrée un AST en aplatissant les classes Regime + # (ie en en extrayant et renommant les variables OpenFisca). + variables_node = [] + for node in input_node.body: + if type(node) is ast.ClassDef and 'Regime' in node.name: + if 'Abstract' not in node.name: + parameters_prefix = get_regime_attribute(node, 'parameters_prefix') + variable_prefix = get_regime_attribute(node, 'variable_prefix') + flatten_regime( + regime_class_node_by_name, + node, + parameters_prefix, + variable_prefix, + set(), + variables_node, + ) + else: + output_node.body.append(node) + # Trie les variables OpenFisca par ordre alphabétique. + variables_node.sort(key = lambda variable_node: variable_node.name) + for variable_node in variables_node: + output_node.body.append(variable_node) + + # pour afficher le nouveau arbre AST faire une commande + # print(ast.dump(output_node, indent=4)) + + # convertir la structure logique de l'arbre AST en code python formatté (type string) + output_string = ast.unparse(output_node) + + # sauvegarder + with open(output_filename, 'w', encoding='utf-8') as file: + file.write(output_string) + + log.info(f'Result saved as {output_filename}') + + +def get_regime_attribute(regime_class_node, attribute): + '''Gets regime attribute. + + Args: + regime_class_node (ast.ClassDef): regime node + attribute (str): attribute name + + Returns: + [Any]: the attribute value + ''' + assert ( + isinstance(regime_class_node, ast.ClassDef) + and 'Regime' in regime_class_node.name + and 'Abstract' not in regime_class_node.name + ), f'{regime_class_node.name} is not a valid regime' + for sub_node in regime_class_node.body: + if isinstance(sub_node, ast.Assign): + for target in sub_node.targets: + if target.id == attribute: + assert isinstance(sub_node.value, ast.Constant), f'{sub_node.value} is not a constant' + return sub_node.value.value + + return None + + +def main(verbose = False): + regime_de_base = os.path.join( + tunisia_pension_root, + 'openfisca_tunisia_pension', + 'regimes', + 'regime.py' + ) + + regimes_files_by_name = { + name: { + 'input': os.path.join( + tunisia_pension_root, + 'openfisca_tunisia_pension', + 'regimes', + f'{name}.py' + ), + 'output': os.path.join( + tunisia_pension_root, + 'openfisca_tunisia_pension', + 'variables', + f'{name}.py' + ) + } + for name in [ + 'rsna', + # 'rsa', + # 'cnrps' + ] + } + + for _regime_name, regime_files in regimes_files_by_name.items(): + input_file_names = [ + regime_de_base, + regime_files['input'], + ] + input_string = '' + for input_filename in input_file_names: + with open(input_filename, encoding='utf-8') as file: + input_string += '\n' + file.read() + + logging.basicConfig(level = logging.DEBUG if verbose else logging.WARNING, stream = sys.stdout) + flatten_regimes(input_string, regime_files['output']) + + +if __name__ == '__main__': + main(verbose = True) diff --git a/openfisca_tunisia_pension/tunisia_pension_taxbenefitsystem.py b/openfisca_tunisia_pension/tunisia_pension_taxbenefitsystem.py deleted file mode 100644 index a844e21..0000000 --- a/openfisca_tunisia_pension/tunisia_pension_taxbenefitsystem.py +++ /dev/null @@ -1,25 +0,0 @@ -import glob -import os - -from openfisca_core.taxbenefitsystems import TaxBenefitSystem - -from openfisca_tunisia_pension import entities - -COUNTRY_DIR = os.path.dirname(os.path.abspath(__file__)) -EXTENSIONS_PATH = os.path.join(COUNTRY_DIR, 'extensions') -EXTENSIONS_DIRECTORIES = glob.glob(os.path.join(EXTENSIONS_PATH, '*/')) - - -class TunisiaPensionTaxBenefitSystem(TaxBenefitSystem): - '''Tunisian pensions tax benefit system''' - CURRENCY = 'DT' - - def __init__(self): - super(TunisiaPensionTaxBenefitSystem, self).__init__(entities.entities) - - # We add to our tax and benefit system all the variables - self.add_variables_from_directory(os.path.join(COUNTRY_DIR, 'variables')) - - # We add to our tax and benefit system all the legislation parameters defined in the parameters files - parameters_path = os.path.join(COUNTRY_DIR, 'parameters') - self.load_parameters(parameters_path) diff --git a/openfisca_tunisia_pension/variables/cnrps.py b/openfisca_tunisia_pension/variables/cnrps.py index 10fba9c..dec0cc0 100644 --- a/openfisca_tunisia_pension/variables/cnrps.py +++ b/openfisca_tunisia_pension/variables/cnrps.py @@ -40,7 +40,7 @@ class cnrps_pension(Variable): definition_period = YEAR def formula(individu, period, parameters): - trimestres_valides = individu('trimestres_valides', period = period) + duree_assurance = individu('duree_assurance', period = period) salaire_reference = individu('rsna_salaire_reference', period = period) age = individu('age', period = period) @@ -56,12 +56,12 @@ def formula(individu, period, parameters): pension_min_sup = rsna.pension_minimale.sup pension_min_inf = rsna.pension_minimale.inf - stage = trimestres_valides > 4 * duree_stage + stage = duree_assurance > 4 * duree_stage pension_minimale = ( stage * pension_min_sup + not_(stage) * pension_min_inf ) montant = pension_generique( - trimestres_valides, + duree_assurance, salaire_reference, taux_annuite_base, taux_annuite_supplementaire, diff --git a/openfisca_tunisia_pension/variables/data.py b/openfisca_tunisia_pension/variables/data.py index 51a2ebc..8b666e5 100644 --- a/openfisca_tunisia_pension/variables/data.py +++ b/openfisca_tunisia_pension/variables/data.py @@ -25,7 +25,7 @@ class date_naissance(Variable): definition_period = ETERNITY -class trimestres_valides(Variable): +class duree_assurance(Variable): value_type = int entity = Individu label = 'Nombre de trimestres validés' diff --git a/openfisca_tunisia_pension/variables/helpers.py b/openfisca_tunisia_pension/variables/helpers.py index 01cde80..6f45c3a 100644 --- a/openfisca_tunisia_pension/variables/helpers.py +++ b/openfisca_tunisia_pension/variables/helpers.py @@ -5,13 +5,13 @@ from numpy import minimum as min_ -def pension_generique(trimestres_valides, sal_ref, taux_annuite_base, taux_annuite_supplementaire, duree_stage, +def pension_generique(duree_assurance, sal_ref, taux_annuite_base, taux_annuite_supplementaire, duree_stage, age_elig, periode_remplacement_base, plaf_taux_pension): taux_pension = ( - (trimestres_valides < 4 * periode_remplacement_base) * (trimestres_valides / 4) * taux_annuite_base - + (trimestres_valides >= 4 * periode_remplacement_base) * ( + (duree_assurance < 4 * periode_remplacement_base) * (duree_assurance / 4) * taux_annuite_base + + (duree_assurance >= 4 * periode_remplacement_base) * ( taux_annuite_base * periode_remplacement_base - + (trimestres_valides / 4 - periode_remplacement_base) * taux_annuite_supplementaire + + (duree_assurance / 4 - periode_remplacement_base) * taux_annuite_supplementaire ) ) montant = min_(taux_pension, plaf_taux_pension) * sal_ref diff --git a/openfisca_tunisia_pension/variables/rsa.py b/openfisca_tunisia_pension/variables/rsa.py index e7a84f3..e7f6d26 100644 --- a/openfisca_tunisia_pension/variables/rsa.py +++ b/openfisca_tunisia_pension/variables/rsa.py @@ -51,12 +51,12 @@ def formula(individu, period, parameters): periode_remplacement_base = rsa.periode_remplacement_base plaf_taux_pension = rsa.plaf_taux_pension smag = parameters(period).marche_travail.smag * 25 - duree_stage_validee = trimestres_valides > 4 * duree_stage + duree_stage_validee = duree_assurance > 4 * duree_stage pension_min = rsa.pension_min salaire_reference = individu('salaire_reference_rsa', period) montant = pension_generique( - trimestres_valides, + duree_assurance, sal_ref, taux_annuite_base, taux_annuite_supplementaire, diff --git a/openfisca_tunisia_pension/variables/rsna.py b/openfisca_tunisia_pension/variables/rsna.py index de52705..9e01ac5 100644 --- a/openfisca_tunisia_pension/variables/rsna.py +++ b/openfisca_tunisia_pension/variables/rsna.py @@ -1,37 +1,78 @@ -import functools -from numpy import ( - apply_along_axis, - logical_not as not_, - maximum as max_, - vstack, - ) - +"""Abstract regimes definition.""" +from openfisca_core.model_api import * +from openfisca_tunisia_pension.entities import Individu +'Régime des salariés non agricoles.' from openfisca_core.model_api import * from openfisca_tunisia_pension.entities import Individu +from openfisca_tunisia_pension.regimes.regime import AbstractRegimeEnAnnuites +import functools +from numpy import apply_along_axis, logical_not as not_, maximum as max_, vstack from openfisca_tunisia_pension.variables.helpers import mean_over_k_largest, pension_generique +class rsna_cotisation(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'cotisation retraite employeur' -class rsna_salaire_reference(Variable): + def formula(individu, period, parameters): + NotImplementedError + +class rsna_duree_assurance(Variable): + value_type = int + entity = Individu + definition_period = YEAR + label = "Durée d'assurance (trimestres validés)" + +class rsna_duree_assurance_annuelle(Variable): value_type = float entity = Individu - label = 'Salaires de référence du régime des salariés non agricoles' definition_period = YEAR + label = "Durée d'assurance (en trimestres validés l'année considérée)" + +class rsna_liquidation_date(Variable): + value_type = date + entity = Individu + definition_period = ETERNITY + label = 'Date de liquidation' + default_value = date(2250, 12, 31) + +class rsna_majoration_duree_assurance(Variable): + value_type = float + entity = Individu + definition_period = ETERNITY + label = "Majoration de durée d'assurance" def formula(individu, period): - # TODO: gérer le nombre d'année n - # TODO: plafonner les salaires à 6 fois le smig de l'année d'encaissement - n = 10 - mean_over_largest = functools.partial(mean_over_k_largest, k = n) - salaire_refererence = apply_along_axis( - mean_over_largest, - axis = 0, - arr = vstack([ - individu('salaire', period = year) - for year in range(period.start.year, period.start.year - n, -1) - ]), - ) - return salaire_refererence + return individu('rsna_majoration_duree_assurance_enfant', period) + individu('rsna_majoration_duree_assurance_autre', period) + +class rsna_majoration_duree_assurance_autre(Variable): + value_type = float + entity = Individu + definition_period = ETERNITY + label = "Majoration de durée d'assurance autre que celle attribuée au motif des enfants" +class rsna_majoration_pension(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Majoration de pension' + +class rsna_majoration_pension_au_31_decembre(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Majoration de pension au 31 décembre' + + def formula(individu, period, parameters): + annee_de_liquidation = individu('rsna_liquidation_date', period).astype('datetime64[Y]').astype(int) + 1970 + if all(annee_de_liquidation > period.start.year): + return individu.empty_array() + last_year = period.last_year + majoration_pension_au_31_decembre_annee_precedente = individu('rsna_majoration_pension_au_31_decembre', last_year) + revalorisation = parameters(period).rsna.revalorisation_pension_au_31_decembre + majoration_pension = individu('rsna_majoration_pension', period) + return revalorise(majoration_pension_au_31_decembre_annee_precedente, majoration_pension, annee_de_liquidation, revalorisation, period) class rsna_pension(Variable): value_type = float @@ -40,10 +81,9 @@ class rsna_pension(Variable): definition_period = YEAR def formula(individu, period, parameters): - trimestres_valides = individu('trimestres_valides', period = period) - salaire_reference = individu('rsna_salaire_reference', period = period) - age = individu('age', period = period) - + duree_assurance = individu('rsna_duree_assurance', period=period) + salaire_reference = individu('rsna_salaire_reference', period=period) + age = individu('age', period=period) rsna = parameters(period).retraite.rsna taux_annuite_base = rsna.taux_annuite_base taux_annuite_supplementaire = rsna.taux_annuite_supplementaire @@ -52,27 +92,101 @@ def formula(individu, period, parameters): periode_remplacement_base = rsna.periode_remplacement_base plaf_taux_pension = rsna.plaf_taux_pension smig = parameters(period).marche_travail.smig_48h - pension_min_sup = rsna.pension_minimale.sup pension_min_inf = rsna.pension_minimale.inf - - stage = trimestres_valides > 4 * duree_stage - pension_minimale = ( - stage * pension_min_sup + not_(stage) * pension_min_inf - ) - montant = pension_generique( - trimestres_valides, - salaire_reference, - taux_annuite_base, - taux_annuite_supplementaire, - duree_stage, - age_eligible, - periode_remplacement_base, - plaf_taux_pension, - ) - # eligibilite + stage = duree_assurance > 4 * duree_stage + pension_minimale = stage * pension_min_sup + not_(stage) * pension_min_inf + montant = pension_generique(duree_assurance, salaire_reference, taux_annuite_base, taux_annuite_supplementaire, duree_stage, age_eligible, periode_remplacement_base, plaf_taux_pension) eligibilite_age = age > age_eligible eligibilite = stage * eligibilite_age * (salaire_reference > 0) - # plafonnement montant_pension_percu = max_(montant, pension_minimale * smig) return eligibilite * montant_pension_percu + +class rsna_pension_au_31_decembre(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension' + + def formula(individu, period): + pension_brute_au_31_decembre = individu('rsna_pension_brute_au_31_decembre', period) + majoration_pension_au_31_decembre = individu('rsna_majoration_pension_au_31_decembre', period) + return pension_brute_au_31_decembre + majoration_pension_au_31_decembre + +class rsna_pension_brute(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension brute' + + def formula(individu, period, parameters): + NotImplementedError + +class rsna_pension_brute_au_31_decembre(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension brute au 31 décembre' + + def formula(individu, period, parameters): + annee_de_liquidation = individu('rsna_liquidation_date', period).astype('datetime64[Y]').astype(int) + 1970 + if all(period.start.year < annee_de_liquidation): + return individu.empty_array() + last_year = period.last_year + pension_brute_au_31_decembre_annee_precedente = individu('rsna_pension_brute_au_31_decembre', last_year) + revalorisation = parameters(period).rsna.revalorisation_pension_au_31_decembre + pension_brute = individu('rsna_pension_brute', period) + return revalorise(pension_brute_au_31_decembre_annee_precedente, pension_brute, annee_de_liquidation, revalorisation, period) + +class rsna_pension_servie(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Pension servie' + + def formula(individu, period, parameters): + annee_de_liquidation = individu('rsna_liquidation_date', period).astype('datetime64[Y]').astype(int) + 1970 + if all(annee_de_liquidation > period.start.year): + return individu.empty_array() + last_year = period.last_year + pension_au_31_decembre_annee_precedente = individu('rsna_pension_au_31_decembre', last_year) + revalorisation = parameters(period).rsna.revalarisation_pension_servie + pension = individu('rsna_pension_au_31_decembre', period) + return revalorise(pension_au_31_decembre_annee_precedente, pension, annee_de_liquidation, revalorisation, period) + +class rsna_salaire_de_base(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Salaire de base (salaire brut)' + set_input = set_input_divide_by_period + +class rsna_salaire_de_reference(Variable): + value_type = float + entity = Individu + definition_period = ETERNITY + label = 'Salaire de référence' + +class rsna_salaire_reference(Variable): + value_type = float + entity = Individu + label = 'Salaires de référence du régime des salariés non agricoles' + definition_period = YEAR + + def formula(individu, period): + n = 10 + mean_over_largest = functools.partial(mean_over_k_largest, k=n) + salaire_refererence = apply_along_axis(mean_over_largest, axis=0, arr=vstack([individu('salaire', period=year) for year in range(period.start.year, period.start.year - n, -1)])) + return salaire_refererence + +class rsna_taux_de_liquidation(Variable): + value_type = float + entity = Individu + definition_period = YEAR + label = 'Taux de liquidation de la pension' + + def formula(individu, period, parameters): + decote = individu('rsna_decote', period) + surcote = individu('rsna_surcote', period) + taux_plein = parameters(period).rsna.taux_plein.taux_plein + return taux_plein * (1 - decote + surcote) \ No newline at end of file diff --git a/tests/formulas/pension_rsna.yaml b/tests/formulas/rsna_pension.yaml similarity index 94% rename from tests/formulas/pension_rsna.yaml rename to tests/formulas/rsna_pension.yaml index d8791e1..4ac5083 100644 --- a/tests/formulas/pension_rsna.yaml +++ b/tests/formulas/rsna_pension.yaml @@ -3,7 +3,7 @@ absolute_error_margin: 0.5 input: age: 60 - trimestres_valides: 50 + rsna_duree_assurance: 50 salaire: 1975: 12000 1976: 12000 @@ -46,6 +46,5 @@ 2013: 12000 2014: 12000 output: - trimestres_valides: 50 rsna_salaire_reference: 12000 rsna_pension: 5400 diff --git a/tests/formulas/salaire_reference_rsna.yaml b/tests/formulas/rsna_salaire_reference.yaml similarity index 94% rename from tests/formulas/salaire_reference_rsna.yaml rename to tests/formulas/rsna_salaire_reference.yaml index 852a402..f565e77 100644 --- a/tests/formulas/salaire_reference_rsna.yaml +++ b/tests/formulas/rsna_salaire_reference.yaml @@ -3,7 +3,7 @@ absolute_error_margin: 0.5 input: age: 60 - trimestres_valides: 50 + rsna_duree_assurance: 50 salaire: 1975: 12000 1976: 12000 @@ -46,5 +46,4 @@ 2013: 12000 2014: 12000 output: - trimestres_valides: 50 rsna_salaire_reference: 12000