Merge pull request #171 from betagouv/taxe-salaires-cits

Taxe salaires - CITS
pull/176/merge
Mael 2018-02-27 18:12:04 +01:00 committed by GitHub
commit b8c46f3e69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 617 additions and 222 deletions

View File

@ -83,8 +83,8 @@
"fantasy-land": "^3.3.0",
"fantasy-tuples": "^1.0.0",
"file-loader": "^1.1.6",
"hard-source-webpack-plugin": "^0.5.18",
"html-loader": "^0.5.1",
"img-loader": "^2.0.0",
"jsdom": "^11.0.0",
"json-loader": "^0.5.4",
"nearley-loader": "0.0.2",

View File

@ -6,6 +6,10 @@ export function stepAction(name, step, source) {
return { type: STEP_ACTION, name, step, source }
}
export function setExample(name, situation) {
return { type: 'SET_EXAMPLE', situation, name }
}
export const START_CONVERSATION = 'START_CONVERSATION'
// Reset the form

View File

@ -26,7 +26,8 @@ import { capitalise0, humanFigure } from '../utils'
import { nameLeaf, encodeRuleName } from 'Engine/rules'
// Filtered variables and rules can't be filtered in a uniform way, for now
let paidBy = pathEq(['explanation', 'cotisation', 'dû par'])
let paidBy = payer => item =>
pathEq(['explanation', item.explanation.type, 'dû par'], payer, item)
let filteredBy = pathEq(['cotisation', 'dû par'])
export let byName = groupBy(prop('dottedName'))

View File

@ -9,6 +9,7 @@ import { START_CONVERSATION } from '../actions'
import {
rules,
findRuleByName,
findRule,
findRuleByDottedName,
decodeRuleName
} from 'Engine/rules'
@ -57,7 +58,7 @@ export default class extends Component {
this.targetNames = targetNames
this.targetRules = reject(isNil)(
targetNames.map(name => findRuleByName(rules, name))
targetNames.map(name => findRule(rules, name))
)
this.targetRules.map(({ dottedName }) => resetFormField(dottedName))

View File

@ -29,13 +29,17 @@ export class SearchBar extends React.Component {
componentWillMount() {
var options = {
keys: [
{
name: 'name',
weight: 0.3
},
{
name: 'title',
weight: 0.5
weight: 0.3
},
{
name: 'espace',
weight: 0.3
weight: 0.2
},
{
name: 'description',
@ -55,11 +59,19 @@ export class SearchBar extends React.Component {
handleChange = selectedOption => {
this.setState({ selectedOption })
}
renderOption = option => (
<Highlighter
searchWords={[this.state.inputValue]}
textToHighlight={option.title}
/>
renderOption = ({ title, dottedName }) => (
<span>
<Highlighter
searchWords={[this.state.inputValue]}
textToHighlight={title}
/>
<span style={{ opacity: 0.6, fontSize: '75%', marginLeft: '.6em' }}>
<Highlighter
searchWords={[this.state.inputValue]}
textToHighlight={dottedName}
/>
</span>
</span>
)
filterOptions = (options, filter) => this.fuse.search(filter)
render() {

View File

@ -6,6 +6,9 @@ import knownMecanisms from 'Engine/known-mecanisms.yaml'
import { makeJsx } from 'Engine/evaluation'
import './Algorithm.css'
import { humanFigure } from '../../utils'
import { head } from 'ramda'
import { analyse } from 'Engine/traverse'
import { exampleSituationGateWithDefaults } from './Examples'
let RuleWithoutFormula = () => (
<p>
@ -16,14 +19,21 @@ let RuleWithoutFormula = () => (
@AttachDictionary(knownMecanisms)
export default class Algorithm extends React.Component {
state = {
showValues: true
}
render() {
let { rule, showValues } = this.props,
let { rule: displayedRule, showValues, currentExample, rules } = this.props,
ruleWithoutFormula =
!rule['formule'] ||
path(['formule', 'explanation', 'une possibilité'], rule)
!displayedRule['formule'] ||
path(['formule', 'explanation', 'une possibilité'], displayedRule)
let rule = currentExample
? head(
analyse(rules, displayedRule.dottedName)(
exampleSituationGateWithDefaults(currentExample.situation, rules)
).targets
)
: displayedRule
console.log('didcomp')
return (
<div id="algorithm">

View File

@ -1,11 +1,15 @@
import React, { Component } from 'react'
import { evolve, path, isEmpty } from 'ramda'
import { evolve, path, isEmpty, compose } from 'ramda'
import classNames from 'classnames'
import { connect } from 'react-redux'
import { disambiguateExampleSituation, collectDefaults } from 'Engine/rules.js'
import { analyse } from 'Engine/traverse'
import './Examples.css'
import { assume } from '../../reducers'
import { setExample } from '../../actions'
export let exampleSituationGateWithDefaults = (situationObject, rules) =>
assume(() => name => situationObject[name], collectDefaults(rules))()
// By luck this works as expected for both null and undefined, * but with different branches failing :O *
export let isFloat = n => Number(n) === n && n % 1 !== 0
@ -16,10 +20,8 @@ export let runExamples = (examples, rule, parsedRules) =>
examples
.map(evolve({ situation: disambiguateExampleSituation(parsedRules, rule) }))
.map(ex => {
let exampleSituationGate = () => name => ex.situation[name]
let runExample = analyse(parsedRules, rule.name)(
assume(exampleSituationGate, collectDefaults(parsedRules))()
let runExample = analyse(parsedRules, rule.dottedName)(
exampleSituationGateWithDefaults(ex.situation, parsedRules)
),
exampleValue = runExample.targets[0].nodeValue,
goal = ex['valeur attendue'],
@ -37,21 +39,25 @@ export let runExamples = (examples, rule, parsedRules) =>
}
})
@connect(state => ({
situationGate: state.situationGate,
parsedRules: state.parsedRules,
colour: state.themeColours.colour
}))
@connect(
state => ({
situationGate: state.situationGate,
parsedRules: state.parsedRules,
colour: state.themeColours.colour
}),
dispatch => ({
setExample: compose(dispatch, setExample)
})
)
export default class Examples extends Component {
render() {
let focusedExample = path(['focusedExample', 'nom'])(this.props),
{
inject,
let {
situationExists,
showValues,
rule,
parsedRules,
colour
colour,
setExample,
currentExample
} = this.props,
{ exemples = [] } = rule,
examples = runExamples(exemples, rule, parsedRules)
@ -70,46 +76,51 @@ export default class Examples extends Component {
</p>
) : (
<ul>
{examples.map(({ nom, ok, rule, 'valeur attendue': expected }) => (
<li
key={nom}
className={classNames('example', {
ok,
selected: focusedExample == nom
})}
onClick={() => inject({ nom, ok, rule })}
>
<span>
{' '}
{ok ? (
<i className="fa fa-check-circle" aria-hidden="true" />
) : (
<i className="fa fa-times" aria-hidden="true" />
)}
</span>
<span className="name">{nom}</span>
{!ok &&
focusedExample == nom && (
<div className="ko">
Ce test ne passe pas
{showValues && (
{examples.map(
({ nom, ok, rule, 'valeur attendue': expected, situation }) => (
<li
key={nom}
className={classNames('example', {
ok,
selected: currentExample && currentExample.name == nom
})}
onClick={() =>
currentExample
? setExample(null)
: setExample(nom, situation)
}
>
<span>
{' '}
{ok ? (
<i className="fa fa-check-circle" aria-hidden="true" />
) : (
<i className="fa fa-times" aria-hidden="true" />
)}
</span>
<span className="name">{nom}</span>
{!ok &&
currentExample &&
currentExample.name == nom && (
<div className="ko">
Ce test ne passe pas
<span>
: le résultat attendu était{' '}
<span className="expected">{expected}</span>
</span>
)}
</div>
)}
</li>
))}
</div>
)}
</li>
)
)}
</ul>
)}
{situationExists &&
focusedExample && (
currentExample && (
<div>
<button
id="injectSituation"
onClick={() => inject()}
onClick={() => setExample(null)}
style={{ background: colour }}
>
Revenir à votre situation

View File

@ -65,8 +65,8 @@
}
#meta-content {
border-top: 1px solid rgba(51, 51, 80, 0.2);
background: rgba(51, 51, 80, 0.03);
border: 1px solid rgba(41, 117, 209, 0.3);
background: rgba(41, 117, 209, 0.08);
padding: 1em;
display: flex;
justify-content: flex-start;
@ -78,12 +78,6 @@
font-size: 95%;
}
#rule h3 {
font-size: 100%;
font-weight: 400;
margin: 0.4em 0;
}
#destinataire {
text-align: center;
max-width: 25%;
@ -127,3 +121,7 @@
margin: 3em auto 0;
display: block;
}
#notes {
color: #666;
}

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'
import { isEmpty, path } from 'ramda'
import { isEmpty, path, last } from 'ramda'
import { connect } from 'react-redux'
import './Rule.css'
import { capitalise0 } from '../../utils'
@ -17,21 +17,16 @@ import SearchButton from 'Components/SearchButton'
@connect(state => ({
form: state.form,
rules: state.parsedRules
rules: state.parsedRules,
currentExample: state.currentExample
}))
export default class Rule extends Component {
state = {
example: null,
showValues: true
}
render() {
let { form, rule } = this.props,
conversationStarted = !isEmpty(form),
situationExists = conversationStarted || this.state.example != null
let { form, rule, currentExample, rules } = this.props,
conversationStarted = !isEmpty(form)
let { type, name, title, description, question, ns } = rule,
situationOrExampleRule = path(['example', 'rule'])(this.state) || rule,
namespaceRules = findRuleByNamespace(this.props.rules, rule.dottedName)
namespaceRules = findRuleByNamespace(rules, rule.dottedName)
return (
<div id="rule">
@ -53,22 +48,24 @@ export default class Rule extends Component {
<section id="rule-content">
<Algorithm
rule={situationOrExampleRule}
showValues={situationExists}
rules={rules}
currentExample={currentExample}
rule={rule}
showValues={conversationStarted || currentExample}
/>
{rule.note && (
<section id="notes">
<h3>Note: </h3>
{createMarkdownDiv(rule.note)}
</section>
)}
<Examples
currentExample={currentExample}
situationExists={conversationStarted}
rule={rule}
focusedExample={this.state.example}
showValues={this.state.showValues}
inject={example =>
this.state.example != null
? this.setState({ example: null })
: this.setState({ example, showValues: true })
}
/>
{!isEmpty(namespaceRules) && (
<NamespaceRules {...{ rule, namespaceRules }} />
<NamespaceRulesList {...{ rule, namespaceRules }} />
)}
{this.renderReferences(rule)}
</section>
@ -86,7 +83,7 @@ export default class Rule extends Component {
) : null
}
let NamespaceRules = withColours(({ namespaceRules, rule, colours }) => (
let NamespaceRulesList = withColours(({ namespaceRules, rule, colours }) => (
<section>
<h2>
Règles attachées<small>
@ -101,7 +98,7 @@ let NamespaceRules = withColours(({ namespaceRules, rule, colours }) => (
color: colours.textColourOnWhite,
textDecoration: 'underline'
}}
to={'/règle/' + encodeRuleName(r.name)}
to={'/règle/' + encodeRuleName(r.dottedName)}
>
{r.name}
</Link>
@ -133,24 +130,33 @@ let RuleMeta = ({ ns, type, description, question, rule, name }) => (
export let Namespace = withColours(({ ns, colours }) => (
<ul id="namespace">
{ns.split(' . ').map(fragment => (
<li key={fragment}>
<Link
style={{
color: colours.textColourOnWhite,
textDecoration: 'underline'
}}
to={'/règle/' + encodeRuleName(fragment)}
>
{capitalise0(fragment)}
</Link>
<i
style={{ margin: '0 .6em', fontSize: '85%' }}
className="fa fa-chevron-right"
aria-hidden="true"
/>
</li>
))}
{ns
.split(' . ')
.reduce(
(memo, next) => [
...memo,
[...(memo.length ? memo.reverse()[0] : []), next]
],
[]
)
.map(fragments => (
<li key={fragments.join()}>
<Link
style={{
color: colours.textColourOnWhite,
textDecoration: 'underline'
}}
to={'/règle/' + encodeRuleName(fragments.join(' . '))}
>
{capitalise0(last(fragments))}
</Link>
<i
style={{ margin: '0 .6em', fontSize: '85%' }}
className="fa fa-chevron-right"
aria-hidden="true"
/>
</li>
))}
</ul>
))

View File

@ -141,3 +141,10 @@ composantes:
Il est même possible, pour les mécanismes `barème` et `multiplication` de garder en commun un paramètre comme l'assiette, puis de déclarer des composantes pour le taux.
> L'example le plus courant de composantes, c'est la distinction part employeur, part salarié (ex. retraite AGIRC).
allègement:
type: numeric
description: |
Permet de réduire le montant d'une variable.
Très utilisé dans le contexte des impôts.

View File

@ -0,0 +1,60 @@
import React from 'react'
import { Node } from './common'
import { makeJsx } from '../evaluation'
import { mapObjIndexed, values } from 'ramda'
export default (nodeValue, explanation) => (
<div>
<Node
classes="mecanism allègement"
name="allègement"
value={nodeValue}
child={
<ul className="properties">
<li key="assiette">
<span className="key">assiette: </span>
<span className="value">{makeJsx(explanation.assiette)}</span>
</li>
{explanation.franchise && (
<li key="franchise">
<span className="key">franchise: </span>
<span className="value">{makeJsx(explanation.franchise)}</span>
</li>
)}
{explanation.décote && (
<li key="décote">
<span className="key">décote: </span>
<span className="value">
<ObjectView data={explanation.décote} />
</span>
</li>
)}
{explanation.abattement && (
<li key="abattement">
<span className="key">abattement: </span>
<span className="value">{makeJsx(explanation.abattement)}</span>
</li>
)}
</ul>
}
/>
</div>
)
let ObjectView = ({ data }) =>
console.log('data', data) || (
<ul className="properties">
{values(
mapObjIndexed(
(v, k) => (
<li key={k}>
{' '}
<span className="key">{k}: </span>
<span className="value">{makeJsx(v)}</span>
</li>
),
data
)
)}
</ul>
)

View File

@ -50,6 +50,7 @@ import {
import 'react-virtualized/styles.css'
import Somme from './mecanismViews/Somme'
import Allègement from './mecanismViews/Allègement'
import buildSelectionView from './mecanismViews/Selection'
import uniroot from './uniroot'
@ -194,7 +195,7 @@ let devariate = (recurse, k, v) => {
}
export let mecanismOneOf = (recurse, k, v) => {
if (!is(Array, v)) throw 'should be array'
if (!is(Array, v)) throw new Error('should be array')
let explanation = map(recurse, v)
@ -238,7 +239,7 @@ export let mecanismOneOf = (recurse, k, v) => {
}
export let mecanismAllOf = (recurse, k, v) => {
if (!is(Array, v)) throw 'should be array'
if (!is(Array, v)) throw new Error('should be array')
let explanation = map(recurse, v)
@ -287,7 +288,9 @@ export let mecanismNumericalSwitch = (recurse, k, v) => {
if (is(String, v)) return recurse(v)
if (!is(Object, v) || keys(v).length == 0) {
throw 'Le mécanisme "aiguillage numérique" et ses sous-logiques doivent contenir au moins une proposition'
throw new Error(
'Le mécanisme "aiguillage numérique" et ses sous-logiques doivent contenir au moins une proposition'
)
}
// les termes sont les couples (condition, conséquence) de l'aiguillage numérique
@ -399,7 +402,9 @@ export let mecanismNumericalSwitch = (recurse, k, v) => {
export let findInversion = (situationGate, rules, v, dottedName) => {
let inversions = v.avec
if (!inversions)
throw "Une formule d'inversion doit préciser _avec_ quoi on peut inverser la variable"
throw new Error(
"Une formule d'inversion doit préciser _avec_ quoi on peut inverser la variable"
)
/*
Quelle variable d'inversion possible a sa valeur renseignée dans la situation courante ?
Ex. s'il nous est demandé de calculer le salaire de base, est-ce qu'un candidat à l'inversion, comme
@ -535,6 +540,58 @@ export let mecanismSum = (recurse, k, v) => {
}
}
export let mecanismReduction = (recurse, k, v) => {
let objectShape = {
assiette: false,
abattement: constantNode(0),
franchise: constantNode(0)
}
let effect = ({ assiette, abattement, franchise, décote }) => {
let v_assiette = val(assiette)
if (v_assiette == null) return null
let montantFranchiséDécoté =
val(franchise) && v_assiette < val(franchise)
? 0
: décote
? do {
let plafond = val(décote.plafond),
taux = val(décote.taux)
v_assiette > plafond
? v_assiette
: max(0, (1 + taux) * v_assiette - taux * plafond)
}
: v_assiette
return abattement
? val(abattement) == null
? montantFranchiséDécoté === 0 ? 0 : null
: max(0, montantFranchiséDécoté - val(abattement))
: montantFranchiséDécoté
}
let base = parseObject(recurse, objectShape, v),
explanation = v.décote
? {
...base,
décote: map(recurse, v.décote)
}
: base,
evaluate = evaluateObject(objectShape, effect)
return {
evaluate,
jsx: Allègement,
explanation,
category: 'mecanism',
name: 'allègement',
type: 'numeric'
}
}
export let mecanismProduct = (recurse, k, v) => {
if (v.composantes) {
//mécanisme de composantes. Voir known-mecanisms.md/composantes
@ -921,5 +978,5 @@ export let mecanismSelection = (recurse, k, v) => {
}
export let mecanismError = (recurse, k, v) => {
throw "Le mécanisme '" + k + "' est inconnu !" + v
throw new Error("Le mécanisme '" + k + "' est inconnu !" + v)
}

View File

@ -3,3 +3,4 @@
- aide
- indemnité
- salaire
- taxe

View File

@ -81,13 +81,12 @@ export let decodeRuleName = name =>
/* Les variables peuvent être exprimées dans la formule d'une règle relativement à son propre espace de nom, pour une plus grande lisibilité. Cette fonction résoud cette ambiguité.
*/
export let disambiguateRuleReference = (
allRules,
{ ns, name },
partialName
) => {
let fragments = ns ? ns.split(' . ') : [], // ex. [CDD . événements . rupture]
let fragments = ns ? [...ns.split(' . '), name] : [], // ex. [CDD . événements . rupture]
pathPossibilities = range(0, fragments.length + 1) // -> [ [CDD . événements . rupture], [CDD . événements], [CDD] ]
.map(nbEl => take(nbEl)(fragments))
.reverse(),
@ -103,7 +102,7 @@ export let disambiguateRuleReference = (
return (
(found && found.dottedName) ||
do {
throw `OUUUUPS la référence '${partialName}' dans la règle '${name}' est introuvable dans la base`
throw new `OUUUUPS la référence '${partialName}' dans la règle '${name}' est introuvable dans la base`()
}
)
}
@ -144,6 +143,11 @@ export let findRuleByDottedName = (allRules, dottedName) => {
return allRules.find(rule => rule.dottedName == dottedName)
}
export let findRule = (rules, nameOrDottedName) =>
nameOrDottedName.includes(' . ')
? findRuleByDottedName(rules, nameOrDottedName)
: findRuleByName(rules, nameOrDottedName)
export let findRuleByNamespace = (allRules, ns) =>
allRules.filter(propEq('ns', ns))

View File

@ -2,7 +2,8 @@ import React from 'react'
import {
findRuleByDottedName,
disambiguateRuleReference,
findRuleByName
findRuleByName,
findRule
} from './rules'
import { evaluateVariable } from './variables'
import {
@ -48,7 +49,8 @@ import {
mecanismError,
mecanismComplement,
mecanismSelection,
mecanismInversion
mecanismInversion,
mecanismReduction
} from './mecanisms'
import {
evaluateNode,
@ -390,9 +392,9 @@ let treat = (rules, rule) => rawNode => {
},
treatOther = rawNode => {
console.log() // eslint-disable-line no-console
throw 'Cette donnée : ' +
rawNode +
' doit être un Number, String ou Object'
throw new Error(
'Cette donnée : ' + rawNode + ' doit être un Number, String ou Object'
)
},
treatObject = rawNode => {
let mecanisms = intersection(keys(rawNode), keys(knownMecanisms))
@ -404,7 +406,7 @@ let treat = (rules, rule) => rawNode => {
mecanisms,
rawNode
)
throw 'OUPS !'
throw new Error('OUPS !')
}
let k = head(mecanisms),
@ -425,7 +427,8 @@ let treat = (rules, rule) => rawNode => {
'une possibilité': 'oui',
collectMissing: () => [rule.dottedName]
}),
inversion: mecanismInversion(rule.dottedName)
inversion: mecanismInversion(rule.dottedName),
allègement: mecanismReduction
},
action = propOr(mecanismError, k, dispatch)
@ -621,12 +624,7 @@ export let analyseMany = (parsedRules, targetNames) => situationGate => {
// setRule in Rule.js needs to get smarter and pass dottedName
let cache = {}
let parsedTargets = targetNames.map(
t =>
t.includes(' . ')
? findRuleByDottedName(parsedRules, t)
: findRuleByName(parsedRules, t)
),
let parsedTargets = targetNames.map(t => findRule(parsedRules, t)),
targets = chain(pt => getTargets(pt, parsedRules), parsedTargets).map(t =>
evaluateNode(cache, situationGate, parsedRules, t)
)

View File

@ -1,4 +1,13 @@
import { head, isEmpty, pathOr, reject, contains, without, concat, length } from 'ramda'
import {
head,
isEmpty,
pathOr,
reject,
contains,
without,
concat,
length
} from 'ramda'
import { combineReducers } from 'redux'
import reduceReducers from 'reduce-reducers'
import { reducer as formReducer, formValueSelector } from 'redux-form'
@ -116,7 +125,7 @@ export let reduceSteps = (tracker, flatRules, answerSource) => (
if (action.type == STEP_ACTION && action.name == 'fold') {
tracker.push([
'trackEvent',
'answer:'+action.source,
'answer:' + action.source,
action.step + ': ' + situationWithDefaults(state)(action.step)
])
@ -124,7 +133,7 @@ export let reduceSteps = (tracker, flatRules, answerSource) => (
tracker.push([
'trackEvent',
'done',
'after'+length(newState.foldedSteps)+'questions'
'after' + length(newState.foldedSteps) + 'questions'
])
}
@ -166,6 +175,15 @@ function explainedVariable(state = null, { type, variableName = null }) {
}
}
function currentExample(state = null, { type, situation, name }) {
switch (type) {
case 'SET_EXAMPLE':
return name != null ? { name, situation } : null
default:
return state
}
}
export default reduceReducers(
combineReducers({
sessionId: (id = Math.floor(Math.random() * 1000000000000) + '') => id,
@ -191,7 +209,9 @@ export default reduceReducers(
themeColours,
explainedVariable
explainedVariable,
currentExample
}),
// cross-cutting concerns because here `state` is the whole state tree
reduceSteps(ReactPiwik, rules, formatInputs(rules, formValueSelector))

View File

@ -132,7 +132,7 @@
valeur attendue: 55.21
notes: |
note: |
À noter, la loi El Khomri modifie l'article L3141-12:
- avant : Les congés peuvent être pris dès l'ouverture des droits [...]
@ -157,7 +157,7 @@
alias: prime de précarité
description: Somme versée en fin de CDD comme compensation de précarité.
notes: |
note: |
Attention, les exceptions sont légion. Conventions collectives...
- Dans les faits, les CDD Senior perçoivent une indemnité dun montant équivalent à lindemnité de précarité : [line](https://www.easycdd.com/LEGISLATION-CDD/Fin-ou-rupture-du-contrat-CDD/La-prime-de-precarite/La-prime-de-precarite-n-est-pas-due-si)
@ -273,10 +273,8 @@
La majoration de la contribution chômage: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/lassurance-chomage-et-lags/la-majoration-de-la-contribution.html
Circulaire Unédic: http://www.unedic.org/sites/default/files/ci201317_1.pdf
notes: |
note: |
- L'URSSAF liste à la fois des conditions pour "Les CDD concernés par lapplication de la majoration" et pour "Les contrats de travail exclus de la majoration". Un doute persiste : difficile de savoir si les premières suffisent au calcul (donc que les deuxièmes sont là pour enlever les doutes) ou si les deuxièmes peuvent faire exception...
- Depuis le 1er juillet 2013
- l'URSSAF explique longuement la notion de durée du CDD : "Comment déterminer la durée du CDD ?"
- espace: contrat salarié
@ -564,7 +562,7 @@
titre: Contrat jeune vacances
question: Est-ce un contrat jeune vacances ?
description: Aussi appelé CDD vendanges. Contrat conclu avec un jeune pendant ses vacances scolaires ou universitaires.
notes: Ce n'est pas un motif de CDD.
note: Ce n'est pas un motif de CDD.
par défaut: non
- espace: contrat salarié . CDD
@ -992,8 +990,6 @@
- espace: contrat salarié
nom: plafond cice
# TODO: calcul du smic proratisé
# TODO: smic mensuel défini dans la réduction générale, à déplacer pour mutualiser
formule: multiplicateur cice * smic mensuel
- espace: contrat salarié
@ -1001,6 +997,25 @@
formule: 2.5
- espace: contrat salarié . CITS
nom: non abattu
formule:
multiplication:
assiette: assiette cotisations sociales
taux: 4%
- espace: contrat salarié . CITS
nom: abattement mensuel par salarié
formule:
multiplication:
assiette: entreprise . taxe sur les salaires . abattement associations
taux: 1 / entreprise . effectif
facteur: 1 / 12
note: |
Cette variable révèle deux lacunes de notre modélisation :
- on ne peut pas spécifier plusieurs salariés à l'entreprise, et donc calculer correctement le CITS. On fait donc comme si l'unique salarié simulé était le salarié type, multiplié.
- on ne gère pas la conversion entre les périodes temporelles.
On les résoud en exploitant à tort les capacités du mécanisme multiplication.
- espace: contrat salarié
nom: CITS
@ -1013,36 +1028,48 @@
La loi de finances pour 2017 instaure, au bénéfice des associations et organismes sans but lucratif (OSBL),
un dispositif de crédit d'impôt de taxe sur les salaires (CITS).
références:
fiche: https://www.service-public.fr/associations/actualites/A11012
fiche: https://www.service-public.fr/associations/vosdroits/F34066
non applicable si:
une de ces conditions:
- assiette cotisations sociales > plafond CITS
- ≠ entreprise . association non lucrative
applicable si: entreprise . association non lucrative
non applicable si: assiette cotisations sociales > plafond
note: Le CITS est un crédit sur la taxe sur les salaires. Celle-ci est abattue de ~20k. Le CITS est lui-même abattu de ~20k ! Quel intérêt, pourquoi ne pas simplement supprimer l'abattement initial ? Parce que dans certains cas, une entreprise d'un salarié au SMIC, 4% des rémunérations annuelles < abattement de 20k. Donc le crédit est nul. Donc la taxe sur les salaires reste abattue comme initialement prévu.
formule:
multiplication:
assiette: assiette cotisations sociales
taux: 4%
allègement:
assiette: non abattu
abattement: abattement mensuel par salarié
exemples:
- nom: SMIC
- nom: Petite association, non applicable car taxe sur les salaires nulle (car abattue)
situation:
assiette cotisations sociales: 2300
entreprise . association non lucrative: oui
valeur attendue: 92
valeur attendue: 0
- nom: Applicable
situation:
entreprise . association non lucrative: oui
assiette cotisations sociales: 3000
entreprise . effectif: 50
valeur attendue: 85.82
- nom: Non applicable car rémunération trop forte
situation:
entreprise . association non lucrative: oui
assiette cotisations sociales: 4000
entreprise . effectif: 50
valeur attendue: 0
- nom: Non applicable si organisme lucratif
situation:
assiette cotisations sociales: 2300
entreprise . association non lucrative: non
assiette cotisations sociales: 2300
valeur attendue: 0
- espace: contrat salarié
nom: plafond CITS
formule: multiplicateur CITS * smic mensuel
- espace: contrat salarié . CITS
nom: plafond
formule: multiplicateur * smic mensuel
- espace: contrat salarié
nom: multiplicateur CITS
- espace: contrat salarié . CITS
nom: multiplicateur
formule: 2.5
@ -1171,8 +1198,6 @@
Cotisation de retraite complémentaire
(Cotisation pour l'Association pour la Gestion du Fonds de Financement de lAGIRC et de lARRCO)
référence: http://www.agirc-arrco.fr/entreprises/gerer-les-salaries/calcul-des-cotisations/
notes: |
Attention: les tranches du barème sont différentes pour les cadres et non-cadres, en valeur et en nombres.
formule:
barème:
@ -1322,7 +1347,7 @@
- au-dessus de: 8
taux: 0%
notes: |
note: |
Il existe une tranche C, de 4 à 8 fois la base, sur laquelle la répartition des cotisations est décidée au sein de lentreprise jusquà 20 %. De 20 % à 20,30 %, la répartition est la suivante : 66,67 % à la charge du salarié et 33,33 % pour lemployeur.
références:
@ -1336,12 +1361,6 @@
branche: chômage
references:
calcul: https://www.service-public.fr/professionnels-entreprises/vosdroits/F31409
notes: |
- taux différent pour le personnel intérimaire des entreprises de travail temporaire
- Ne sont pas assujetties :
- les personnes morales de droit public,
- les syndicats de copropriété,
- les particuliers employeurs.
# non applicable si: assimilé salarié
@ -1396,8 +1415,6 @@
(Association Pour lEmploi des Cadres)
références:
chiffres clés: http://www.agirc-arrco.fr/l-agirc-et-larrco/chiffres-cles
notes: |
Avant 2011, il y avait une cotisation forfaitaire au lieu de la tranche A
#TODO double négation en attendant d'ajouter 'applicable si'
non applicable si: ≠ statut cadre
@ -1558,12 +1575,14 @@
cotisation:
dû par: employeur
collecteur: URSSAF
description: Contribution patronale destinée à abonder un fonds paritaire dédié au financement des organisations syndicales et des organisations professionnelles demployeurs.
description: |
Contribution patronale destinée à abonder un fonds paritaire dédié au financement des organisations syndicales et des organisations professionnelles demployeurs.
Anciennement 'contribution patronale au financement des organisations syndicales'
références:
- https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/la-contribution-patronale-au-dia.html
- https://www.service-public.fr/professionnels-entreprises/vosdroits/F33308
notes: |
Anciennement 'contribution patronale au financement des organisations syndicales'
formule:
multiplication:
@ -1595,8 +1614,9 @@
- attributs:
dû par: salarié
taux: 0.13%
- espace: contrat salarié
note: Cette assiette est complexe, cette version n'est qu'une simplification. #TODO
note: Cette assiette est complexe, cette version n'est qu'une simplification.
nom: assiette CSG
références:
calcul: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/la-csg-crds/les-revenus-salariaux-soumis-a-l.html
@ -1608,7 +1628,6 @@
- complémentaire santé (employeur)
- espace: contrat salarié
note: Cette assiette est complexe, cette version n'est qu'une simplification. #TODO
nom: assiette CSG abattue
formule:
barème:
@ -1673,9 +1692,6 @@
branche: logement
références:
calcul: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/la-contribution-au-fonds-nationa.html
note: Il y avait une cotisation supplémentaire pour les entreprises de >= 20 employés avant 2015, mais le résultat était le même.
explication: |
Si l'entreprise a un effectif supérieur ou égal à 20 salariés, elle doit verser 0,50 % sur la totalité des salaires. Pour les entreprises de moins de 20 salariés et pour les employeurs occupés aux activités mentionnées aux 1° à 4° de l'article L. 722-1 du code rural et de la pêche maritime et les coopératives mentionnées à l'article L. 521-1 du même code, la cotisation est de 0,10 % des salaires limités au plafond de sécurité sociale (tranche A). Intégrée aux cotisations de sécurité sociale, elle est recouvrée par les Urssaf pour le financement des allocations logement versées par les caisses d'allocations familiales. Les employeurs qui ont atteint ou dépassé pour la première fois, au titre des années 2008 à 2012, le seuil de 20 salariés ont été dispensés de lancien Fnal supplémentaire pendant 3 ans. La contribution était ensuite progressivement appelée sur les 3 années suivantes. Un dispositif est mis en place pour 2016, 2017 et 2018. Les employeurs qui atteignent ou dépassent au titre de ces années l'effectif de 20 salariés continuent d'appliquer le taux de 0,10 % pendant trois ans (suite au franchissement de seuil). Cette modalité n'implique pas d'adaptation du calcul du coefficient de la réduction générale. Le seuil de 20 salariés s'apprécie au 31 décembre et la modification de la cotisation est effective au 1er avril suivant.
formule:
multiplication:
assiette: assiette cotisations sociales
@ -1708,13 +1724,14 @@
taux: 1%
- si: entreprise . effectif < 11
taux: 0.55%
- espace: contrat salarié
nom: maladie
cotisation:
branche: santé
dû par: employeur
description: Cotisations de la branche maladie
références:
références:
fiche: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/la-cotisation-maladie---maternit.html
Décret n° 2017-1891 relatif au taux des cotisations d'assurance maladie: https://www.legifrance.gouv.fr/eli/decret/2017/12/30/CPAS1732212D/jo/texte
formule:
@ -1759,10 +1776,9 @@
impôt: oui
références:
fiche: https://www.service-public.fr/professionnels-entreprises/vosdroits/F22583
notes: |
note: |
L'employeur a le choix entre verser cet impôt à un "organisme du 1 % patronal" agréé, investir la somme dans le logement de ses salariés, ou accorder à eux et leur famille des prêts de construction à taux réduit.
applicable si: entreprise . effectif >= 20
formule:
multiplication:
@ -1798,7 +1814,7 @@
description: https://www.service-public.fr/professionnels-entreprises/vosdroits/F22574
csa: http://www.opcalia.com/employeurs/financer-la-formation-et-lapprentissage/taxe-dapprentissage/contribution-supplementaire-a-lapprentissage-csa/
notes: Taxe complexe, comportant notamment des exonérations non prises en compte ici.
note: Taxe complexe, comportant notamment des exonérations non prises en compte ici.
non applicable si: entreprise . association non lucrative
# L'association a but non lucratif ne paie pas d'IS de droit commun article 206 du Code général des impôts
@ -1858,28 +1874,28 @@
- entreprise . effectif >= 2000
- entreprise . ratio alternants < 1%
# TODO chantier droits: introduire la répartition entre part régionale, quota d'app., hors quota
# https://www.service-public.fr/professionnels-entreprises/vosdroits/F22574
- espace: contrat salarié
nom: assujettie à la taxe sur les salaires
titre: Entreprise assujettie à la taxe sur les salaires
description: |
Sont assujetties les associations à but non lucratif et les entreprises non soumises à la TVA ou payant la TVA sur moins de 10% de leur chiffre. Les particuliers employeurs, les employeurs agricoles, les établissements d'enseignement supérieur, les auto-entrepreneurs ne sont pas concernés.
question: L'entreprise est-elle assujettie à la taxe sur les salaires ?
# variable non utilisée pour l'instant, comme dans le simulateur v1
# à ajouter quand nous aurons des mécanismes logiques plus évolués (notamment 'applicable si')
par défaut: non
- espace: contrat salarié . taxe sur les salaires
nom: assiette
formule:
somme:
- assiette cotisations sociales
- prévoyance obligatoire cadre
- complémentaire santé (employeur)
- espace: contrat salarié
nom: taxe sur les salaires annuelle
références:
assiette: http://bofip.impots.gouv.fr/bofip/6690-PGP.html
- espace: contrat salarié . taxe sur les salaires
nom: barème annuel
références:
description: https://www.service-public.fr/professionnels-entreprises/vosdroits/F22576
barème: https://www.service-public.fr/professionnels-entreprises/vosdroits/F22576
formule:
barème:
# TODO - les barèmes étant exprimés en base annuelle, et également à cause des limitations
# de la grammaire, on a recours à cette formulation trop compliquée; à simplifier
assiette: assiette taxe sur les salaires * 12
assiette: assiette * 12
tranches:
- en-dessous de: 7721
taux: 4.25%
@ -1890,40 +1906,79 @@
à: 152279
taux: 13.6%
- au-dessus de: 152279
taux: 20%
taux: 0%
exemples:
- nom: salaire médian
situation:
assiette taxe sur les salaires: 2300
assiette: 2300
valeur attendue: 2639.16 # calcul annuel : 7721×4.25%+(154177721)×8.5%+(2760015417)×13.6%
- espace: contrat salarié
nom: assiette taxe sur les salaires
- espace: entreprise . taxe sur les salaires
nom: barème annuel
formule:
somme:
- assiette cotisations sociales
- prévoyance obligatoire cadre
- complémentaire santé (employeur)
contrat salarié . taxe sur les salaires . barème annuel * effectif
- espace: entreprise . taxe sur les salaires
nom: abattement associations
formule: 20507
- espace: entreprise
nom: taxe sur les salaires
formule:
allègement:
assiette: barème annuel
franchise: 1200
décote:
plafond: 2040
taux: 75%
abattement: abattement associations
note: |
Attention : l'abattement n'est valable que pour les organismes à but non lucratif.
Il n'est pas conditionné ici car on réserve pour l'instant la taxe sur les salaires aux associations 1901
- espace: contrat salarié . taxe sur les salaires
nom: montant annuel
formule: entreprise . taxe sur les salaires / entreprise . effectif
- espace: contrat salarié
nom: taxe sur les salaires
description: La taxe sur les salaires en France est un impôt progressif créé en 1948 que certains employeurs doivent acquitter sur les salaires qu'ils distribuent.
non applicable si: ≠ entreprise . association non lucrative
taxe:
dû par: employeur
description: La taxe sur les salaires en France est un impôt progressif créé en 1948 que certains employeurs doivent acquitter sur les salaires qu'ils distribuent.
applicable si: entreprise . association non lucrative
formule: montant annuel / 12
note: Cette implémentation de la taxe sur les salaires est spécifique aux associations à but non lucratif, elle est donc largement simplifiée. Plein d'autres organisations sont concernées, en fonction de la TVA qu'elles paient. Les associations y sont assujetties automatiquement.
formule: taxe sur les salaires annuelle / 12
exemples:
- nom: non applicable par défaut
situation:
salaire brut: 2300
salaire de base: 2300
valeur attendue: 0
- nom: association non lucrative
# Ce test ne sert qu'à tester la condition "association non lucrative", tant que nous faisons face à la limitation des calculs temporels
- nom: association non lucrative unipersonnelle
situation:
entreprise . association non lucrative: oui
taxe sur les salaires annuelle: 2639.16
valeur attendue: 219.93
salaire de base: 2300
entreprise . effectif: 1
valeur attendue: 0
- nom: association non lucrative
situation:
entreprise . association non lucrative: oui
salaire de base: 2300
entreprise . effectif: 10
forfait complémentaire santé: 0
valeur attendue: 49
références:
fiche: https://www.service-public.fr/professionnels-entreprises/vosdroits/F22576
- espace: contrat salarié
nom: versement transport
description: Contribution sur le travail consacrée au financement des transports publics.

View File

@ -1,6 +1,7 @@
var webpack = require('webpack'),
path = require('path'),
prodEnv = process.env.NODE_ENV == 'production' // eslint-disable-line no-undef
prodEnv = process.env.NODE_ENV == 'production', // eslint-disable-line no-undef
HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
module.exports = {
devtool: 'cheap-module-source-map',
@ -13,10 +14,8 @@ module.exports = {
'@babel/polyfill',
'react-hot-loader/patch',
'./source/entry.js'
],
]
// le nom "simulateur" est là pour des raisons historiques
simulateur: './source/iframe-script.js',
'colour-chooser': ['@babel/polyfill', './source/entry-colour-chooser.js']
},
output: {
path: path.resolve('./dist/'),
@ -92,6 +91,13 @@ module.exports = {
new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }),
new webpack.NoEmitOnErrorsPlugin()
]
.concat(!prodEnv ? [new webpack.HotModuleReplacementPlugin()] : [])
.concat(
!prodEnv
? [
new webpack.HotModuleReplacementPlugin(),
new HardSourceWebpackPlugin()
]
: []
)
.concat(prodEnv ? [new webpack.optimize.UglifyJsPlugin()] : [])
}

View File

@ -0,0 +1,97 @@
- nom: montant
format:
- test: montant franchisé
format:
formule:
allègement:
assiette: montant
franchise: 1200
exemples:
- situation:
montant: 1000
valeur attendue: 0
- situation:
valeur attendue: null
variables manquantes:
- montant
- test: montant décoté
format:
formule:
allègement:
assiette: montant
décote:
plafond: 2040
taux: 100%
exemples:
- situation:
montant: 1000
valeur attendue: 0
- test: montant franchisé et décoté
format:
formule:
allègement:
assiette: montant
franchise: 1200
décote:
plafond: 2040
taux: 75%
exemples:
- situation:
montant: 100
valeur attendue: 0
- situation:
montant: 1200
valeur attendue: 570
- situation:
montant: 1620
valeur attendue: 1305
- situation:
montant: 2040
valeur attendue: 2040
- test: montant abattu
format:
formule:
allègement:
assiette: montant
abattement: 20507
exemples:
- situation:
montant: 10000
valeur attendue: 0
- situation:
montant: 80000
valeur attendue: 59493
- test: montant franchisé, décote, abattu
format:
formule:
allègement:
assiette: montant
franchise: 1200
décote:
plafond: 2040
taux: 75%
abattement: 20507
exemples:
- situation:
montant: 100
valeur attendue: 0
- situation:
montant: 1620
valeur attendue: 0
- situation:
montant: 3000
valeur attendue: 0
- situation:
montant: 21000
valeur attendue: 493

View File

@ -13,7 +13,6 @@ import R from 'ramda'
import { runExamples, isFloat } from '../source/components/rule/Examples'
let parsedRules = parseAll(rules)
describe('Tests des règles de notre base de règles', () =>
parsedRules.map(rule => {
if (!rule.exemples) return null

View File

@ -173,4 +173,46 @@ describe('results grid', function() {
subCell(maladie, 'contrat salarié . ATMP', 'employeur')
).to.be.closeTo(54, 1)
})
it('should access taxe sur les salaires', function() {
let fakeState = {}
let stateSelector = state => name => fakeState[name]
let rules = realRules.map(enrichRule),
reducer = reduceSteps(tracker, rules, stateSelector)
var step1 = reducer(
{ foldedSteps: [] },
{
type: 'START_CONVERSATION',
targetNames: ['salaire net', 'salaire total']
}
)
fakeState['contrat salarié . salaire de base'] = 2300
var step2 = reducer(step1, {
type: 'STEP_ACTION',
name: 'fold',
step: 'contrat salarié . salaire de base'
})
fakeState['entreprise . association non lucrative'] = 'oui'
var step3 = reducer(step2, {
type: 'STEP_ACTION',
name: 'fold',
step: 'entreprise . association non lucrative'
})
fakeState['entreprise . effectif'] = 10
var step4 = reducer(step3, {
type: 'STEP_ACTION',
name: 'fold',
step: 'entreprise . effectif'
})
let analysis = step4.analysis,
result = byBranch(analysis),
autre = byName(result['autre'])
expect(
subCell(autre, 'contrat salarié . taxe sur les salaires', 'employeur')
).to.be.closeTo(51, 1)
})
})

View File

@ -52,7 +52,13 @@ describe('rule checks', function() {
r.defaultValue == null
)
rulesNeedingDefault.map(r => console.log('yo', r.dottedName))
rulesNeedingDefault.map(r =>
console.log(
'cette règle, ',
r.dottedName,
'devrait avoir une valeur par défaut'
)
)
expect(rulesNeedingDefault).to.be.empty
})
})