Merge pull request #51 from sgmap/versement-transport-local

Intègre le versement transport
pull/60/head
Laurent Bossavit 2017-10-01 16:33:52 +02:00 committed by GitHub
commit 91956f2d4b
24 changed files with 172079 additions and 12433 deletions

3
.gitignore vendored
View File

@ -3,3 +3,6 @@
node_modules/
dist/
.DS_Store
yarn.lock
package-lock.json
yarn-error.log

12378
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,10 @@
"react-router": "^4.1.1",
"react-router-dom": "^4.1.1",
"react-scroll": "^1.5.4",
"react-select": "^1.0.0-rc.10",
"react-select-fast-filter-options": "^0.2.3",
"react-virtualized": "^9.10.1",
"react-virtualized-select": "^3.1.0",
"reduce-reducers": "^0.1.2",
"redux": "^3.6.0",
"redux-form": "6.8.0",
@ -43,6 +47,7 @@
"babel-core": "^6.24.1",
"babel-eslint": "^7.2.3",
"babel-loader": "^7.0.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-do-expressions": "^6.22.0",
@ -55,6 +60,7 @@
"chokidar": "^1.7.0",
"core-js": "^2.4.1",
"css-loader": "^0.28.1",
"csv-loader": "^2.1.1",
"daggy": "^1.1.0",
"eslint": "^4.4.1",
"eslint-plugin-react": "^7.0.1",
@ -79,7 +85,7 @@
"source-map-support": "^0.4.15",
"style-loader": "^0.18.2",
"url-loader": "^0.5.8",
"webpack": "^3.5.4",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.4.5"
},
"scripts": {

39202
règles/communes.csv Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
- espace: contrat salarié
nom: versement transport
non applicable si: entreprise . effectif < 11
# TODO variations sur la période
# variations:
# - si: période >= 2016
# condition: Entreprise . effectif >= 11
# - si: période < 2016
# condition: Entreprise . effectif >= 10
formule:
multiplication:
assiette: assiette cotisations sociales
taux: établissement . taux versement transport
- espace: établissement
nom: taux versement transport
données: taux_versement_transport
formule:
sélection:
données: taux versement transport
cherche: commune
dans: nomLaposte
composantes:
- nom: aot
renvoie: aot
- nom: smt
renvoie: smt

View File

@ -1,25 +0,0 @@
- contrat salarié: versement transport
applicable si:
# TODO variations sur la période
# variations:
# - si: période >= 2016
# condition: Entreprise . effectif >= 11
# - si: période < 2016
# condition: Entreprise . effectif >= 10
Entreprise . effectif >= 11
formule:
multiplication:
assiette: assiette cotisations sociales
taux: Établissement . taux versement transport
- contrat salarié . versement transport: taux
saisie:
sélection:
# On est ici sur l'option d'une recherche par mots clefs dans toutes les colonnes du tableau (ville par nom, par code postal, par code INSEE). On peut imaginer restreindre la recherche à l'une de ces clefs.
données: https://raw.githubusercontent.com/openfisca/openfisca-france/master/openfisca_france/assets/versement_transport/taux.csv
valeur clef:
composantes:
- taux # taux AOT
- taux additionnel # taux SMT

View File

@ -150,6 +150,7 @@
- FNAL
- Contribution au Dialogue Social
- formation professionnelle
- versement transport
- taxe d'apprentissage
- cotisation pénibilité
- taxe sur les salaires

View File

@ -0,0 +1,19 @@
- espace: établissement
nom: code postal
titre: Code postal de l'établissement
question: Quel est le code postal de la commune où est implanté l'établissement ?
description: |
Lorsqu'une entreprise dispose de plusieurs établissements, certaines cotisations sont
calculées à l'échelle de l'établissement et sont fonction de règlementations locales.
format: nombre
- espace: établissement
nom: commune
titre: Commune de l'établissement
question: Quel est la commune où est implanté l'établissement ?
description: |
Lorsqu'une entreprise dispose de plusieurs établissements, certaines cotisations sont
calculées à l'échelle de l'établissement et sont fonction de règlementations locales.
# format: objet
format: texte
suggestions: communes

View File

@ -13,6 +13,7 @@
"transform-do-expressions",
"transform-object-rest-spread",
"transform-class-properties",
"syntax-dynamic-import",
["webpack-alias", { "config": "./source/webpack.config.js" }]
]
}

View File

@ -12,18 +12,19 @@ export default class Input extends Component {
let {
name,
input,
stepProps: {attributes, submit, valueType, suggestions, setFormValue},
stepProps: {attributes, submit, valueType, suggestions},
meta: {
touched, error, active,
},
themeColours,
} = this.props,
answerSuffix = valueType.suffix,
suffixed = answerSuffix != null,
inputError = touched && error,
sendButtonDisabled = this.state.suggestedInput || !input.value || inputError
if (typeof suggestions == 'string')
return <Select />
return (
<span>
<span className="answer">
@ -51,21 +52,28 @@ export default class Input extends Component {
<span className="icon">&#10003;</span>
</button>
</span>
{suggestions && <span className="inputSuggestions">suggestions:
<ul>
{R.toPairs(suggestions).map(([text, value]) =>
<li key={value}
onClick={e => setFormValue('' + value) && submit() && e.preventDefault()}
onMouseOver={() => setFormValue('' + value) && this.setState({suggestedInput: true})}
onMouseOut={() => setFormValue('') && this.setState({suggestedInput: false})}>
<a href="#" title="cliquer pour valider">{text}</a>
</li>
)}
</ul>
</span>
}
{this.renderSuggestions()}
{inputError && <span className="step-input-error">{error}</span>}
</span>
)
}
renderSuggestions(){
let {setFormValue, submit, suggestions} = this.props.stepProps
if (!suggestions) return null
return (
<span className="inputSuggestions">suggestions:
<ul>
{R.toPairs(suggestions).map(([text, value]) =>
<li key={value}
onClick={e => setFormValue('' + value) && submit() && e.preventDefault()}
onMouseOver={() => setFormValue('' + value) && this.setState({suggestedInput: true})}
onMouseOut={() => setFormValue('') && this.setState({suggestedInput: false})}>
<a href="#" title="cliquer pour valider">{text}</a>
</li>
)}
</ul>
</span>)
}
}

View File

@ -29,15 +29,20 @@ let jours = {
}
let nombre = {
suffix: '',
human: value => value,
validator: int
}
let texte = {
human: value => value,
validator: {test: () => true}
}
export default {
pourcentage,
euros,
mois,
jours,
nombre
nombre,
texte
}

View File

@ -0,0 +1,52 @@
import React, { Component } from 'react'
import {FormDecorator} from '../FormDecorator'
import VirtualizedSelect from 'react-virtualized-select'
import createFilterOptions from 'react-select-fast-filter-options'
import 'react-select/dist/react-select.css'
import './Select.css'
@FormDecorator('select')
export default class Select extends Component {
state = {
data: null
}
componentDidMount(){
import(/* webpackChunkName: "communescsv" */ 'Règles/communes.csv')
.then(module => this.setState({
data: module,
}))
.catch(error => 'An error occurred while loading the component')
}
render() {
let {
input: {
onChange,
},
stepProps: {submit, suggestions}
} = this.props,
submitOnChange =
option => {
onChange(option.Nom_commune)
submit()
}
if (!this.state.data)
return <div>Nous reçevons les données... </div>
return (
<div className="select-answer commune">
<VirtualizedSelect
options={this.state.data}
onChange={submitOnChange}
ignoreAccents={false}
labelKey="Nom_commune"
valueKey="Nom_commune"
placeholder="Entrez le nom de commune"
noResultsText="Nous n'avons trouvé aucune commune"
/>
</div>
)
}
}

View File

@ -4,6 +4,7 @@ import R from 'ramda'
import Explicable from 'Components/conversation/Explicable'
import Question from 'Components/conversation/Question'
import Input from 'Components/conversation/Input'
import Select from 'Components/conversation/select/Select'
import formValueTypes from 'Components/conversation/formValueTypes'
import {analyseSituation} from './traverse'
@ -141,7 +142,7 @@ export let generateQuestion = flatRules => ([dottedName, objectives]) => {
// console.log(isVariant(rule)?"variant":"generateQuestion",[dottedName, objectives.length])
let numericQuestion = rule => ({
let inputQuestion = rule => ({
component: Input,
valueType: formValueTypes[rule.format],
attributes: {
@ -150,6 +151,11 @@ export let generateQuestion = flatRules => ([dottedName, objectives]) => {
},
suggestions: rule.suggestions,
})
let selectQuestion = rule => ({
component: Select,
valueType: formValueTypes[rule.format],
suggestions: rule.suggestions,
})
let binaryQuestion = rule => ({
component: Question,
choices: [
@ -168,9 +174,12 @@ export let generateQuestion = flatRules => ([dottedName, objectives]) => {
common,
isVariant(rule) ?
multiChoiceQuestion(rule) :
rule.format != null ?
numericQuestion(rule) :
binaryQuestion(rule),
rule.format == null ?
binaryQuestion(rule) :
typeof rule.suggestions == 'string' ?
selectQuestion(rule) :
inputQuestion(rule)
,
guidance
)
}

View File

@ -1,6 +1,11 @@
# Liste et description des différents mécanismes compris par le moteur.
# La description peut être rédigée en markdown :-)
sélection:
type: numeric
description: |
C'est tout simplement une valeur numérique exprimée en pourcentage.
une possibilité:
type: enum

View File

@ -3,6 +3,11 @@ import React from 'react'
import {anyNull, val} from './traverse-common-functions'
import {Node, Leaf} from './traverse-common-jsx'
import {makeJsx, evaluateNode, rewriteNode, evaluateArray, evaluateArrayWithFilter, evaluateObject, parseObject, collectNodeMissing} from './evaluation'
import {findRuleByName} from './rules'
import 'react-virtualized/styles.css'
import {Table, Column} from 'react-virtualized'
import taux_versement_transport from 'Règles/rémunération-travail/cotisations/ok/taux.json'
let constantNode = constant => ({nodeValue: constant, jsx: nodeValue => <span className="value">{nodeValue}</span>})
@ -153,7 +158,7 @@ export let mecanismOneOf = (recurse, k, v) => {
let evaluate = (situationGate, parsedRules, node) => {
let evaluateOne = child => evaluateNode(situationGate, parsedRules, child),
explanation = R.map(evaluateOne, node.explanation),
explanation = R.map(evaluateOne, node.explanation),
values = R.pluck("nodeValue",explanation),
nodeValue = R.any(R.equals(true),values) ? true :
(R.any(R.equals(null),values) ? null : false)
@ -609,6 +614,88 @@ export let mecanismComplement = (recurse,k,v) => {
}
}
export let mecanismSelection = (recurse,k,v) => {
if (v.composantes) { //mécanisme de composantes. Voir known-mecanisms.md/composantes
return decompose(recurse,k,v)
}
let dataSourceName = v['données']
let dataSearchField = v['dans']
let dataTargetName = v['renvoie']
let explanation = recurse(v['cherche'])
let evaluate = (situationGate, parsedRules, node) => {
let collectMissing = node => collectNodeMissing(node.explanation),
explanation = evaluateNode(situationGate, parsedRules, node.explanation),
dataSource = findRuleByName(parsedRules, dataSourceName),
data = dataSource ? dataSource['data'] : null,
dataKey = explanation.nodeValue,
dataItems = (data && dataKey && dataSearchField) ? R.filter(item => item[dataSearchField] == dataKey, data) : null,
dataItemValues = (dataItems && !R.isEmpty(dataItems)) ? R.values(dataItems) : null,
// TODO - over-specific! transform the JSON instead
dataItemSubValues = dataItemValues && dataItemValues[0][dataTargetName] ? dataItemValues[0][dataTargetName]["taux"] : null,
sortedSubValues = dataItemSubValues ? R.sortBy(pair => pair[0], R.toPairs(dataItemSubValues)) : null,
// return 0 if we found a match for the lookup but not for the specific field,
// so that component sums don't sum to null
nodeValue = dataItems ? (sortedSubValues ? Number.parseFloat(R.last(sortedSubValues)[1])/100 : 0) : null
return rewriteNode(node,nodeValue,explanation,collectMissing)
}
let indexOf = explanation => explanation.nodeValue ? R.findIndex(x => x['nomLaposte'] == explanation.nodeValue, R.values(taux_versement_transport)) : 0
let indexOffset = 8
let jsx = (nodeValue, explanation) =>
<Node
classes="mecanism"
name="sélection"
value={nodeValue}
child={
<Table
width={300}
height={300}
headerHeight={20}
rowHeight={30}
rowCount={R.values(taux_versement_transport).length}
scrollToIndex={indexOf(explanation)+indexOffset}
rowStyle={
({ index }) => index == indexOf(explanation) ? { fontWeight: "bold" } : {}
}
rowGetter={
({ index }) => {
// transformation de données un peu crade du fichier taux.json qui gagnerait à être un CSV
let line = R.values(taux_versement_transport)[index],
getLastTaux = dataTargetName => {
let lastTaux = R.values(R.path([dataTargetName, 'taux'], line))
return (lastTaux && lastTaux.length && lastTaux[0]) || 0
}
return {
nom: line['nomLaposte'],
taux: getLastTaux(dataTargetName)
}
}
}
>
<Column
label='Nom de commune'
dataKey='nom'
width={200}
/>
<Column
width={100}
label={'Taux ' + dataTargetName}
dataKey="taux"
/>
</Table>
}
/>
return {
evaluate,
explanation,
jsx
}
}
export let mecanismError = (recurse,k,v) => {
throw "Le mécanisme '"+k+"' est inconnu !"+v
}

View File

@ -5,16 +5,20 @@ import R from 'ramda'
import possibleVariableTypes from './possibleVariableTypes.yaml'
import marked from './marked'
// TODO - should be in UI, not engine
import taux_versement_transport from '../../règles/rémunération-travail/cotisations/ok/taux.json'
// console.log('rawRules', rawRules.map(({espace, nom}) => espace + nom))
/***********************************
Méthodes agissant sur une règle */
// Enrichissement de la règle avec des informations évidentes pour un lecteur humain
export let enrichRule = rule => {
export let enrichRule = (rule, sharedData = {}) => {
let
type = possibleVariableTypes.find(t => R.has(t, rule)),
name = rule['nom'],
ns = rule['espace'],
data = rule['données'] ? sharedData[rule['données']] : null,
dottedName = ns ? [
ns,
name
@ -22,7 +26,7 @@ export let enrichRule = rule => {
subquestionMarkdown = rule['sous-question'],
subquestion = subquestionMarkdown && marked(subquestionMarkdown)
return {...rule, type, name, ns, dottedName, subquestion}
return {...rule, type, name, ns, data, dottedName, subquestion}
}
export let hasKnownRuleType = rule => rule && enrichRule(rule).type
@ -64,7 +68,7 @@ export let disambiguateRuleReference = (allRules, {ns, name}, partialName) => {
}
// On enrichit la base de règles avec des propriétés dérivées de celles du YAML
export let rules = rawRules.map(enrichRule)
export let rules = rawRules.map(rule => enrichRule(rule, {taux_versement_transport}))
/****************************************

View File

@ -8,7 +8,8 @@ import Grammar from './grammar.ne'
import {Node, Leaf} from './traverse-common-jsx'
import {
mecanismOneOf,mecanismAllOf,mecanismNumericalSwitch,mecanismSum,mecanismProduct,
mecanismScale,mecanismMax,mecanismMin, mecanismError, mecanismComplement
mecanismScale,mecanismMax,mecanismMin, mecanismError, mecanismComplement,
mecanismSelection
} from "./mecanisms"
import {evaluateNode, rewriteNode, collectNodeMissing, makeJsx} from './evaluation'
@ -291,7 +292,7 @@ let treat = (rules, rule) => rawNode => {
let mecanisms = R.intersection(R.keys(rawNode), R.keys(knownMecanisms))
if (mecanisms.length != 1) {
console.log('Erreur : On ne devrait reconnaître que un et un seul mécanisme dans cet objet', rawNode)
console.log('Erreur : On ne devrait reconnaître que un et un seul mécanisme dans cet objet', mecanisms, rawNode)
throw 'OUPS !'
}
@ -308,6 +309,7 @@ let treat = (rules, rule) => rawNode => {
'le maximum de': mecanismMax,
'le minimum de': mecanismMin,
'complément': mecanismComplement,
'sélection': mecanismSelection,
'une possibilité': R.always({'une possibilité':'oui', collectMissing: node => [rule.dottedName]})
},
action = R.propOr(mecanismError, k, dispatch)

View File

@ -62,6 +62,15 @@ module.exports = {
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.csv$/,
loader: 'csv-loader',
options: {
dynamicTyping: true,
header: true,
skipEmptyLines: true
}
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loader: 'url-loader?limit=10000&name=images/[name].[ext]!img-loader?progressive=true'

View File

@ -27,6 +27,15 @@ module.exports = {
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.csv$/,
loader: 'csv-loader',
options: {
dynamicTyping: true,
header: true,
skipEmptyLines: true
}
},
{ //slow : ~ 3 seconds
test: /\.(jpe?g|png|gif|svg)$/i,
loader: 'ignore-loader'

View File

@ -265,16 +265,16 @@ describe('buildNextSteps', function() {
});
it('should generate questions from the real rules, experimental version', function() {
let stateSelector = (name) => ({"contrat salarié . type de contrat":"CDI"})[name]
let stateSelector = (name) => ({"contrat salarié . type de contrat":"CDI","entreprise . effectif":"50"})[name]
let rules = realRules.map(enrichRule),
situation = analyseTopDown(rules,"Salaire")(stateSelector),
objectives = getObjectives(stateSelector, situation.root, situation.parsedRules),
missing = collectMissingVariables()(stateSelector,situation),
result = buildNextSteps(stateSelector, rules, situation)
expect(R.path(["question","props","label"])(result[0])).to.equal("Quel est le salaire brut ?")
expect(R.path(["question","props","label"])(result[1])).to.equal("Le salarié a-t-il le statut cadre ?")
expect(R.path(["question","props","label"])(result[2])).to.equal("Quel est l'effectif de l'entreprise ?")
});
});

View File

@ -12,6 +12,12 @@ describe('enrichRule', function() {
expect(enrichRule(rule)).to.have.property('type','cotisation')
});
it('should load external data into the rule', function() {
let data = {taux_versement_transport: {one: "two"}}
let rule = {cotisation:{}, données: 'taux_versement_transport'}
expect(enrichRule(rule, data)).to.have.deep.property('data',{one: "two"})
});
it('should extract the dotted name of the rule', function() {
let rule = {espace:"contrat salarié", nom: "CDD"}
expect(enrichRule(rule)).to.have.property('name','CDD')
@ -22,4 +28,5 @@ describe('enrichRule', function() {
let rule = {"sous-question":"**wut**"}
expect(enrichRule(rule)).to.have.property('subquestion','<p><strong>wut</strong></p>\n')
});
});

View File

@ -240,4 +240,40 @@ describe('analyseSituation with mecanisms', function() {
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',100+1200+80)
});
it('should handle selection', function() {
let stateSelector = (name) => ({"top . code postal":"2"})[name]
let data = {taux_versement_transport: {xyz: {codePostal:1, aot: {taux: {"2019":"1.0"}}}, abc: {codePostal:2, smt: {taux: {"2019":"2.0"}}}}}
let rawRules = [
{ espace: "top",
nom: "startHere",
formule: {"sélection": {
données: "startHere",
cherche: "code postal",
dans: "codePostal",
renvoie: "smt"
}},
données: 'taux_versement_transport'},
{espace: "top", nom: "code postal", format: "nombre"}],
rules = rawRules.map(rule => enrichRule(rule,data))
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue',0.02)
});
it('should handle failed selections', function() {
let stateSelector = (name) => ({"top . code postal":"3"})[name]
let data = {taux_versement_transport: {xyz: {codePostal:1, aot: {taux: {"2019":"1.0"}}}, abc: {codePostal:2, smt: {taux: {"2019":"2.0"}}}}}
let rawRules = [
{ espace: "top",
nom: "startHere",
formule: {"sélection": {
données: "startHere",
cherche: "code postal",
dans: "codePostal",
renvoie: "smt"
}},
données: 'taux_versement_transport'},
{espace: "top", nom: "code postal", format: "nombre"}],
rules = rawRules.map(rule => enrichRule(rule,data))
expect(analyseSituation(rules,"startHere")(stateSelector)).to.have.property('nodeValue', 0)
});
});