Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permet à la viseuse de changer la décision de l'instruction #1479

Merged
merged 20 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions api/tests/test_declaration_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,42 +658,70 @@ def test_accept_visa(self):
self.assertEqual(latest_snapshot.expiration_days, 23)

@authenticate
def test_visor_can_modify_comment(self):
def test_visor_can_modify_decision(self):
"""
Une personne avec le rôle de visa peut modifier le commentaire à destination du pro
La viseuse peut modifier la décision de l'instructrice
"""
VisaRoleFactory(user=authenticate.user)

# Visa acceptée
# L'instructrice à marqué cette déclaration comme « autorisée », mais la viseuse la fera
# passer en observation
declaration = OngoingVisaDeclarationFactory(
post_validation_status=Declaration.DeclarationStatus.AUTHORIZED,
post_validation_producer_message="À authoriser",
post_validation_expiration_days=12,
)

response = self.client.post(
reverse("api:accept_visa", kwargs={"pk": declaration.id}), {"comment": "Overriden comment"}, format="json"
)
body = {
"comment": "overridden comment",
"proposal": "OBSERVATION",
"delayDays": 6,
"reasons": [
"a",
"b",
],
}
response = self.client.post(reverse("api:accept_visa", kwargs={"pk": declaration.id}), body, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)

declaration.refresh_from_db()
self.assertEqual(declaration.last_administration_comment, "Overriden comment")
latest_snapshot = declaration.snapshots.latest("creation_date")
self.assertEqual(declaration.status, Declaration.DeclarationStatus.OBSERVATION)
self.assertEqual(latest_snapshot.comment, "overridden comment")
self.assertEqual(latest_snapshot.status, Declaration.DeclarationStatus.OBSERVATION)
self.assertEqual(latest_snapshot.expiration_days, 6)
self.assertEqual(latest_snapshot.blocking_reasons, ["a", "b"])

@authenticate
def test_visor_cant_modify_on_refuse(self):
"""
Refuser un visa n'applique pas les modifications
"""
VisaRoleFactory(user=authenticate.user)

# Visa refusée
declaration = OngoingVisaDeclarationFactory(
post_validation_status=Declaration.DeclarationStatus.REJECTED,
post_validation_producer_message="À refuser",
post_validation_expiration_days=20,
post_validation_status=Declaration.DeclarationStatus.AUTHORIZED,
post_validation_producer_message="À authoriser",
)

response = self.client.post(
reverse("api:refuse_visa", kwargs={"pk": declaration.id}),
{"comment": "Overriden comment 2"},
format="json",
)
body = {
"comment": "overridden comment",
"proposal": "OBSERVATION",
"delayDays": 6,
"reasons": [
"a",
"b",
],
}
response = self.client.post(reverse("api:refuse_visa", kwargs={"pk": declaration.id}), body, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)

declaration.refresh_from_db()
self.assertEqual(declaration.last_administration_comment, "Overriden comment 2")
latest_snapshot = declaration.snapshots.latest("creation_date")
self.assertEqual(declaration.status, Declaration.DeclarationStatus.AWAITING_INSTRUCTION)
self.assertEqual(latest_snapshot.comment, "À authoriser")
self.assertEqual(latest_snapshot.status, Declaration.DeclarationStatus.AWAITING_INSTRUCTION)
self.assertEqual(latest_snapshot.expiration_days, None)
self.assertEqual(latest_snapshot.blocking_reasons, None)

@authenticate
def accept_visa_unauthorized(self):
Expand Down
34 changes: 26 additions & 8 deletions api/views/declaration/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,32 +637,50 @@ def perform_snapshot_creation(self, request, declaration):
declaration.create_snapshot(
user=request.user,
action=self.get_snapshot_action(request, declaration),
comment=request.data.get("comment", declaration.post_validation_producer_message),
comment=declaration.post_validation_producer_message,
post_validation_status=self.get_snapshot_post_validation_status(request, declaration),
)


class DeclarationAcceptVisaView(VisaDecisionView):
"""
ONGOING_VISA -> { AUTHORIZED | REJECTED | OBJECTION | OBSERVATION }
Le ou la viseuse peut surcharger la décision de l'instructrice en envoyant
un objet dans le payload de cette forme :
{
comment: "overridden comment",
proposal: "OBSERVATION", // (ou un autre statut)
delayDays: 10,
reasons: ["a", "b"],
}
"""

snapshot_action = Snapshot.SnapshotActions.ACCEPT_VISA

def get_snapshot_post_validation_status(self, request, declaration):
def get_validation_status(self, request, declaration):
overridden_status = request.data.get("proposal")
if overridden_status:
return Declaration.DeclarationStatus(overridden_status)
return declaration.post_validation_status

def get_snapshot_post_validation_status(self, request, declaration):
return self.get_validation_status(request, declaration)

def perform_snapshot_creation(self, request, declaration):
"""
Possible de le surcharger si la création du snapshot nécessite un
traitement spécial
"""
declaration.create_snapshot(
overridden = request.data.get("proposal")
data = request.data
d = declaration
d.create_snapshot(
user=request.user,
comment=request.data.get("comment", declaration.post_validation_producer_message),
expiration_days=declaration.post_validation_expiration_days,
action=self.get_snapshot_action(request, declaration),
post_validation_status=self.get_snapshot_post_validation_status(request, declaration),
comment=data.get("comment") if overridden else d.post_validation_producer_message,
expiration_days=data.get("delay_days") if overridden else d.post_validation_expiration_days,
action=self.get_snapshot_action(request, d),
post_validation_status=self.get_snapshot_post_validation_status(request, d),
blocking_reasons=data.get("reasons") if overridden else None,
)

def get_transition(self, request, declaration):
Expand All @@ -672,7 +690,7 @@ def get_transition(self, request, declaration):
Declaration.DeclarationStatus.OBJECTION: "accept_visa_object",
Declaration.DeclarationStatus.OBSERVATION: "accept_visa_observe",
}
return transition_map.get(declaration.post_validation_status)
return transition_map.get(self.get_validation_status(request, declaration))

def get_brevo_template_id(self, request, declaration):
template_map = {
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/utils/mappings.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,59 @@ export const populationCategoriesMapping = {
export const getAuthorizationModeInFrench = (type) => {
return authorizationModesMapping[type] || null
}

export const blockingReasons = [
{
title: "Le produit ne répond pas à la définition du complément alimentaire",
items: [
"Forme assimilable à un aliment courant",
"Recommandations d'emploi incompatibles",
"Composition (source concentrée, ...)",
"Autre raison pour laquelle le produit ne répond pas à la définition du complément alimentaire",
],
},
{
title: "Le produit répond à la définition du médicament",
items: ["Médicament par fonction", "Médicament par présentation", "Sevrage tabagique"],
},
{
title: "Les procédures ne sont pas respectées",
items: [
"Présence d'un Novel Food",
"Présence d'une forme d'apport en nutriments non autorisée",
"Demande en article 17 attendue",
"Demande en article 18 attendue",
],
},
{
title: "Le dossier n'est pas recevable",
items: [
"Incohérences entre le dossier et l'étiquetage",
"Informations manquantes",
"Absence de preuve de reconnaissance mutuelle",
"Absence ou non conformité de l'étiquetage",
"Autre motif d'irrecevabilité",
],
},
{
title: "Le complément alimentaire n'est pas acceptable",
items: ["Existence d'un risque"],
},
]

export const decisionCategories = [
{
value: "approve",
title: "J’envoie l’attestation de déclaration",
icon: "ri-checkbox-circle-fill",
description: "La déclaration est conforme et peut être transmise.",
color: "green",
},
{
value: "modify",
title: "Des changements sont nécessaires",
icon: "ri-close-circle-fill",
description: "La déclaration ne peut pas être transmise en l'état.",
color: "red",
},
]
63 changes: 4 additions & 59 deletions frontend/src/views/InstructionPage/DecisionTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import { headers } from "@/utils/data-fetching"
import useToaster from "@/composables/use-toaster"
import { handleError } from "@/utils/error-handling"
import ArticleInfoRow from "@/components/DeclarationSummary/ArticleInfoRow"
import { blockingReasons, decisionCategories } from "@/utils/mappings"

const decisionCategory = ref(null)
watch(decisionCategory, () => (proposal.value = decisionCategory.value === "approve" ? "autorisation" : null))
Expand All @@ -120,7 +121,7 @@ const rules = computed(() => {
})
const declaration = defineModel()
const proposal = ref(null)
const delayDays = ref(15)
const delayDays = ref()
const comment = ref(declaration.value?.lastAdministrationComment || "")
const reasons = ref([])

Expand All @@ -136,68 +137,12 @@ const disableDelayDays = computed(() => proposal.value === "rejection")
watch(proposal, (newProposal) => {
if (mandatoryVisaProposals.indexOf(newProposal) > -1) needsVisa.value = true
if (newProposal === "objection") delayDays.value = 30
else if (newProposal === "rejection") delayDays.value = null
else delayDays.value = 15
else if (newProposal === "observation") delayDays.value = 15
else delayDays.value = null
})

const needsAnsesReferal = computed(() => declaration.value?.article === "ANSES_REFERAL")

const decisionCategories = [
{
value: "approve",
title: "J’envoie l’attestation de déclaration",
icon: "ri-checkbox-circle-fill",
description: "La déclaration est conforme et peut être transmise.",
color: "green",
},
{
value: "modify",
title: "Des changements sont nécessaires",
icon: "ri-close-circle-fill",
description: "La déclaration ne peut pas être transmise en l'état.",
color: "red",
},
]

const blockingReasons = [
{
title: "Le produit ne répond pas à la définition du complément alimentaire",
items: [
"Forme assimilable à un aliment courant",
"Recommandations d'emploi incompatibles",
"Composition (source concentrée, ...)",
"Autre raison pour laquelle le produit ne répond pas à la définition du complément alimentaire",
],
},
{
title: "Le produit répond à la définition du médicament",
items: ["Médicament par fonction", "Médicament par présentation", "Sevrage tabagique"],
},
{
title: "Les procédures ne sont pas respectées",
items: [
"Présence d'un Novel Food",
"Présence d'une forme d'apport en nutriments non autorisée",
"Demande en article 17 attendue",
"Demande en article 18 attendue",
],
},
{
title: "Le dossier n'est pas recevable",
items: [
"Incohérences entre le dossier et l'étiquetage",
"Informations manquantes",
"Absence de preuve de reconnaissance mutuelle",
"Absence ou non conformité de l'étiquetage",
"Autre motif d'irrecevabilité",
],
},
{
title: "Le complément alimentaire n'est pas acceptable",
items: ["Existence d'un risque"],
},
]

const proposalOptions = computed(() => {
if (decisionCategory.value === "approve") return [{ text: "Autorisation", value: "autorisation" }]

Expand Down
Loading
Loading