From 9cbbac6861567731871919c31e88435259f653cd Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Fri, 8 Jan 2021 14:34:37 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Simulateur=20d'imp=C3=B4t=20sur=20l?= =?UTF-8?q?es=20soci=C3=A9t=C3=A9s=20(#1230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../règles/entreprise-établissement.yaml | 145 +++++++++++++++++- .../source/components/SimulationGoals.tsx | 92 +++++++++++ .../components/conversation/DateInput.tsx | 3 + .../components/conversation/RuleInput.tsx | 4 +- .../source/components/utils/EngineContext.tsx | 3 + .../source/components/utils/colors.tsx | 14 +- mon-entreprise/source/locales/rules-en.yaml | 122 +++++++++++---- mon-entreprise/source/locales/ui-en.yaml | 30 +++- mon-entreprise/source/reducers/rootReducer.ts | 5 +- .../source/site/pages/Documentation.tsx | 42 ++--- .../source/site/pages/Gérer/Home.tsx | 21 +++ .../site/pages/Simulateurs/ArtisteAuteur.tsx | 93 ++--------- .../source/site/pages/Simulateurs/Home.tsx | 1 + .../site/pages/Simulateurs/ISSimulation.tsx | 128 ++++++++++++++++ .../source/site/pages/Simulateurs/Page.tsx | 8 +- .../site/pages/Simulateurs/metadata.tsx | 55 +++++++ mon-entreprise/source/site/sitePaths.ts | 2 + .../__snapshots__/simulations.jest.js.snap | 23 +++ .../simulations-impôt-société.yaml | 17 ++ .../test/regressions/simulations.jest.js | 8 + publicodes/core/source/index.ts | 3 +- .../ui-react/source/mecanisms/common.tsx | 41 ++--- 22 files changed, 696 insertions(+), 164 deletions(-) create mode 100644 mon-entreprise/source/components/SimulationGoals.tsx create mode 100644 mon-entreprise/source/site/pages/Simulateurs/ISSimulation.tsx create mode 100644 mon-entreprise/test/regressions/simulations-impôt-société.yaml diff --git a/modele-social/règles/entreprise-établissement.yaml b/modele-social/règles/entreprise-établissement.yaml index e76184cd1..64cc0bf4a 100644 --- a/modele-social/règles/entreprise-établissement.yaml +++ b/modele-social/règles/entreprise-établissement.yaml @@ -112,25 +112,156 @@ entreprise . imposition . IS . notification IS non intégré: le calcul de l'impôt sur le revenu. entreprise . bénéfice: + titre: Bénéfice de l'exercice + résumé: Imposable à l'impôt sur les sociétés formule: chiffre d'affaires - charges dont rémunération dirigeant +entreprise . bénéfice . information sur le report de déficit: + type: notification + formule: bénéfice < 0 €/an + # TODO: Support des références dans les notifications + description: | + Les déficits subits au cours d'un exercice peuvent être reportés sur les exercices suivants (report en avant), ou sur le seul exercice précédent (report en arrière). + + [Plus d'infos sur service-public.fr](https://www.service-public.fr/professionnels-entreprises/vosdroits/F23628) + entreprise . résultat net: résumé: Ce qu'il reste après impôt sur les sociétés formule: bénéfice - impôt sur les sociétés +entreprise . exercice: + +entreprise . exercice . début: + type: date + par défaut: 01/01/2020 + +entreprise . exercice . fin: + type: date + par défaut: 31/12/2020 + +entreprise . exercice . durée: + titre: durée de l'exercice + formule: + durée: + depuis: début + jusqu'à: fin + +entreprise . exercice . date trop ancienne: + type: notification + sévérité: avertissement + formule: début < 01/01/2018 + description: La date saisie est trop ancienne. Le simulateur n'intègre pas les barèmes avant 2018. + +entreprise . exercice . date trop éloignée: + type: notification + sévérité: avertissement + formule: fin > 31/12/2022 + description: La date saisie est trop éloignée. Le simulateur n'intègre pas les barèmes au delà de 2022. + +entreprise . exercice . début après la fin: + type: notification + sévérité: avertissement + formule: début >= fin + description: La fin de l'exercice doit être postérieure à son début. + +entreprise . exercice . durée maximale: + type: notification + sévérité: avertissement + formule: durée >= 24 mois + description: La durée maximale d'un exercice comptable est de 24 mois. + entreprise . impôt sur les sociétés: unité: €/an formule: barème: assiette: bénéfice - tranches: - - taux: 15% - plafond: 38120 €/an - - taux: 28% - plafond: 500000 €/an - - taux: 33.3% + multiplicateur: prorata temporis + variations: + - si: exercice . début >= 01/01/2022 + alors: + tranches: + - taux: 15% + plafond: plafond taux réduit 1 + - taux: 25% + - si: exercice . début >= 01/01/2021 + alors: + tranches: + - taux: 15% + plafond: plafond taux réduit 1 + - taux: 26.5% + - si: exercice . début >= 01/01/2020 + alors: + tranches: + - taux: 15% + plafond: plafond taux réduit 1 + - taux: 28% + - si: exercice . début >= 01/01/2019 + alors: + tranches: + - taux: 15% + plafond: plafond taux réduit 1 + - taux: 28% + plafond: plafond taux réduit 2 + - taux: 31% + - si: exercice . début >= 01/01/2018 + alors: + tranches: + - taux: 15% + plafond: plafond taux réduit 1 + - taux: 28% + plafond: plafond taux réduit 2 + - taux: 33.3333% + arrondi: oui références: - fiche service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23575 + Fiche impots.gouv.fr: https://www.impots.gouv.fr/portail/international-professionnel/impot-sur-les-societes + Fiche service-public.fr: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23575 + +entreprise . impôt sur les sociétés . plafond taux réduit 1: + applicable si: éligible taux réduit + valeur: 38120 €/an + +entreprise . impôt sur les sociétés . plafond taux réduit 2: + applicable si: éligible taux réduit + valeur: 500000 €/an + +entreprise . impôt sur les sociétés . éligible taux réduit: + formule: + toutes ces conditions: + - chiffre d'affaires <= 7630 k€/an * prorata temporis + - nom: capital détenu au moins à 75 pourcents par des personnes physiques + valeur: oui + +entreprise . impôt sur les sociétés . prorata temporis: + description: | + Lorsque la durée de l’exercice n'est pas égale à un an, on pro-ratise les + plafonds utilisés dans le barème de l'impôt sur les sociétés. + unité: '%' + formule: exercice . durée / 1 an + # TODO: c'est un peu plus subtil que cela : « En cas d’exercice ouvert ou + # arrêté en cours de mois calendaire, le nombre de jours résiduels concourt à + # la détermination du rapport pour un montant égal au rapport existant entre + # ce nombre et 30. » + références: + Bofip: https://bofip.impots.gouv.fr/bofip/2065-PGP.html/identifiant%3DBOI-IS-LIQ-20-20-20180801 + +entreprise . impôt sur les sociétés . contribution sociale: + # description: | + # La contribution sociale sur les bénéfices est un impôt distinct de l'impôt sur les sociétés. Son montant n'est pas déductible des résultats. + + # L'assiette bénéficie d'un abattement important, et seules les entreprises réalisant plus de 2,3 millions d'euros de bénéficie sont concernées par cette contribution. + description: | + La contribution sociale sur les bénéfices est un impôt distinct de l’impôt sur les sociétés. Son montant n’est pas déductible des résultats. + + L’assiette bénéficie d’un abattement important, et seules les entreprises réalisant plus de 2,3 millions d’euros de bénéfices sont concernées par cette contribution. + formule: + produit: + taux: 3.3% + assiette: + allègement: + assiette: impôt sur les sociétés + abattement: 763000 €/an * prorata temporis + références: + Bofip: https://bofip.impots.gouv.fr/bofip/3492-PGP.html/identifiant%3DBOI-IS-AUT-10-20-20130318 entreprise . charges dont rémunération dirigeant: formule: charges + dirigeant . rémunération totale diff --git a/mon-entreprise/source/components/SimulationGoals.tsx b/mon-entreprise/source/components/SimulationGoals.tsx new file mode 100644 index 000000000..f6a8759da --- /dev/null +++ b/mon-entreprise/source/components/SimulationGoals.tsx @@ -0,0 +1,92 @@ +import { updateSituation } from 'Actions/actions' +import { DottedName } from 'modele-social' +import { UNSAFE_isNotApplicable } from 'publicodes' +import { useDispatch, useSelector } from 'react-redux' +import { situationSelector } from 'Selectors/simulationSelectors' +import RuleInput, { RuleInputProps } from './conversation/RuleInput' +import { useEngine } from './utils/EngineContext' +import Animate from 'Components/ui/animate' +import { useState, useEffect, createContext, useContext } from 'react' + +type SimulationGoalsProps = { + className?: string + children: React.ReactNode +} + +const InitialRenderContext = createContext(false) + +export function SimulationGoals({ + className = '', + children, +}: SimulationGoalsProps) { + const initialRender = useInitialRender() + + return ( + +
+
+
    {children}
+
+
+
+ ) +} + +function useInitialRender() { + const [initialRender, setInitialRender] = useState(true) + useEffect(() => { + setInitialRender(false) + }, []) + return initialRender +} + +type SimulationGoalProps = { + dottedName: DottedName +} & Omit + +export function SimulationGoal({ + dottedName, + ...otherProps +}: SimulationGoalProps) { + const dispatch = useDispatch() + const engine = useEngine() + const situation = useSelector(situationSelector) + const isNotApplicable = UNSAFE_isNotApplicable(engine, dottedName) + const evaluation = engine.evaluate(dottedName) + const rule = engine.getRule(dottedName) + const initialRender = useContext(InitialRenderContext) + if ( + isNotApplicable === true || + (!(dottedName in situation) && + evaluation.nodeValue === false && + !(dottedName in evaluation.missingVariables)) + ) { + return null + } + + return ( +
  • + +
    +
    + +
    +
    + dispatch(updateSituation(dottedName, x))} + useSwitch + /> +
    +
    +
    +
  • + ) +} diff --git a/mon-entreprise/source/components/conversation/DateInput.tsx b/mon-entreprise/source/components/conversation/DateInput.tsx index 15e994f52..4c9bd3ee9 100644 --- a/mon-entreprise/source/components/conversation/DateInput.tsx +++ b/mon-entreprise/source/components/conversation/DateInput.tsx @@ -13,6 +13,7 @@ type DateInputProps = { onSubmit: RuleInputProps['onSubmit'] value: InputCommonProps['value'] suggestions: RuleNode['suggestions'] + required: RuleInputProps['required'] } export default function DateInput({ @@ -20,6 +21,7 @@ export default function DateInput({ onChange, id, onSubmit, + required, value, }: DateInputProps) { const dateValue = useMemo(() => { @@ -64,6 +66,7 @@ export default function DateInput({ id={id} type="date" value={dateValue} + required={required} onChange={handleDateChange} /> diff --git a/mon-entreprise/source/components/conversation/RuleInput.tsx b/mon-entreprise/source/components/conversation/RuleInput.tsx index d48730ff3..e20909084 100644 --- a/mon-entreprise/source/components/conversation/RuleInput.tsx +++ b/mon-entreprise/source/components/conversation/RuleInput.tsx @@ -24,6 +24,7 @@ export type RuleInputProps = { useSwitch?: boolean isTarget?: boolean autoFocus?: boolean + required?: boolean id?: string className?: string onSubmit?: (source: string) => void @@ -58,6 +59,7 @@ export default function RuleInput({ id, isTarget = false, autoFocus = false, + required = true, className, onSubmit = () => null, }: RuleInputProps) { @@ -74,11 +76,11 @@ export default function RuleInput({ onChange, autoFocus, className, + required, title: rule.title, id: id ?? dottedName, question: rule.rawNode.question, suggestions: rule.suggestions, - required: true, } if (getVariant(engine.getRule(dottedName))) { return ( diff --git a/mon-entreprise/source/components/utils/EngineContext.tsx b/mon-entreprise/source/components/utils/EngineContext.tsx index 68959ba46..16318ce92 100644 --- a/mon-entreprise/source/components/utils/EngineContext.tsx +++ b/mon-entreprise/source/components/utils/EngineContext.tsx @@ -15,6 +15,9 @@ export const engineOptions = { .replace(/_plural$/, '') return key || unit }, + formatUnit(unit: string, count: number): string { + return i18n?.t(`units:${unit}`, { count }) + }, } export function useEngine(): Engine { diff --git a/mon-entreprise/source/components/utils/colors.tsx b/mon-entreprise/source/components/utils/colors.tsx index f4bfea533..957d0dd43 100644 --- a/mon-entreprise/source/components/utils/colors.tsx +++ b/mon-entreprise/source/components/utils/colors.tsx @@ -1,5 +1,5 @@ import convert from 'color-convert' -import React, { createContext, useEffect, useRef } from 'react' +import React, { createContext, useContext, useEffect, useRef } from 'react' /* Hex to RGB conversion: @@ -55,9 +55,9 @@ const deriveAnalogousPalettes = (hex: string) => { ] } -const generateTheme = (themeColor?: string) => { +export const generateTheme = (themeColor: string) => { const // Use the default theme color if the host page hasn't made a choice - color = themeColor || '#2975D1', + color = themeColor, lightColor = lightenColor(color, 10), darkColor = lightenColor(color, -20), lighterColor = lightenColor(color, 45), @@ -90,9 +90,13 @@ const generateTheme = (themeColor?: string) => { } } +const defaultColor = '#2975D1' + export type ThemeColors = ReturnType -export const ThemeColorsContext = createContext(generateTheme()) +export const ThemeColorsContext = createContext( + generateTheme(defaultColor) +) type ProviderProps = { color?: string @@ -100,7 +104,7 @@ type ProviderProps = { } export function ThemeColorsProvider({ color, children }: ProviderProps) { - const colors = generateTheme(color) + const colors = generateTheme(color ?? useContext(ThemeColorsContext).color) const divRef = useRef(null) useEffect(() => { Object.entries(colors).forEach(([key, value]) => { diff --git a/mon-entreprise/source/locales/rules-en.yaml b/mon-entreprise/source/locales/rules-en.yaml index 834ccb891..ba5f8d50e 100644 --- a/mon-entreprise/source/locales/rules-en.yaml +++ b/mon-entreprise/source/locales/rules-en.yaml @@ -2580,8 +2580,6 @@ contrat salarié . prix du travail: résumé.fr: Dépensé par l'entreprise titre.en: labor cost titre.fr: Coût total - identifiant court.en: employee-laborcost - identifiant court.fr: salarie-coutembauche contrat salarié . profession spécifique: question.en: '[automatic] Does the employee work in one of the following professions?' question.fr: Le salarié exerce t-il l'une des professions suivantes ? @@ -3046,8 +3044,6 @@ contrat salarié . rémunération . brut de base: suggestions.salaire médian.fr: salaire médian titre.en: Gross salary titre.fr: Salaire brut - identifiant court.en: employee-brut - identifiant court.fr: salarie-brut contrat salarié . rémunération . brut de base . équivalent temps plein: question.en: What is the full-time equivalent salary? question.fr: Quel est le salaire en équivalent temps plein ? @@ -3105,8 +3101,6 @@ contrat salarié . rémunération . net: résumé.fr: Salaire net avant impôt titre.en: Net salary titre.fr: Salaire net - identifiant court.en: employee-net - identifiant court.fr: salarie-net contrat salarié . rémunération . net après impôt: description.en: >- Net salary after deduction of the **neutral** income tax (also called @@ -3132,8 +3126,6 @@ contrat salarié . rémunération . net après impôt: résumé.fr: Versé sur le compte bancaire titre.en: Net salary after income tax titre.fr: Salaire net après impôt - identifiant court.en: employee-netaftertax - identifiant court.fr: salarie-netapresimpot contrat salarié . rémunération . net avec revenus de remplacement: titre.en: '[automatic] net with replacement income' titre.fr: net avec revenus de remplacement @@ -3292,8 +3284,6 @@ contrat salarié . rémunération . total: résumé.fr: Dépensé par l'entreprise titre.en: Total salary titre.fr: Total chargé - identifiant court.fr: salarie-total - identifiant court.en: employee-total contrat salarié . salarié majeur: question.en: Is the employee over 18 (age of majority)? question.fr: Le salarié est-il majeur ? @@ -3989,8 +3979,6 @@ dirigeant . auto-entrepreneur . net après impôt: résumé.fr: Avant déduction des dépenses liées à l'activité titre.en: net income after tax titre.fr: revenu net après impôt - identifiant court.en: auto-entrepreneur-netaftertax - identifiant court.fr: auto-entrepreneur-netapresimpot dirigeant . auto-entrepreneur . net de cotisations: description.en: This is the income net of contributions and expenses, before the @@ -4003,8 +3991,6 @@ dirigeant . auto-entrepreneur . net de cotisations: résumé.fr: Avant impôt titre.en: Net contribution income titre.fr: Revenu net de cotisations - identifiant court.en: auto-entrepreneur-net - identifiant court.fr: auto-entrepreneur-net dirigeant . auto-entrepreneur . notification calcul ACRE annuel: description.en: > [automatic] The ACRE rate used is the one corresponding to the current @@ -5776,8 +5762,6 @@ dirigeant . indépendant . revenu net de cotisations: résumé.fr: Avant déduction de l'impôt sur le revenu titre.en: net contribution income titre.fr: revenu net de cotisations - identifiant court.en: independant-net - identifiant court.fr: independant-net dirigeant . indépendant . revenu professionnel: description.en: > This is the income net of deductible contributions of the self-employed @@ -5858,8 +5842,6 @@ dirigeant . rémunération totale: résumé.fr: Dépensé par l'entreprise titre.en: Director total income titre.fr: rémunération totale - identifiant court.en: director-total - identifiant court.fr: dirigeant-total entreprise: description.en: The contract binds a company and an employee description.fr: | @@ -5951,8 +5933,27 @@ entreprise . auto entreprise impossible: titre.en: auto enterprise impossible titre.fr: auto entreprise impossible entreprise . bénéfice: - titre.en: profit - titre.fr: bénéfice + résumé.en: '[automatic] Taxable for corporate income tax purposes' + résumé.fr: Imposable à l'impôt sur les sociétés + titre.en: '[automatic] Profit for the year' + titre.fr: Bénéfice de l'exercice +entreprise . bénéfice . information sur le report de déficit: + description.en: > + [automatic] Deficits incurred in one year can be carried forward to + subsequent years (carry-forward), or carried back to the previous year only + (carry-back). + + + [More info on service-public.fr](https://www.service-public.fr/professionnels-entreprises/vosdroits/F23628) + description.fr: > + Les déficits subits au cours d'un exercice peuvent être reportés sur les + exercices suivants (report en avant), ou sur le seul exercice précédent + (report en arrière). + + + [Plus d'infos sur service-public.fr](https://www.service-public.fr/professionnels-entreprises/vosdroits/F23628) + titre.en: '[automatic] deficit carry forward information' + titre.fr: information sur le report de déficit entreprise . catégorie d'activité: description.en: Your activity category will determine a large part of the calculations of contribution, contribution and tax. @@ -6164,8 +6165,6 @@ entreprise . charges: résumé.fr: Toutes les dépenses nécessaires à l'entreprise titre.en: expenses titre.fr: charges de fonctionnement - identifiant court.en: company-expenses - identifiant court.fr: entreprise-charges entreprise . charges dont rémunération dirigeant: titre.en: expenses of which executive compensation titre.fr: charges dont rémunération dirigeant @@ -6176,8 +6175,6 @@ entreprise . chiffre d'affaires: résumé.fr: Montant total des recettes brutes (hors taxe) titre.en: '[automatic] revenues' titre.fr: chiffre d'affaires - identifiant court.en: company-turnover - identifiant court.fr: entreprise-ca entreprise . chiffre d'affaires de société: titre.en: company turnover titre.fr: chiffre d'affaires de société @@ -6191,8 +6188,6 @@ entreprise . chiffre d'affaires minimum: question.fr: Quel est votre chiffre d'affaires minimum envisagé ? titre.en: Minimum turnover titre.fr: chiffre d'affaires minimum - identifiant court.en: company-turnover-min - identifiant court.fr: entreprise-ca-min entreprise . date de création: description.en: > [automatic] The activity start date (or creation date) is set at the time of @@ -6286,6 +6281,44 @@ entreprise . effectif . seuil . moins de 50: entreprise . effectif . seuil . plus de 250: titre.en: '[automatic] 251 and more' titre.fr: 251 et plus +entreprise . exercice: + titre.en: '[automatic] exercise' + titre.fr: exercice +entreprise . exercice . date trop ancienne: + description.en: + '[automatic] The date entered is too old. The simulator does not + integrate the scales until 2018.' + description.fr: La date saisie est trop ancienne. Le simulateur n'intègre pas + les barèmes avant 2018. + titre.en: '[automatic] too old date' + titre.fr: date trop ancienne +entreprise . exercice . date trop éloignée: + description.en: '[automatic] The date entered is too far in the future. The + simulator does not integrate scales beyond 2022.' + description.fr: La date saisie est trop éloignée. Le simulateur n'intègre pas + les barèmes au delà de 2022. + titre.en: '[automatic] date too far in the future' + titre.fr: date trop éloignée +entreprise . exercice . durée: + titre.en: '[automatic] fiscal year' + titre.fr: durée de l'exercice +entreprise . exercice . durée maximale: + description.en: '[automatic] The maximum length of a fiscal year is 24 months.' + description.fr: La durée maximale d'un exercice comptable est de 24 mois. + titre.en: '[automatic] maximal duration' + titre.fr: durée maximale +entreprise . exercice . début: + titre.en: '[automatic] start' + titre.fr: début +entreprise . exercice . début après la fin: + description.en: '[automatic] The end of the fiscal year must be after the + beginning of the fiscal year.' + description.fr: La fin de l'exercice doit être postérieure à son début. + titre.en: '[automatic] start after completion' + titre.fr: début après la fin +entreprise . exercice . fin: + titre.en: '[automatic] end' + titre.fr: fin entreprise . franchise de TVA: description.en: | [automatic] The VAT exemption is a device that exempts businesses from the @@ -6351,6 +6384,41 @@ entreprise . imposition . IS . notification IS non intégré: entreprise . impôt sur les sociétés: titre.en: corporate income tax titre.fr: impôt sur les sociétés +entreprise . impôt sur les sociétés . contribution sociale: + description.en: > + [automatic] The social contribution on profits is a tax separate from the + corporate tax. Its amount is not deductible from profits. + + + The tax base benefits from a significant reduction, and only companies making more than EUR 2,3 million in profits are concerned by this contribution. + description.fr: > + La contribution sociale sur les bénéfices est un impôt distinct de l’impôt + sur les sociétés. Son montant n’est pas déductible des résultats. + + + L’assiette bénéficie d’un abattement important, et seules les entreprises réalisant plus de 2,3 millions d’euros de bénéfices sont concernées par cette contribution. + titre.en: '[automatic] social contribution' + titre.fr: contribution sociale +entreprise . impôt sur les sociétés . plafond taux réduit 1: + titre.en: '[automatic] ceiling reduced rate 1' + titre.fr: plafond taux réduit 1 +entreprise . impôt sur les sociétés . plafond taux réduit 2: + titre.en: '[automatic] ceiling reduced rate 2' + titre.fr: plafond taux réduit 2 +entreprise . impôt sur les sociétés . prorata temporis: + description.en: > + [automatic] When the duration of the fiscal year is not equal to one year, + we pro-rate the + + ceilings used in the corporate tax scale. + description.fr: | + Lorsque la durée de l’exercice n'est pas égale à un an, on pro-ratise les + plafonds utilisés dans le barème de l'impôt sur les sociétés. + titre.en: '[automatic] prorata temporis' + titre.fr: prorata temporis +entreprise . impôt sur les sociétés . éligible taux réduit: + titre.en: '[automatic] eligible reduced rate' + titre.fr: éligible taux réduit entreprise . non assujettie à TVA: description.en: > [automatic] Some businesses are not subject to VAT. @@ -7264,8 +7332,6 @@ revenu net après impôt: résumé.fr: Disponible sur votre compte en banque titre.en: Net income after tax titre.fr: revenu net après impôt - identifiant court.en: netaftertax - identifiant court.fr: netapresimpot revenus net de cotisations: description.en: | l'impôt sur le revenu. diff --git a/mon-entreprise/source/locales/ui-en.yaml b/mon-entreprise/source/locales/ui-en.yaml index 7412e731d..620e02ed1 100644 --- a/mon-entreprise/source/locales/ui-en.yaml +++ b/mon-entreprise/source/locales/ui-en.yaml @@ -93,6 +93,7 @@ Modifier mes réponses: Change my answers Mon entreprise: My company Mon revenu: My income Montant: Amount +Montant de l'impôt sur les sociétés: Amount of corporate income tax Montant des cotisations: Amount of contributions "Nom de l'entreprise ou SIREN ": Company name or SIREN code Non: No @@ -714,6 +715,8 @@ gérer: <0>Estimate the amount spent for hiring <1>Calculate how much your company will have to spend to pay your next employee + is: <0>Estimate the amount of corporate income tax<1>Calculate the amount of + corporation tax based on your gross profit. revenus: > <0>Calculate my net income @@ -747,6 +750,10 @@ gérer: titre: Manage my business heure: hour heures: hours +impotSociété: + exerciceDates: Exercise from <2> to <6> + warning: "This simulator is aimed at <2>“TPE”: it takes into account the + reduced corporate tax rates." imprimer: Print impôt: tax impôt sur le revenu: income tax @@ -1106,6 +1113,26 @@ pages: title: "Independent: Urssaf income simulator" shortname: Independent title: Income simulator for the self-employed + is: + meta: + title: Corporate Tax Simulator + seo: <0>How is corporate tax calculated?<1>Corporate income tax applies to + the profits made by corporations (SA, SAS, SASU, SARL, etc.) and on an + optional basis for certain other kind of companies (EIRL, EURL, SNC, + etc.).<2>It is calculated on the basis of the profits made in France + during the financial year. The duration of a financial year is normally + 12 months but it may be shorter or longer (in particular at the beginning + of activity or on the dissolution of the company). In this case, the tax + scale is scaled according to the length of the fiscal year, which is + taken into account in the simulator by changing the start and end dates + of the fiscal year.<3>Reduced rate and specific regimes<4>“TPE” + with a turnover of less than €7.63 million and 75% of their capital + owned by individuals benefit from a reduced corporate tax rate. This + rate is taken into account on the simulator and it is currently not + possible to simulate ineligibility for the reduced rates.<5>Finally, + there are specific tax regimes with dedicated rates for certain types of + capital gains (transfer of securities, transfer of patents). These + regimes are not included in the simulator. médecin: shortname: Doctor title: Income simulator for private practitioners @@ -1314,8 +1341,7 @@ selectionRégime: urssaf: The figures are indicative and do not replace the actual accounts of the Urssaf, impots.gouv.fr, etc shareSimulation: - banner: "You can share your simulation: <2 onClick={onClick}>Share - link" + banner: "You can share your simulation: <2 onClick={onClick}>Share link" modal: button: Copy link helpText: The link is already selected, you can just hit "copy". diff --git a/mon-entreprise/source/reducers/rootReducer.ts b/mon-entreprise/source/reducers/rootReducer.ts index 950a90f2c..19391c6f0 100644 --- a/mon-entreprise/source/reducers/rootReducer.ts +++ b/mon-entreprise/source/reducers/rootReducer.ts @@ -49,15 +49,16 @@ type QuestionsKind = | 'liste noire' export type SimulationConfig = { - objectifs: + objectifs?: | Array | Array<{ icône: string; nom: string; objectifs: Array }> - 'objectifs cachés': Array + 'objectifs cachés'?: Array situation: Simulation['situation'] bloquant?: Array questions?: Partial>> branches?: Array<{ nom: string; situation: SimulationConfig['situation'] }> 'unité par défaut': string + color?: string } export type Situation = Partial> diff --git a/mon-entreprise/source/site/pages/Documentation.tsx b/mon-entreprise/source/site/pages/Documentation.tsx index 7e5bc5caa..3dc56dc0b 100644 --- a/mon-entreprise/source/site/pages/Documentation.tsx +++ b/mon-entreprise/source/site/pages/Documentation.tsx @@ -5,17 +5,21 @@ import { useEngine } from 'Components/utils/EngineContext' import { ScrollToTop } from 'Components/utils/Scroll' import { SitePathsContext } from 'Components/utils/SitePathsContext' import { Documentation, getDocumentationSiteMap } from 'publicodes-react' -import { useCallback, useContext, useMemo } from 'react' +import { useCallback, useContext, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { Redirect, useLocation } from 'react-router-dom' import { RootState } from 'Reducers/rootReducer' import SearchBar from 'Components/SearchBar' +import { ThemeColorsProvider } from 'Components/utils/colors' export default function RulePage() { const currentSimulation = useSelector( (state: RootState) => !!state.simulation?.url ) + const documentationColor = useSelector( + (state: RootState) => state.simulation?.config.color + ) const engine = useEngine() const documentationPath = useContext(SitePathsContext).documentation.index const { pathname } = useLocation() @@ -34,23 +38,25 @@ export default function RulePage() { return ( -
    - {currentSimulation ? : } - -
    - - {/* */} + +
    + {currentSimulation ? : } + +
    + + {/* */} +
    ) } diff --git a/mon-entreprise/source/site/pages/Gérer/Home.tsx b/mon-entreprise/source/site/pages/Gérer/Home.tsx index cde47b596..d8b8d4973 100644 --- a/mon-entreprise/source/site/pages/Gérer/Home.tsx +++ b/mon-entreprise/source/site/pages/Gérer/Home.tsx @@ -171,6 +171,27 @@ export default function SocialSecurity() { Commencer + +
    {emoji('🗓')}
    + +

    Estimer le montant de l’impôt sur les sociétés

    +

    + Calculez le montant de l'impôt sur les sociétés à partir + de votre bénéfice. +

    +
    +
    + Commencer +
    + )} diff --git a/mon-entreprise/source/site/pages/Simulateurs/ArtisteAuteur.tsx b/mon-entreprise/source/site/pages/Simulateurs/ArtisteAuteur.tsx index cde421c19..489d91949 100644 --- a/mon-entreprise/source/site/pages/Simulateurs/ArtisteAuteur.tsx +++ b/mon-entreprise/source/site/pages/Simulateurs/ArtisteAuteur.tsx @@ -1,109 +1,44 @@ -import { setSimulationConfig, updateSituation } from 'Actions/actions' -import RuleInput from 'Components/conversation/RuleInput' +import { setSimulationConfig } from 'Actions/actions' import { DistributionBranch } from 'Components/Distribution' import Value, { Condition } from 'Components/EngineValue' import SimulateurWarning from 'Components/SimulateurWarning' import AidesCovid from 'Components/simulationExplanation/AidesCovid' +import { SimulationGoal, SimulationGoals } from 'Components/SimulationGoals' import 'Components/TargetSelection.css' import Animate from 'Components/ui/animate' -import { EngineContext, useEngine } from 'Components/utils/EngineContext' +import { EngineContext } from 'Components/utils/EngineContext' import { DottedName } from 'modele-social' -import { UNSAFE_isNotApplicable } from 'publicodes' -import { createContext, useContext, useEffect, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { Trans } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { situationSelector } from 'Selectors/simulationSelectors' import styled from 'styled-components' import config from './configs/artiste-auteur.yaml' -const InitialRenderContext = createContext(false) -function useInitialRender() { - const [initialRender, setInitialRender] = useState(true) - useEffect(() => { - setInitialRender(false) - }, []) - return initialRender -} - export default function ArtisteAuteur() { const dispatch = useDispatch() useEffect(() => { dispatch(setSimulationConfig(config)) }, []) - const initialRender = useInitialRender() return ( <> -
    -
    -
      - - - - - - - - - - -
    -
    -
    + + + + + + + + + + ) } -type SimpleFieldProps = { - dottedName: DottedName -} - -function SimpleField({ dottedName }: SimpleFieldProps) { - const dispatch = useDispatch() - const engine = useEngine() - const situation = useSelector(situationSelector) - const isNotApplicable = UNSAFE_isNotApplicable(engine, dottedName) - const evaluation = engine.evaluate(dottedName) - const rule = engine.getRule(dottedName) - const initialRender = useContext(InitialRenderContext) - if ( - isNotApplicable === true || - (!(dottedName in situation) && - evaluation.nodeValue === false && - !(dottedName in evaluation.missingVariables)) - ) { - return null - } - - return ( -
  • - -
    -
    - -
    -
    - dispatch(updateSituation(dottedName, x))} - useSwitch - /> -
    -
    -
    -
  • - ) -} - type WarningProps = { dottedName: DottedName } diff --git a/mon-entreprise/source/site/pages/Simulateurs/Home.tsx b/mon-entreprise/source/site/pages/Simulateurs/Home.tsx index de9384fee..4a07a43b0 100644 --- a/mon-entreprise/source/site/pages/Simulateurs/Home.tsx +++ b/mon-entreprise/source/site/pages/Simulateurs/Home.tsx @@ -78,6 +78,7 @@ export default function Simulateurs() { Autres outils
    + {language === 'fr' && ( )} diff --git a/mon-entreprise/source/site/pages/Simulateurs/ISSimulation.tsx b/mon-entreprise/source/site/pages/Simulateurs/ISSimulation.tsx new file mode 100644 index 000000000..4a18475e2 --- /dev/null +++ b/mon-entreprise/source/site/pages/Simulateurs/ISSimulation.tsx @@ -0,0 +1,128 @@ +import { setSimulationConfig, updateSituation } from 'Actions/actions' +import RuleInput from 'Components/conversation/RuleInput' +import Value from 'Components/EngineValue' +import Notifications from 'Components/Notifications' +import { SimulationGoal, SimulationGoals } from 'Components/SimulationGoals' +import Animate from 'Components/ui/animate' +import Warning from 'Components/ui/WarningBlock' +import { ThemeColorsContext } from 'Components/utils/colors' +import { useContext, useEffect } from 'react' +import emoji from 'react-easy-emoji' +import { Trans } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { situationSelector } from 'Selectors/simulationSelectors' + +const config = { + color: '', + 'unité par défaut': '€/an', + situation: {}, +} +export default function ISSimulation() { + const dispatch = useDispatch() + const { color } = useContext(ThemeColorsContext) + useEffect(() => { + // HACK The config is mutated to avoid reseting the situation everytime the + // component is loaded. `setSimulationConfig` relies on config object + // equality. The `setSimulationConfig` design should be improved. + config.color = color + dispatch(setSimulationConfig(config)) + }, []) + + return ( + <> + + + Ce simulateur s’adresse aux{' '} + TPE : il prend en compte + les taux réduits de l’impôt sur les sociétés. + + + + + + + + + + ) +} + +function ExerciceDate() { + const dispatch = useDispatch() + return ( +

    + {emoji('📆')}  + + Exercice du{' '} + + dispatch(updateSituation('entreprise . exercice . début', x)) + } + />{' '} + au{' '} + + dispatch(updateSituation('entreprise . exercice . fin', x)) + } + /> + +

    + ) +} + +function Explanations() { + const situation = useSelector(situationSelector) + const showResult = situation['entreprise . bénéfice'] + if (!showResult) { + return null + } + return ( + +

    + + + +
    + + Montant de l'impôt sur les sociétés + +

    +
    + ) +} diff --git a/mon-entreprise/source/site/pages/Simulateurs/Page.tsx b/mon-entreprise/source/site/pages/Simulateurs/Page.tsx index 6349beb55..c4a91de7d 100644 --- a/mon-entreprise/source/site/pages/Simulateurs/Page.tsx +++ b/mon-entreprise/source/site/pages/Simulateurs/Page.tsx @@ -1,4 +1,5 @@ import { setSimulationConfig } from 'Actions/actions' +import { ThemeColorsProvider } from 'Components/utils/colors' import { IsEmbeddedContext } from 'Components/utils/embeddedContext' import Meta from 'Components/utils/Meta' import { default as React, useContext, useEffect } from 'react' @@ -41,8 +42,11 @@ export default function SimulateurPage({ )} )} - - {seoExplanations && !inIframe && seoExplanations} + + + + {seoExplanations && !inIframe && seoExplanations} + ) } diff --git a/mon-entreprise/source/site/pages/Simulateurs/metadata.tsx b/mon-entreprise/source/site/pages/Simulateurs/metadata.tsx index f3e6fad94..1276a3dd4 100644 --- a/mon-entreprise/source/site/pages/Simulateurs/metadata.tsx +++ b/mon-entreprise/source/site/pages/Simulateurs/metadata.tsx @@ -32,6 +32,7 @@ import IndépendantSimulation, { IndépendantPLSimulation, } from './IndépendantSimulation' import SalariéSimulation from './SalariéSimulation' +import ISSimulation from './ISSimulation' import SchemeComparaisonPage from './SchemeComparaison' import ÉconomieCollaborative from './ÉconomieCollaborative' @@ -54,6 +55,7 @@ const simulateurs = [ 'avocat', 'expert-comptable', 'pamc', + 'is', ] as const export type SimulatorData = Record< @@ -65,6 +67,7 @@ export type SimulatorData = Record< ogTitle?: string ogDescription?: string ogImage?: string + color?: string } icône: string shortName: string @@ -742,6 +745,58 @@ export function getSimulatorsData({ shortName: t('pages.simulateurs.pamc.shortname', 'PAMC'), component: PAMCHome, }, + is: { + icône: '🗓', + path: sitePaths.simulateurs.is, + iframe: 'impot-societe', + meta: { + title: t('pages.simulateurs.is.meta.title', 'Impôt sur les sociétés'), + description: t( + 'pages.simulateurs.pamc.meta.description', + 'Calculez votre impôt sur les sociétés' + ), + color: '#E71D66', + }, + shortName: t('pages.simulateurs.is.meta.title', 'Impôt sur les sociétés'), + title: t( + 'pages.simulateurs.is.meta.title', + "Simulateur d'impôt sur les sociétés" + ), + component: ISSimulation, + seoExplanations: ( + +

    Comment est calculé l’impôt sur les sociétés ?

    +

    + L’impôt sur les sociétés s’applique aux bénéfices réalisés par les + sociétés de capitaux (SA, SAS, SASU, SARL, etc.) et sur option + facultative pour certaines autres sociétés (EIRL, EURL, SNC, etc.). +

    +

    + Il est calculé sur la base des bénéfices réalisés en France au cours + de l’exercice comptable. La durée d’un exercice est normalement d’un + an mais il peut être plus court ou plus long (notamment en début + d’activité ou à la dissolution de l’entreprise). Dans ce cas le + barème de l’impôt est pro-ratisé en fonction de la durée de + l’exercice, ce qui est pris en compte dans le simulateur en + modifiant les dates de début et de fin de l’exercice. +

    +

    Taux réduit et régimes spécifiques

    +

    + Les PME réalisant moins de 7,63 millions d’euros de chiffre + d’affaire et dont le capital est détenu à 75% par des personnes + physiques bénéficient d’un taux réduit d’impôt sur les sociétés. Ce + taux est pris en compte sur le simulateur et il n’est pour l’instant + pas possible de simuler l’inéligibilité aux taux réduits. +

    +

    + Enfin il existe des régimes d’impositions spécifiques avec des taux + dédiés pour certains types de plus-values (cession de titres, + cession de brevets). Ces régimes ne sont pas intégrés dans le + simulateur. +

    +
    + ), + }, } } diff --git a/mon-entreprise/source/site/sitePaths.ts b/mon-entreprise/source/site/sitePaths.ts index 6f6aa34d6..46b6fd268 100644 --- a/mon-entreprise/source/site/sitePaths.ts +++ b/mon-entreprise/source/site/sitePaths.ts @@ -69,6 +69,7 @@ const sitePathsFr = { index: '/économie-collaborative', votreSituation: '/votre-situation', }, + is: '/impot-societe', }, nouveautés: '/nouveautés', stats: '/stats', @@ -128,6 +129,7 @@ const sitePathsEn = { index: '/sharing-economy', votreSituation: '/your-situation', }, + is: '/corporate-tax', }, nouveautés: '/news', integration: { diff --git a/mon-entreprise/test/regressions/__snapshots__/simulations.jest.js.snap b/mon-entreprise/test/regressions/__snapshots__/simulations.jest.js.snap index cd7f4da13..39d810180 100644 --- a/mon-entreprise/test/regressions/__snapshots__/simulations.jest.js.snap +++ b/mon-entreprise/test/regressions/__snapshots__/simulations.jest.js.snap @@ -147,6 +147,29 @@ exports[`calculate simulations-auto-entrepreneur: échelle de revenus 10`] = ` Notifications affichées : dirigeant . auto-entrepreneur . contrôle seuil de CA dépassé, entreprise . seuil de franchise de TVA dépassé" `; +exports[`calculate simulations-impot-société: bénéfices 1`] = ` +"[0,0] +Notifications affichées : entreprise . bénéfice . information sur le report de déficit" +`; + +exports[`calculate simulations-impot-société: bénéfices 2`] = `"[0,0]"`; + +exports[`calculate simulations-impot-société: bénéfices 3`] = `"[300,0]"`; + +exports[`calculate simulations-impot-société: bénéfices 4`] = `"[3000,0]"`; + +exports[`calculate simulations-impot-société: bénéfices 5`] = `"[51044,0]"`; + +exports[`calculate simulations-impot-société: bénéfices 6`] = `"[555044,0]"`; + +exports[`calculate simulations-impot-société: bénéfices 7`] = `"[5595044,159457]"`; + +exports[`calculate simulations-impot-société: prorata temporis 1`] = `"[275044,0]"`; + +exports[`calculate simulations-impot-société: prorata temporis 2`] = `"[277936,0]"`; + +exports[`calculate simulations-impot-société: prorata temporis 3`] = `"[272981,0]"`; + exports[`calculate simulations-indépendant: acre 1`] = `"[73025,23025,50000,51980,8213,41787,0,73025]"`; exports[`calculate simulations-indépendant: activité 1`] = `"[26840,6840,20000,20726,601,19399,0,26840]"`; diff --git a/mon-entreprise/test/regressions/simulations-impôt-société.yaml b/mon-entreprise/test/regressions/simulations-impôt-société.yaml new file mode 100644 index 000000000..61f4e97e6 --- /dev/null +++ b/mon-entreprise/test/regressions/simulations-impôt-société.yaml @@ -0,0 +1,17 @@ +bénéfices: + - entreprise . bénéfice: -2000 €/an + - entreprise . bénéfice: 0 €/an + - entreprise . bénéfice: 2000 €/an + - entreprise . bénéfice: 20000 €/an + - entreprise . bénéfice: 200000 €/an + - entreprise . bénéfice: 2000000 €/an + - entreprise . bénéfice: 20000000 €/an + +prorata temporis: + - entreprise . bénéfice: 1000000 €/an + - entreprise . bénéfice: 1000000 €/an + entreprise . exercice . début: 01/01/2020 + entreprise . exercice . fin: 01/06/2020 + - entreprise . bénéfice: 1000000 €/an + entreprise . exercice . début: 01/01/2020 + entreprise . exercice . fin: 01/06/2021 diff --git a/mon-entreprise/test/regressions/simulations.jest.js b/mon-entreprise/test/regressions/simulations.jest.js index e84f8ac97..a03c0f554 100644 --- a/mon-entreprise/test/regressions/simulations.jest.js +++ b/mon-entreprise/test/regressions/simulations.jest.js @@ -21,6 +21,7 @@ import autoEntrepreneurSituations from './simulations-auto-entrepreneur.yaml' import independentSituations from './simulations-indépendant.yaml' import professionsLibéralesSituations from './simulations-professions-libérales.yaml' import remunerationDirigeantSituations from './simulations-rémunération-dirigeant.yaml' +import impotSocieteSituations from './simulations-impôt-société.yaml' import employeeSituations from './simulations-salarié.yaml' import aideDéclarationIndépendantsSituations from './aide-déclaration-indépendants.yaml' @@ -150,3 +151,10 @@ it('calculate simulations-professions-libérales', () => { } ) }) + +it('calculate simulations-impot-société', () => { + runSimulations(impotSocieteSituations, [ + 'entreprise . impôt sur les sociétés', + 'entreprise . impôt sur les sociétés . contribution sociale', + ]) +}) diff --git a/publicodes/core/source/index.ts b/publicodes/core/source/index.ts index d9516de1f..bef52faf2 100644 --- a/publicodes/core/source/index.ts +++ b/publicodes/core/source/index.ts @@ -11,7 +11,7 @@ import { } from './replacement' import { Rule, RuleNode } from './rule' import * as utils from './ruleUtils' -import { getUnitKey } from './units' +import { formatUnit, getUnitKey } from './units' const emptyCache = () => ({ _meta: { ruleStack: [] }, @@ -60,6 +60,7 @@ export type Logger = { type Options = { logger: Logger getUnitKey?: getUnitKey + formatUnit?: formatUnit } export type EvaluationFunction = ( this: Engine, diff --git a/publicodes/ui-react/source/mecanisms/common.tsx b/publicodes/ui-react/source/mecanisms/common.tsx index 45968e770..870ac0bef 100644 --- a/publicodes/ui-react/source/mecanisms/common.tsx +++ b/publicodes/ui-react/source/mecanisms/common.tsx @@ -51,25 +51,28 @@ type NodeValuePointerProps = { unit: Unit | undefined } -export const NodeValuePointer = ({ data, unit }: NodeValuePointerProps) => ( - - {formatValue(simplifyNodeUnit({ nodeValue: data, unit }), { - language: 'fr', - })} - -) +export const NodeValuePointer = ({ data, unit }: NodeValuePointerProps) => { + const engine = useContext(EngineContext) + return ( + + {formatValue(simplifyNodeUnit({ nodeValue: data, unit }), { + formatUnit: engine?.options?.formatUnit, + })} + + ) +} // Un élément du graphe de calcul qui a une valeur interprétée (à afficher) type NodeProps = {