diff --git a/package.json b/package.json index 64c77128c..fe89c7650 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@babel/polyfill": "^7.4.0", "@babel/runtime": "^7.3.4", "classnames": "^2.2.5", + "cleave.js": "^1.5.3", "color-convert": "^1.9.2", "core-js": "^3.2.1", "focus-trap-react": "^3.1.2", diff --git a/source/components/conversation/DateInput.js b/source/components/conversation/DateInput.js new file mode 100644 index 000000000..96111e76f --- /dev/null +++ b/source/components/conversation/DateInput.js @@ -0,0 +1,63 @@ +import FormattedInput from 'cleave.js/react' +import withColours from 'Components/utils/withColours' +import { compose } from 'ramda' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { debounce } from '../../utils' +import { FormDecorator } from './FormDecorator' +import InputSuggestions from './InputSuggestions' +import SendButton from './SendButton' + +// TODO: fusionner Input.js et CurrencyInput.js +export default compose( + FormDecorator('input'), + withColours +)(function Input({ + suggestions, + setFormValue, + submit, + dottedName, + value, + colours, + unit +}) { + const debouncedSetFormValue = useCallback(debounce(750, setFormValue), []) + const { language } = useTranslation().i18n + + return ( + <> +
+ { + setFormValue(value) + }} + onSecondClick={() => submit('suggestion')} + /> +
+ +
+ { + debouncedSetFormValue(value) + }} + value={value} + autoComplete="off" + /> + + +
+ + ) +}) diff --git a/source/components/simulationConfigs/indépendant.yaml b/source/components/simulationConfigs/indépendant.yaml index e05e0179c..5593f3a1d 100644 --- a/source/components/simulationConfigs/indépendant.yaml +++ b/source/components/simulationConfigs/indépendant.yaml @@ -17,8 +17,9 @@ objectifs: questions: à l'affiche: Type d'activité: entreprise . catégorie d'activité - Impôt sur le revenu: impôt . méthode de calcul + Date de création: entreprise . date de création ACRE: entreprise . ACRE + Impôt sur le revenu: impôt . méthode de calcul liste noire: - entreprise . charges non prioritaires: diff --git a/source/engine/date.ts b/source/engine/date.ts index 50ad3199e..32b690998 100644 --- a/source/engine/date.ts +++ b/source/engine/date.ts @@ -8,7 +8,7 @@ export function convertToDateIfNeeded(...values: string[]) { dateStrings.forEach(dateString => { if (!dateString.match(dateRegexp)) { throw new TypeError( - `L'opérande '${dateString}' n'est pas une date valide` + `'${dateString}' n'est pas une date valide (format attendu: mm/aaaa ou jj/mm/aaaa)` ) } }) diff --git a/source/engine/getInputComponent.js b/source/engine/getInputComponent.js index 2ca5cdd8b..c68c2aa26 100644 --- a/source/engine/getInputComponent.js +++ b/source/engine/getInputComponent.js @@ -5,6 +5,7 @@ import SelectAtmp from 'Components/conversation/select/SelectTauxRisque' import { serialiseUnit } from 'Engine/units' import { is, pick, prop, unless } from 'ramda' import React from 'react' +import DateInput from '../components/conversation/DateInput' import { findRuleByDottedName, queryRule } from './rules' // This function takes the unknown rule and finds which React component should be displayed to get a user input through successive if statements @@ -17,42 +18,37 @@ export default rules => dottedName => { let commonProps = { key: dottedName, fieldName: dottedName, - ...pick(['dottedName', 'title', 'question', 'defaultValue'], rule) + ...pick( + ['dottedName', 'title', 'question', 'defaultValue', 'suggestions'], + rule + ) } if (getVariant(rule)) return ( ) if (rule.API && rule.API === 'géo') return if (rule.API) throw new Error("Le seul API implémenté est l'API géo") - if (rule.suggestions == 'atmp-2017') - return ( - - ) + if (rule.suggestions == 'atmp-2017') return + + if (rule.type === 'date') { + return + } if (rule.unit == null && rule.defaultUnit == null) return ( ) @@ -60,11 +56,8 @@ export default rules => dottedName => { return ( ) } diff --git a/source/engine/known-mecanisms.yaml b/source/engine/known-mecanisms.yaml index a30d32501..8aab2d93d 100644 --- a/source/engine/known-mecanisms.yaml +++ b/source/engine/known-mecanisms.yaml @@ -183,6 +183,11 @@ encadrement: description: | Permet d'ajouter un plafond et/ou un plancher à une valeur. +durée: + type: numeric + description: | + Permet d'obtenir le nombre de jours entre deux dates + synchronisation: type: object description: | diff --git a/source/engine/mecanisms/durée.tsx b/source/engine/mecanisms/durée.tsx new file mode 100644 index 000000000..81224929f --- /dev/null +++ b/source/engine/mecanisms/durée.tsx @@ -0,0 +1,88 @@ +import { convertToDateIfNeeded } from 'Engine/date' +import { + defaultNode, + evaluateNode, + makeJsx, + parseObject +} from 'Engine/evaluation' +import { Node } from 'Engine/mecanismViews/common' +import { parseUnit } from 'Engine/units' +import React from 'react' + +function MecanismDurée({ nodeValue, explanation, unit }) { + return ( + +

+ Depuis : + {makeJsx(explanation.depuis)} +

+

+ Jusqu'à : + {makeJsx(explanation["jusqu'à"])} +

+ + } + /> + ) +} +const pad = (n: number) => (n < 10 ? `0{n}` : +n) +const today = new Date() +const todayString = `${pad(today.getDate())}/${pad( + today.getMonth() + 1 +)}/${today.getFullYear()}` + +const objectShape = { + depuis: defaultNode(todayString), + "jusqu'à": defaultNode(todayString) +} + +const evaluate = (cache, situation, parsedRules, node) => { + let evaluateAttribute = evaluateNode.bind(null, cache, situation, parsedRules) + let from = evaluateAttribute(node.explanation.depuis) + let to = evaluateAttribute(node.explanation["jusqu'à"]) + let nodeValue = 0 + if ([from, to].some(({ nodeValue }) => nodeValue === null)) { + nodeValue = null + } else { + let [fromDate, toDate] = convertToDateIfNeeded(from.nodeValue, to.nodeValue) + nodeValue = Math.max( + 0, + Math.round((toDate - fromDate) / (1000 * 60 * 60 * 24)) + ) + } + return { + ...node, + nodeValue, + explanation: { + depuis: from, + "jusqu'à": to + } + } +} + +export default (recurse, k, v) => { + const explanation = parseObject(recurse, objectShape, v) + + return { + evaluate, + // eslint-disable-next-line + jsx: (nodeValue, explanation, _, unit) => ( + + ), + explanation, + category: 'mecanism', + name: 'Durée', + type: 'numeric', + unit: parseUnit('jours') + } +} diff --git a/source/engine/mecanisms/operation.js b/source/engine/mecanisms/operation.js index 80987985a..51294586c 100644 --- a/source/engine/mecanisms/operation.js +++ b/source/engine/mecanisms/operation.js @@ -43,9 +43,22 @@ export default (k, operatorFunction, symbol) => (recurse, k, v) => { ) } } - let nodeValue = operatorFunction( - ...convertToDateIfNeeded(node1.nodeValue, node2.nodeValue) - ) + let node1Value = node1.nodeValue + let node2Value = node2.nodeValue + try { + ;[node1Value, node2Value] = convertToDateIfNeeded( + node1.nodeValue, + node2.nodeValue + ) + } catch (e) { + typeWarning( + cache._meta.contextRule, + `Impossible de convertir une des valeur en date`, + e + ) + } + let nodeValue = operatorFunction(node1Value, node2Value) + let unit = inferUnit(k, [node1.unit, node2.unit]) return { ...node, diff --git a/source/engine/parse.js b/source/engine/parse.js index b3aebc166..1c353cdc8 100644 --- a/source/engine/parse.js +++ b/source/engine/parse.js @@ -6,6 +6,7 @@ import { formatValue } from 'Engine/format' import barème from 'Engine/mecanisms/barème' import barèmeContinu from 'Engine/mecanisms/barème-continu' import barèmeLinéaire from 'Engine/mecanisms/barème-linéaire' +import durée from 'Engine/mecanisms/durée' import encadrement from 'Engine/mecanisms/encadrement' import operation from 'Engine/mecanisms/operation' import variations from 'Engine/mecanisms/variations' @@ -143,6 +144,7 @@ export let parseObject = (rules, rule, parsedRules) => rawNode => { 'barème linéaire': barèmeLinéaire, 'barème continu': barèmeContinu, encadrement, + durée, 'le maximum de': mecanismMax, 'le minimum de': mecanismMin, complément: mecanismComplement, diff --git a/source/engine/possibleVariableTypes.yaml b/source/engine/possibleVariableTypes.yaml deleted file mode 100644 index 10aba1bfb..000000000 --- a/source/engine/possibleVariableTypes.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Ce fichier n'est que temporaire et remplace une vraie définition de types -- cotisation -- aide -- indemnité -- salaire -- taxe diff --git a/source/engine/rules.js b/source/engine/rules.js index 980d427e7..bf7b88bc4 100644 --- a/source/engine/rules.js +++ b/source/engine/rules.js @@ -5,7 +5,6 @@ import { dropLast, filter, fromPairs, - has, is, isNil, join, @@ -30,7 +29,6 @@ import translations from 'Règles/externalized.yaml' // TODO - should be in UI, not engine import { capitalise0, coerceArray } from '../utils' import { syntaxError, warning } from './error' -import possibleVariableTypes from './possibleVariableTypes.yaml' /*********************************** Functions working on one rule */ @@ -55,7 +53,7 @@ export let enrichRule = rule => { ...rule, dottedName, name, - type: possibleVariableTypes.find(t => has(t, rule) || rule.type === t), + type: rule.type, title: capitalise0(rule['titre'] || name), defaultValue: rule['par défaut'], examples: rule['exemples'], diff --git a/source/engine/units.ts b/source/engine/units.ts index 16fd87048..870c72005 100644 --- a/source/engine/units.ts +++ b/source/engine/units.ts @@ -258,7 +258,6 @@ export function areUnitConvertible(a: Unit, b: Unit) { flatten, uniq )([numA, denomA, numB, denomB]) - return unitClasses.every( unitClass => (numA[unitClass] || 0) - (denomA[unitClass] || 0) === diff --git a/source/locales/units.yaml b/source/locales/units.yaml index 22d596d4d..1d7e1d7af 100644 --- a/source/locales/units.yaml +++ b/source/locales/units.yaml @@ -3,6 +3,7 @@ fr: jour_plural: jours semaine_plural: semaines trimestre_plural: trimestres + an_plural: ans employé_plural: employés point_plural: points en: @@ -18,3 +19,5 @@ en: repas_plural: meals employé: employee employé_plural: employees + an: year + an_plural: years diff --git a/source/règles/base.yaml b/source/règles/base.yaml index 74562d85a..1ece2288d 100644 --- a/source/règles/base.yaml +++ b/source/règles/base.yaml @@ -1,5 +1,5 @@ -année courante: - formule: 2019 +début d'année: + formule: 01/01/2019 période: période . jours ouvrés moyen par mois: @@ -3149,23 +3149,40 @@ impôt . CEHR: Bofip.impots.gouv.fr: http://bofip.impots.gouv.fr/bofip/7804-PGP entreprise . date de création: - question: En quelle année avez-vous créé votre entreprise ? - par défaut: 2019 + question: Quand avez-vous créé votre entreprise ? + par défaut: 01/01/2019 suggestions: - 2019: 2019 - 2018: 2018 - 2017: 2017 - unité: " " + Décembre 2019: 01/12/2019 + Janvier 2019: 01/01/2019 + Janvier 2018: 01/01/2018 + type: date contrôles: - - si: date de création > 2020 + - si: date de création > 01/2020 niveau: avertissement message: Nous ne pouvons voir aussi loin dans le futur - - si: date de création < 1900 + - si: date de création < 01/1900 niveau: avertissement message: Il s'agit d'une très vieille entreprise ! Êtes-vous sûr de ne pas vous être trompé dans la saisie ? -entreprise . année d'activité: - formule: année courante - date de création +entreprise . durée d'activité: + formule: + durée: + depuis: date de création + +entreprise . durée d'activité . en fin d'année: + titre: durée d'activité à la fin de l'année + formule: + durée: + depuis: date de création + jusqu'à: 31/12/2019 + +entreprise . durée d'activité . en début d'année: + titre: durée d'activité au début de l'année + formule: + durée: + depuis: date de création + jusqu'à: 01/01/2019 + entreprise . chiffre d'affaires: titre: chiffre d'affaires (H.T.) @@ -3271,18 +3288,42 @@ entreprise . charges: par défaut: 0 unité par défaut: €/an -dirigeant . indépendant . cotisations et contributions . réduction ACRE: +dirigeant . indépendant . cotisations et contributions . exonérations . ACRE: applicable si: entreprise . ACRE formule: multiplication: assiette: cotisations - cotisations . retraite complémentaire - taux: taux ACRE + taux: taux + facteur: prorata sur l'année -dirigeant . indépendant . cotisations et contributions . réduction ACRE . taux ACRE: + +dirigeant . indépendant . cotisations et contributions . exonérations . ACRE . PSS proratisé: + formule: + multiplication: + assiette: plafond sécurité sociale temps plein + taux: + encadrement: + valeur: entreprise . durée d'activité . en fin d'année / 1 an + plafond: 100% + +dirigeant . indépendant . cotisations et contributions . exonérations . ACRE . prorata sur l'année: + description: | + Comme le calcul des cotisations indépendants s'effectue sur l'année entière, + l'exonération est proratisée en fonction de la durée effective de l'ACRE sur l'année courante. + + Par exemple, pour une entreprise crée le 1 fevrier 2018, le calcul du prorata pour les + cotisations 2019 sera le suivant : + + `31 jours d'acre restant en 2019 / 365 jours = 8,5%` + + formule: (1 an - entreprise . durée d'activité . en début d'année) / 1 an + + +dirigeant . indépendant . cotisations et contributions . exonérations . ACRE . taux: formule: barème continu: assiette: revenu professionnel - multiplicateur: plafond sécurité sociale temps plein + multiplicateur: PSS proratisé points: 0: 100% 0.75: 100% @@ -3294,7 +3335,7 @@ dirigeant . indépendant . revenu net de cotisations: somme: - revenu professionnel - situation personnelle . IJSS . défiscalisées - - (- cotisations et contributions . CSG et CRDS [non déductible]) + - (- cotisations et contributions . CSG et CRDS .non déductible) résumé: Avant impôt question: Quel revenu avant impôt voulez-vous toucher ? description: Il s'agit du revenu net de cotisations et de charges, avant le paiement de l'impôt sur le revenu. @@ -3682,7 +3723,6 @@ dirigeant . indépendant . cotisations et contributions: - CSG et CRDS - formation professionnelle - conjoint collaborateur . cotisations - - (- réduction ACRE) - (- exonérations) unité par défaut: €/an @@ -3721,11 +3761,11 @@ dirigeant . rattachement CIPAV: - entreprise . catégorie d'activité . libérale règlementée - toutes ces conditions: - dirigeant = 'indépendant' - - entreprise . date de création < 2019 + - entreprise . date de création < 01/2019 - entreprise . catégorie d'activité = 'libérale' - toutes ces conditions: - dirigeant = 'auto-entrepreneur' - - entreprise . date de création < 2018 + - entreprise . date de création < 01/2018 - entreprise . catégorie d'activité = 'libérale' rend non applicable: @@ -3739,7 +3779,7 @@ dirigeant . indépendant . PLNR régime général: toutes ces conditions: - entreprise . catégorie d'activité = 'libérale' - entreprise . catégorie d'activité . libérale règlementée = non - - entreprise . date de création < 2019 + - entreprise . date de création < 01/2019 question: Avez-vous opté pour le rattachement au régime général des indépendant ? description: En tant que profession libéral non reglementée, vous pouvez choisir d'être rattaché au régime général plutôt que la CIPAV rend non applicable: rattachement CIPAV @@ -3953,7 +3993,6 @@ dirigeant . indépendant . cotisations et contributions . cotisations . retraite taux: 8% dirigeant . indépendant . cotisations et contributions . cotisations . invalidité et décès: - non applicable si: exonérations . âge formule: multiplication: assiette: assiette @@ -4058,46 +4097,50 @@ dirigeant . indépendant . cotisations et contributions . cotisations . allocati 1.1: 0% 1.4: 3.1% -entreprise . ZFU: - question: Votre entreprise bénéficie-t-elle du dispositif zone franche urbaine (ZFU) ? +établissement . ZFU: + applicable si: entreprise . date de création < 01/2015 + question: Votre établissement bénéficie-t-il du dispositif zone franche urbaine (ZFU) ? par défaut: non +établissement . ZFU . durée d'implantation en fin d'année: + formule: + durée: + depuis: entreprise . date de création + jusqu'à: 31/12/2019 + dirigeant . indépendant . cotisations et contributions . exonérations: période: flexible formule: somme: - ZFU - + - ACRE + dirigeant . indépendant . cotisations et contributions . exonérations . ZFU: - applicable si: - toutes ces conditions: - - entreprise . date de création < 2015 - - entreprise . ZFU + applicable si: établissement . ZFU formule: multiplication: - assiette: - le minimum de: - - cotisations . maladie . assiette [annuel] - - 3042 * SMIC horaire + assiette: cotisations . maladie + # TODO : ceci n'est pas bon (le plafond est sur le revenu exonéré, et est proratisé en début / fin d'éxo) + plafond: 3042 heures/an * SMIC horaire taux: taux dirigeant . indépendant . cotisations et contributions . exonérations . âge: question: Bénéficiez-vous du dispositif d'exonération "âge" description: Ce dispositif a été arrêté en 2015, mais est toujours actif pour les personnes qui en bénéficiait avant son abbrogation. - applicable si: entreprise . date de création < 2016 par défaut: non + applicable si: entreprise . date de création < 01/2016 + rend non applicable: cotisations . invalidité et décès dirigeant . indépendant . cotisations et contributions . exonérations . invalidité: question: Êtes-vous titulaire d’une pension d’invalidité du régime des travailleurs indépendants ? description: Les personnes titulaires d’une pension d’invalidité versée par un régime des travailleurs non-salariés non agricoles bénéficient d’une exonération totale des cotisations maladie et retraite complémentaire. par défaut: non rend non applicable: + - exonérations . ZFU - cotisations . maladie - cotisations . indemnités journalières maladie - cotisations . retraite complémentaire - - situation personnelle . IJSS: titre: indemnités journalières de sécurité sociale description: | @@ -4154,43 +4197,34 @@ situation personnelle . IJSS . total: somme: - fiscalisées - défiscalisées - - dirigeant . indépendant . cotisations et contributions . exonérations . ZFU . taux: titre: taux exonération ZFU formule: - variations: - - si: entreprise . effectif < 5 - alors: - barème linéaire: - assiette: entreprise . année d'activité - retourne seulement le taux: oui - tranches: - - en-dessous de: 6 - taux: 100% - - de: 6 - à: 10 - taux: 60% - - de: 11 - à: 12 - taux: 40% - - de: 13 - à: 14 - taux: 20% - - au-dessus de: 14 - taux: 0% - - sinon: - variations: - - si: entreprise . année d'activité <= 5 - alors: 100% - - si: entreprise . année d'activité = 6 - alors: 60% - - si: entreprise . année d'activité = 7 - alors: 40% - - si: entreprise . année d'activité = 8 - alors: 20% - - sinon: 0% + barème continu: + assiette: établissement . ZFU . durée d'implantation en fin d'année [an] + retourne seulement le taux: oui + variations: + - si: entreprise . effectif + alors: + points: + 0: 100% + 5: 100% + 6: 60% + 10: 60% + 11: 40% + 12: 40% + 13: 20% + 14: 20% + 15: 0% + - sinon: + points: + 0: 100% + 5: 100% + 6: 60% + 7: 40% + 8: 20% + 9: 0% situation personnelle . domiciliation fiscale à l'étranger: description: | @@ -4465,46 +4499,30 @@ entreprise . ACRE: une de ces conditions: - toutes ces conditions: - dirigeant = 'auto-entrepreneur' - - entreprise . année d'activité < 4 - - entreprise . année d'activité < 2 + - entreprise . durée d'activité < 3 ans + - entreprise . date de création < 01/01/2020 + - entreprise . durée d'activité . en début d'année < 1 an par défaut: non note: Les auto-entreprises crées entre le 1er janvier et le 31 décembre 2019 bénéficient d'un dispositif plus favorable, actif pendant 3 années. -entreprise . ACRE . année: - question: Quel est l'âge de l'entreprise ? - formule: - une possibilité: - choix obligatoire: oui - possibilités: - - moins d'un an - - moins de deux ans - - moins de trois ans - par défaut: moins d'un an - -entreprise . ACRE . année . moins d'un an: - -entreprise . ACRE . année . moins de deux ans: - -entreprise . ACRE . année . moins de trois ans: - dirigeant . auto-entrepreneur . cotisations et contributions . cotisations . taux ACRE: formule: 100% - réduction ACRE dirigeant . auto-entrepreneur . cotisations et contributions . cotisations . réduction ACRE: titre: réduction ACRE applicable si: entreprise . ACRE - description: Ce taux peut dans certains cas réduire le montant des cotisations sociales de l'auto-entrepreneur pour l'aider dans ses premières année d'activité. + description: Ce taux peut dans certains cas réduire le montant des cotisations sociales de l'auto-entrepreneur pour l'aider dans ses premières années d'activité. formule: variations: # TODO : ce code fonctionne en 2020, mais en 2021 il faudra faire la # distinction entre les entreprises créées en 2019 (qui ont le droit à # l'ACRE pendant 3 ans) et celles créées en 2020 et après qui n'ont le # droit qu'à une année de réduction. - - si: entreprise . ACRE . année = 'moins d'un an' + - si: entreprise . durée d'activité < 1 an alors: 50% - - si: entreprise . ACRE . année = 'moins de deux ans' + - si: entreprise . durée d'activité < 2 ans alors: 25% - - si: entreprise . ACRE . année = 'moins de trois ans' + - si: entreprise . durée d'activité < 3 ans alors: 10% références: Fiche URSSAF: https://www.urssaf.fr/portail/home/indépendant/je-beneficie-dexonerations/accre.html diff --git a/test/units.test.js b/test/units.test.js index 75ab82ebb..1ae3a26f1 100644 --- a/test/units.test.js +++ b/test/units.test.js @@ -119,6 +119,7 @@ describe('convertUnit', () => { describe('areUnitConvertible', () => { it('should be true for temporel unit', () => { expect(areUnitConvertible(parseUnit('mois'), parseUnit('an'))).to.eq(true) + expect(areUnitConvertible(parseUnit('jours'), parseUnit('ans'))).to.eq(true) expect(areUnitConvertible(parseUnit('kg/an'), parseUnit('kg/mois'))).to.eq( true ) diff --git a/yarn.lock b/yarn.lock index 724dbdb23..dd53d7df0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2813,6 +2813,11 @@ clean-css@4.2.x: dependencies: source-map "~0.6.0" +cleave.js@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/cleave.js/-/cleave.js-1.5.3.tgz#1ef36e1375dea289bffeefe8ebb1e3ef2ee23240" + integrity sha512-tLum0abk+NRUZtMxpqLQS0jE9k2KYzsUlnMAqfMd2LAf8bb5xTHLHXTtPyI+dCk6DThtmWER3EzKfUi4NHdR7A== + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"