Commit fb3e098f authored by Christophe Benz's avatar Christophe Benz

Refactor CLI and doc

parent d856d9ca
...@@ -29,84 +29,145 @@ python3 calculette_impots/scripts/json_ast_to_python.py /path/to/calculette-impo ...@@ -29,84 +29,145 @@ python3 calculette_impots/scripts/json_ast_to_python.py /path/to/calculette-impo
## Utilisation ## Utilisation
Ce paquet installe une commande `calculette-impots`. Ce paquet installe une commande `calculette-impots` dans votre répertoire utilisateur.
Cela signifie que vous pouvez la taper dans un terminal et elle sera reconnue, sauf si elle a été installée
Sans argument, `calculette-impots` calcule toutes les variables restituée, en dehors du "PATH".
Cette commande accepte des sous-commandes. Voyons son utilisation :
```
$ calculette-impots
Usage:
calculette-impots calculate [--no-verifs] VARIABLE...
calculette-impots info VARIABLE...
calculette-impots (-h | --help)
calculette-impots --version
```
### La sous-commande `info`
La sous-commande `info` permet d'afficher des informations sur les variables, en particulier leur type,
quelques attributs, leurs éventuelles dépendances et dépendances inverses.
Exemple :
```
$ $ calculette-impots info TSHALLOV
{
"TSHALLOV": {
"variable_definition": {
"alias": "1AJ",
"attributes": {
"acompte": 1,
"avfisc": 0,
"categorie_TL": 20,
"classe": 0,
"cotsoc": 5,
"ind_abat": 1,
"nat_code": 0,
"priorite": 10,
"rapcat": 4,
"sanction": 8
},
"description": "Salaires - Declarant 1",
"name": "TSHALLOV",
"restituee": true,
"subtype": "revenu",
"tgvh_linecol": [
13637,
1
],
"type": "variable_saisie"
},
"variable_reverse_dependencies": [
"ABTS1AJ",
"INDPPEV",
"INDREV1A8IR",
[...]
]
}
}
```
Certains bouts ont été coupés pour ne pas polluer le README.
Ceci était une variable de saisie mais on peut également obtenir des infos les variables calculées.
### La sous-commande `calculate`
Sans argument, la sous-commande `calculate` calcule toutes les variables restituées,
sans remplir aucune case de la déclaration. Cela équivaut à un célibataire sans enfants et sans revenu. sans remplir aucune case de la déclaration. Cela équivaut à un célibataire sans enfants et sans revenu.
Il faut tout de même préciser une variable de saisie spéciale (`V_ANREV` pour "Annee des revenus") pour laquelle Il faut tout de même préciser une variable de saisie spéciale (`V_ANREV` pour "Annee des revenus") pour laquelle
on donne la valeur `2014` car ici on calcule les impôts de 2015 sur les revenus de 2014. on donne la valeur `2014` car ici on calcule les impôts de 2015 sur les revenus de 2014.
Si on ne le fait pas un "warning" nous le rappelle.
De plus, pour l'instant, on peut passer une autre option (`--no-verifs`) à la commande `calculette-impots` De plus, pour l'instant, on peut passer une autre option (`--no-verifs`) à la commande `calculette-impots`
pour ignorer les "vérifs", qui sont des tests validant la cohérence des calculs. pour ignorer les "vérifs", qui sont des tests validant la cohérence des calculs.
``` ```
$ calculette-impots --saisies V_ANREV=2014 --no-verifs $ calculette-impots calculate V_ANREV=2014 --no-verifs
SEUILCIIMSI = 13956 (donnee equipe batch pour CNTDF : seuil imposition tx normal CSG) {
SEUILCIRIRF = 10676 (donnee equipe batch pour CNTDF : seuil imposition CSG CRDS) "calculate_results": {
PPENBJ = 360 (PPE:NOMBRE DE JOURS DE LA PERIODE) "ABSPE": 9,
TXTO = 80 (TAUX TO RESTITUE) "APPLI_BATCH": 1,
TXREGV = 80 (TAUX interets de retard + majo Regul assurance vie - 'avis) "CIIMSI": 2,
ABSPE = 9 (indicateur abattements speciaux personnes agees) "CIRIRF": 2,
INDGARD = 9 (Indicateur de plafonnement de frais de garde (AGARD)) [...]
MESGOUV2 = 6 (Indicateur beneficiaire mesures fiscales bas de bareme (simulateur)) }
MESGOUV = 3 (Indicateur beneficiaire des mesures fiscales de bas de bareme) }
INDCEX = 3 (Indicateur Brav pour restit non imp presqu imposable et autre )
[...]
``` ```
Les résultats sont triés par ordre décroissant sur les valeurs. Le JSON de sortie est coupé ici pour ne pas surcharger le README.
La sortie est coupée pour ne pas surcharger le README. Pour info on peut utiliser un outil comme [`jq`](https://stedolan.github.io/jq/) pour le manipuler.
On peut changer le format de sortie en précision l'option `--output-format`, par exemple en JSON comme ceci :
``` Ceci était une simulation à vide, maintenant il serait intéressant de remplir les cases de la déclaration d'impôts.
$ calculette-impots --saisies V_ANREV=2014 --output-format=json --no-verifs
{"results": {"ABSPE": 9, "APPLI_BATCH": 1, "CIIMSI": 2, "CIRIRF": 2, "IND12": 3, "IND61IR": 3, "IND61PS": 3, "INDCEX": 3, "INDEFTS1": 1, "INDEFTS2": 1, "INDEFTS3": 1, "INDEFTS4": 1, "INDEFTSC": 1, "INDEFTSV": 1, "INDEXOGEN": 1, "INDGARD": 9, "INDIRPS": 1, "LIG1": 1, "LIG10": 1, "LIG10C": 1, "LIG10P": 1, "LIG10V": 1, "LIG2": 1, "LIG2052": 1, "LIG2501": 1, "LIGLOCSEUL": 1, "LIGNEMP": 1, "MESGOUV": 3, "MESGOUV2": 6, "NATMAJ": 1, "NATMAJC": 1, "NATMAJCDIS": 1, "NATMAJCVN": 1, "NATMAJGLOA": 1, "NATMAJP": 1, "NATMAJR": 1, "NATMAJREGV": 1, "NATMAJRSE1": 1, "NATMAJRSE2": 1, "NATMAJRSE3": 1, "NATMAJRSE4": 1, "NATMAJRSE5": 1, "NBFOTH": 1, "NBPT": 1.0, "NONLIGPS": 1, "PPENBJ": 360, "SEUILCIIMSI": 13956, "SEUILCIRIRF": 10676, "TXREGV": 80, "TXTO": 80}}
```
Le JSON résultant étant très compact on peut utiliser un outil comme [`jq`](https://stedolan.github.io/jq/) ## Cas-types
pour le manipuler.
> À noter qu'avec la sortie au format JSON les résultats ne sont pas triés par valeur
> mais les clés du JSON sont triées par ordre alphabétique.
On peut demander à calculer seulement une ou plusieurs variables en particulier : Tout d'abord il faut savoir qu'on peut demander à calculer une ou plusieurs variables en particulier.
Pour cela il suffit d'ajouter le nom des variables mais sans le signe "=".
``` ```
$ calculette-impots --saisies V_ANREV=2014 --calculees PPENBJ --no-verifs $ calculette-impots calculate V_ANREV=2014 IRN IDRS2 --no-verifs
PPENBJ = 360 (PPE:NOMBRE DE JOURS DE LA PERIODE) {
"calculate_results": {
$ calculette-impots --saisies V_ANREV=2014 --calculees PPENBJ LIGNEMP --no-verifs "IDRS2": 0,
PPENBJ = 360 (PPE:NOMBRE DE JOURS DE LA PERIODE) "IRN": 0
LIGNEMP = 1 (Indicateur ligne impot net) }
}
``` ```
## Cas-types Il faut aussi savoir que la variable `IRN` correspond (grosso-modo) à l'impôt total. C'est pour cela qu'on la demande.
Testons d'autres cas-types. Testons à présent quelques cas-types.
- Un célibataire sans enfants gagnant 10000€ par an - Un célibataire sans enfants gagnant 10000€ par an
``` ```
$ calculette-impots --saisies V_ANREV=2014 TSHALLOV=10000 --no-verifs --calculees IRN $ calculette-impots calculate V_ANREV=2014 TSHALLOV=10000 IRN --no-verifs
IRN = 0 (Impot net ou restitution nette) {
"calculate_results": {
"IRN": 0
}
}
``` ```
- Un célibataire sans enfants gagnant 30000€ par an - Un célibataire sans enfants gagnant 30000€ par an
``` ```
$ calculette-impots --saisies V_ANREV=2014 TSHALLOV=30000 --no-verifs --calculees IRN $ calculette-impots calculate V_ANREV=2014 TSHALLOV=30000 IRN --no-verifs
IRN = 2461 (Impot net ou restitution nette) {
``` "calculate_results": {
- Un couple non marié sans enfants dont le déclarant 1 gagne 10000€ par an et le déclarant 2 gagne 20000€ par an "IRN": 2461
``` }
$ calculette-impots --saisies V_ANREV=2014 TSHALLOV=10000 TSHALLOC=20000 --no-verifs --calculees IRN }
IRN = 2461 (Impot net ou restitution nette) ```
``` - Un couple marié (date du mariage 05/05/1980) sans enfants dont le déclarant 1 gagne 10000€ par an et le déclarant 2 gagne 20000€ par an
> Note: ce couple est censé remplir deux déclarations, la situation simulée ici n'est pas correcte et c'est la désactivation des vérifications qui rend possible ce calcul. ```
$ calculette-impots calculate V_ANREV=2014 TSHALLOV=10000 TSHALLOC=20000 V_0AM=1 V_0AX=05051980 IRN --no-verifs
- Un couple marié (date du mariage `05/05/1980`) sans enfants dont le déclarant 1 gagne 10000€ par an et le déclarant 2 gagne 20000€ par an {
``` "calculate_results": {
$ calculette-impots --saisies V_ANREV=2014 TSHALLOV=10000 TSHALLOC=20000 V_0AM=1 V_0AX=05051980 --no-verifs --calculees IRN "IRN": 264
IRN = 264 (Impot net ou restitution nette) }
}
``` ```
## Simulateur en ligne ## Simulateur en ligne
......
...@@ -2,78 +2,54 @@ ...@@ -2,78 +2,54 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""\
Calculette Impôts
Usage:
calculette-impots calculate [--no-verifs] VARIABLE...
calculette-impots info VARIABLE...
calculette-impots (-h | --help)
calculette-impots --version
"""
from collections import defaultdict from collections import defaultdict
import argparse
import json import json
import logging import pkg_resources
import os
import sys import sys
from toolz.curried import get, map, unique, valfilter from docopt import docopt
from toolz.curried import unique, valfilter
from calculette_impots import loaders from calculette_impots import loaders
from calculette_impots.generated import formulas, verifs from calculette_impots.generated import formulas, verifs
from calculette_impots.variables_definitions import VariablesDefinitions from calculette_impots.variables_definitions import VariablesDefinitions
args = None arguments = None
script_name = os.path.splitext(os.path.basename(__file__))[0]
log = logging.getLogger(script_name)
constants = loaders.load_constants() constants = loaders.load_constants()
dependencies_by_formula_name = loaders.load_formulas_dependencies()
variables_definitions = VariablesDefinitions(constants=constants) variables_definitions = VariablesDefinitions(constants=constants)
def iter_saisie_variables(values): def calculate(calculee_variable_names=[], saisie_variables={}):
global variables_definitions global arguments
for value, words in map(lambda value: (value, value.strip('=').split('=', 1)), values):
if len(words) == 1:
parser.error('Missing value for variable saisie: "{}"'.format(value))
elif len(words) > 2:
parser.error('Invalid syntax for variable saisie: "{}"'.format(value))
variable_name, variable_value = words
try:
variable_value = float(variable_value)
except ValueError:
parser.error('Variable "{}" value is not a float'.format(value))
if not variables_definitions.is_saisie(variable_name):
parser.error('Variable "{}" is not a variable saisie'.format(variable_name))
yield variable_name, variable_value
def main():
global args, constants, parser, variables_definitions
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Increase output verbosity')
parser.add_argument('-d', '--debug', action='store_true', default=False, help='Display debug messages')
parser.add_argument('--calculees', dest='calculee_variables', metavar='variable', nargs='+',
help='Variables calculées')
parser.add_argument('--saisies', dest='saisie_variables', metavar='nom=valeur', nargs='+', help='Variables saisies')
parser.add_argument('--no-verifs', action='store_true', default=False, help='Skip verifs step')
parser.add_argument('--output-format', choices=['text', 'json'], default='text', help='Output format')
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.debug else (logging.INFO if args.verbose else logging.WARNING),
stream=sys.stdout,
)
warning_messages_by_section = defaultdict(list) warning_messages_by_section = defaultdict(list)
if args.calculee_variables is None: nb_calculee_variable_names_requested = len(calculee_variable_names)
if not calculee_variable_names:
calculee_variable_names = variables_definitions.filter_by_subtype('restituee') calculee_variable_names = variables_definitions.filter_by_subtype('restituee')
else: else:
calculee_variable_names = args.calculee_variables
for calculee_variable_name in calculee_variable_names: for calculee_variable_name in calculee_variable_names:
if not variables_definitions.has_subtype(calculee_variable_name, 'restituee'): if not variables_definitions.has_subtype(calculee_variable_name, 'restituee'):
warning_messages_by_section['saisies'].append( warning_messages_by_section['saisies'].append(
'Variable "{}" is not a variable of type "calculee restituee"'.format(calculee_variable_name) 'Variable "{}" is not a variable of type "calculee restituee"'.format(calculee_variable_name)
) )
saisie_variables = {} if args.saisie_variables is None else dict(iter_saisie_variables(args.saisie_variables))
if 'V_ANREV' not in saisie_variables: if 'V_ANREV' not in saisie_variables:
warning_messages_by_section['saisies'].append( warning_messages_by_section['saisies'].append(
'V_ANREV should be given as a "saisie" variable. Hint: --saisie V_ANREV=2014' 'V_ANREV should be given as a "saisie" variable. Hint: V_ANREV=2014'
) )
result_by_formula_name_cache = {} result_by_formula_name_cache = {}
...@@ -83,7 +59,7 @@ def main(): ...@@ -83,7 +59,7 @@ def main():
saisie_variables=saisie_variables, saisie_variables=saisie_variables,
) )
if not args.no_verifs: if not arguments['--no-verifs']:
errors = verifs.get_errors( errors = verifs.get_errors(
formulas=formulas_functions, formulas=formulas_functions,
saisie_variables=saisie_variables, saisie_variables=saisie_variables,
...@@ -95,37 +71,95 @@ def main(): ...@@ -95,37 +71,95 @@ def main():
for error in unique(errors) # Keep order for error in unique(errors) # Keep order
] ]
results = {} results = {
for calculee_variable_name in calculee_variable_names: calculee_variable_name: formulas_functions[calculee_variable_name]()
try: for calculee_variable_name in calculee_variable_names
result = formulas_functions[calculee_variable_name]() }
except: if nb_calculee_variable_names_requested == 0:
log.exception('Error while calculating {}'.format(calculee_variable_name))
else:
results[calculee_variable_name] = result
if args.calculee_variables is None:
results = valfilter(lambda val: val > 0, results) results = valfilter(lambda val: val > 0, results)
if args.output_format == 'text': dump(
for section, warning_messages in warning_messages_by_section.items(): valfilter(
for warning_message in warning_messages: lambda val: val is not None,
log.warning('{}:{}'.format(section, warning_message)) {
for calculee_variable_name, result in reversed(sorted(results.items(), key=get(1))): 'calculate_results': results,
print('{} = {} ({})'.format(calculee_variable_name, result, 'warnings': warning_messages_by_section or None,
variables_definitions.get_description(calculee_variable_name))) },
elif args.output_format == 'json': ),
print(json.dumps( )
valfilter(
lambda val: val is not None,
{ def dump(value):
'calculate_results': results, print(json.dumps(value, indent=2, sort_keys=True))
'warnings': warning_messages_by_section,
},
), def get_variable_info(variable_name):
sort_keys=True, variable_definition = variables_definitions.definition_by_variable_name.get(variable_name)
)) if variable_definition is None:
else: exit('The variable {!r} is not defined.'.format(variable_name))
raise NotImplementedError() variable_dependencies = dependencies_by_formula_name.get(variable_name)
if variable_dependencies is not None:
variable_dependencies = sorted(variable_dependencies)
variable_reverse_dependencies = sorted(valfilter(
lambda val: variable_name in val,
dependencies_by_formula_name,
).keys()) or None
return valfilter(
lambda val: val is not None,
{
'variable_definition': variable_definition,
'variable_dependencies': variable_dependencies,
'variable_reverse_dependencies': variable_reverse_dependencies,
},
)
def info(variable_names):
dump({
variable_name: get_variable_info(variable_name)
for variable_name in variable_names
})
def iter_calculate_variables(variables):
for variable_str in variables:
if '=' in variable_str:
variable_name, variable_value = variable_str.strip('=').split('=', 1)
if not variables_definitions.is_saisie(variable_name):
exit('Variable "{}" is not a "saisie" variable'.format(variable_name))
try:
variable_value = float(variable_value)
except ValueError:
exit('In "{}={}" value is not a float'.format(variable_name, variable_value))
yield 'variable_saisie', (variable_name, variable_value)
else:
if not variables_definitions.is_calculee(variable_str):
exit('Variable "{}" is not a "calculee" variable'.format(variable_str))
yield 'variable_calculee', variable_str
def main():
global arguments
package_name = 'calculette_impots'
version = pkg_resources.require(package_name)[0].version
arguments = docopt(__doc__, version='{} {}'.format(package_name, version))
if arguments['calculate']:
variables = list(iter_calculate_variables(arguments['VARIABLE']))
calculate(
calculee_variable_names=[
variable_name
for variable_type, variable_name in variables
if variable_type == 'variable_calculee'
],
saisie_variables=dict([
pair
for variable_type, pair in variables
if variable_type == 'variable_saisie'
]),
)
elif arguments['info']:
info(variable_names=arguments['VARIABLE'])
return 0 return 0
......
...@@ -24,10 +24,11 @@ setup( ...@@ -24,10 +24,11 @@ setup(
url='https://git.framasoft.org/openfisca/calculette-impots-python', url='https://git.framasoft.org/openfisca/calculette-impots-python',
entry_points={ entry_points={
'console_scripts': ['calculette-impots=calculette_impots.scripts.calculate:main'], 'console_scripts': ['calculette-impots=calculette_impots.scripts.command_line:main'],
}, },
install_requires=[ install_requires=[
'calculette_impots_m_language_parser >= 0.0.0.dev0', 'calculette_impots_m_language_parser >= 0.0.0.dev0',
'docopt',
'toolz >= 0.7.4', 'toolz >= 0.7.4',
], ],
packages=find_packages(), packages=find_packages(),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment