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
## Utilisation
Ce paquet installe une commande `calculette-impots`.
Sans argument, `calculette-impots` calcule toutes les variables restituée,
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
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.
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.
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`
pour ignorer les "vérifs", qui sont des tests validant la cohérence des calculs.
```
$ calculette-impots --saisies 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)
PPENBJ = 360 (PPE:NOMBRE DE JOURS DE LA PERIODE)
TXTO = 80 (TAUX TO RESTITUE)
TXREGV = 80 (TAUX interets de retard + majo Regul assurance vie - 'avis)
ABSPE = 9 (indicateur abattements speciaux personnes agees)
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 )
[...]
$ calculette-impots calculate V_ANREV=2014 --no-verifs
{
"calculate_results": {
"ABSPE": 9,
"APPLI_BATCH": 1,
"CIIMSI": 2,
"CIRIRF": 2,
[...]
}
}
```
Les résultats sont triés par ordre décroissant sur les valeurs.
La sortie est coupée pour ne pas surcharger le README.
On peut changer le format de sortie en précision l'option `--output-format`, par exemple en JSON comme ceci :
Le JSON de sortie est coupé ici pour ne pas surcharger le README.
Pour info on peut utiliser un outil comme [`jq`](https://stedolan.github.io/jq/) pour le manipuler.
```
$ 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}}
```
Ceci était une simulation à vide, maintenant il serait intéressant de remplir les cases de la déclaration d'impôts.
Le JSON résultant étant très compact on peut utiliser un outil comme [`jq`](https://stedolan.github.io/jq/)
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.
## Cas-types
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
PPENBJ = 360 (PPE:NOMBRE DE JOURS DE LA PERIODE)
$ calculette-impots --saisies V_ANREV=2014 --calculees PPENBJ LIGNEMP --no-verifs
PPENBJ = 360 (PPE:NOMBRE DE JOURS DE LA PERIODE)
LIGNEMP = 1 (Indicateur ligne impot net)
$ calculette-impots calculate V_ANREV=2014 IRN IDRS2 --no-verifs
{
"calculate_results": {
"IDRS2": 0,
"IRN": 0
}
}
```
## 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
```
$ calculette-impots --saisies V_ANREV=2014 TSHALLOV=10000 --no-verifs --calculees IRN
IRN = 0 (Impot net ou restitution nette)
$ calculette-impots calculate V_ANREV=2014 TSHALLOV=10000 IRN --no-verifs
{
"calculate_results": {
"IRN": 0
}
}
```
- Un célibataire sans enfants gagnant 30000€ par an
```
$ calculette-impots --saisies V_ANREV=2014 TSHALLOV=30000 --no-verifs --calculees IRN
IRN = 2461 (Impot net ou restitution nette)
```
- Un couple non marié sans enfants dont le déclarant 1 gagne 10000€ par an et le déclarant 2 gagne 20000€ par an
```
$ calculette-impots --saisies V_ANREV=2014 TSHALLOV=10000 TSHALLOC=20000 --no-verifs --calculees IRN
IRN = 2461 (Impot net ou restitution nette)
```
> 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.
- 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
```
$ calculette-impots --saisies V_ANREV=2014 TSHALLOV=10000 TSHALLOC=20000 V_0AM=1 V_0AX=05051980 --no-verifs --calculees IRN
IRN = 264 (Impot net ou restitution nette)
$ calculette-impots calculate V_ANREV=2014 TSHALLOV=30000 IRN --no-verifs
{
"calculate_results": {
"IRN": 2461
}
}
```
- 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
```
$ calculette-impots calculate V_ANREV=2014 TSHALLOV=10000 TSHALLOC=20000 V_0AM=1 V_0AX=05051980 IRN --no-verifs
{
"calculate_results": {
"IRN": 264
}
}
```
## Simulateur en ligne
......
......@@ -2,78 +2,54 @@
# -*- 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
import argparse
import json
import logging
import os
import pkg_resources
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.generated import formulas, verifs
from calculette_impots.variables_definitions import VariablesDefinitions
args = None
script_name = os.path.splitext(os.path.basename(__file__))[0]
log = logging.getLogger(script_name)
arguments = None
constants = loaders.load_constants()
dependencies_by_formula_name = loaders.load_formulas_dependencies()
variables_definitions = VariablesDefinitions(constants=constants)
def iter_saisie_variables(values):
global variables_definitions
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,
)
def calculate(calculee_variable_names=[], saisie_variables={}):
global arguments
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')
else:
calculee_variable_names = args.calculee_variables
for calculee_variable_name in calculee_variable_names:
if not variables_definitions.has_subtype(calculee_variable_name, 'restituee'):
warning_messages_by_section['saisies'].append(
'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:
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 = {}
......@@ -83,7 +59,7 @@ def main():
saisie_variables=saisie_variables,
)
if not args.no_verifs:
if not arguments['--no-verifs']:
errors = verifs.get_errors(
formulas=formulas_functions,
saisie_variables=saisie_variables,
......@@ -95,37 +71,95 @@ def main():
for error in unique(errors) # Keep order
]
results = {}
for calculee_variable_name in calculee_variable_names:
try:
result = formulas_functions[calculee_variable_name]()
except:
log.exception('Error while calculating {}'.format(calculee_variable_name))
else:
results[calculee_variable_name] = result
if args.calculee_variables is None:
results = {
calculee_variable_name: formulas_functions[calculee_variable_name]()
for calculee_variable_name in calculee_variable_names
}
if nb_calculee_variable_names_requested == 0:
results = valfilter(lambda val: val > 0, results)
if args.output_format == 'text':
for section, warning_messages in warning_messages_by_section.items():
for warning_message in warning_messages:
log.warning('{}:{}'.format(section, warning_message))
for calculee_variable_name, result in reversed(sorted(results.items(), key=get(1))):
print('{} = {} ({})'.format(calculee_variable_name, result,
variables_definitions.get_description(calculee_variable_name)))
elif args.output_format == 'json':
print(json.dumps(
dump(
valfilter(
lambda val: val is not None,
{
'calculate_results': results,
'warnings': warning_messages_by_section,
'warnings': warning_messages_by_section or None,
},
),
sort_keys=True,
))
)
def dump(value):
print(json.dumps(value, indent=2, sort_keys=True))
def get_variable_info(variable_name):
variable_definition = variables_definitions.definition_by_variable_name.get(variable_name)
if variable_definition is None:
exit('The variable {!r} is not defined.'.format(variable_name))
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:
raise NotImplementedError()
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
......
......@@ -24,10 +24,11 @@ setup(
url='https://git.framasoft.org/openfisca/calculette-impots-python',
entry_points={
'console_scripts': ['calculette-impots=calculette_impots.scripts.calculate:main'],
'console_scripts': ['calculette-impots=calculette_impots.scripts.command_line:main'],
},
install_requires=[
'calculette_impots_m_language_parser >= 0.0.0.dev0',
'docopt',
'toolz >= 0.7.4',
],
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