a8354bd103
The code paths for deleting a dossier were different, depending on whether the dossier was deleted by the user, or from the Manager. This commit unifies the two code paths into one. This has the effect of: - An operation log is now recorded when an user deletes its own dossier; - Gestionnaires are now notified even when the dossier is deleted from the Manager; - The `support:delete_user_account` task now requires the email address of the author.
517 lines
16 KiB
Ruby
517 lines
16 KiB
Ruby
class Dossier < ApplicationRecord
|
|
include DossierFilteringConcern
|
|
|
|
enum state: {
|
|
brouillon: 'brouillon',
|
|
en_construction: 'en_construction',
|
|
en_instruction: 'en_instruction',
|
|
accepte: 'accepte',
|
|
refuse: 'refuse',
|
|
sans_suite: 'sans_suite'
|
|
}
|
|
|
|
EN_CONSTRUCTION_OU_INSTRUCTION = [states.fetch(:en_construction), states.fetch(:en_instruction)]
|
|
TERMINE = [states.fetch(:accepte), states.fetch(:refuse), states.fetch(:sans_suite)]
|
|
INSTRUCTION_COMMENCEE = TERMINE + [states.fetch(:en_instruction)]
|
|
SOUMIS = EN_CONSTRUCTION_OU_INSTRUCTION + TERMINE
|
|
|
|
has_one :etablissement, dependent: :destroy
|
|
has_one :individual, dependent: :destroy
|
|
has_one :attestation, dependent: :destroy
|
|
|
|
has_many :pieces_justificatives, inverse_of: :dossier, dependent: :destroy
|
|
has_one_attached :justificatif_motivation
|
|
|
|
has_many :champs, -> { root.public_only.ordered }, inverse_of: :dossier, dependent: :destroy
|
|
has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy
|
|
has_many :commentaires, inverse_of: :dossier, dependent: :destroy
|
|
has_many :invites, dependent: :destroy
|
|
has_many :follows, -> { active }, inverse_of: :dossier
|
|
has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
|
|
has_many :followers_gestionnaires, through: :follows, source: :gestionnaire
|
|
has_many :previous_followers_gestionnaires, -> { distinct }, through: :previous_follows, source: :gestionnaire
|
|
has_many :avis, inverse_of: :dossier, dependent: :destroy
|
|
|
|
has_many :dossier_operation_logs, dependent: :destroy
|
|
|
|
belongs_to :procedure
|
|
belongs_to :user
|
|
|
|
accepts_nested_attributes_for :champs
|
|
accepts_nested_attributes_for :champs_private
|
|
|
|
include AASM
|
|
|
|
aasm whiny_persistence: true, column: :state, enum: true do
|
|
state :brouillon, initial: true
|
|
state :en_construction
|
|
state :en_instruction
|
|
state :accepte
|
|
state :refuse
|
|
state :sans_suite
|
|
|
|
event :passer_en_construction, after: :after_passer_en_construction do
|
|
transitions from: :brouillon, to: :en_construction
|
|
end
|
|
|
|
event :passer_en_instruction, after: :after_passer_en_instruction do
|
|
transitions from: :en_construction, to: :en_instruction
|
|
end
|
|
|
|
event :passer_automatiquement_en_instruction, after: :after_passer_automatiquement_en_instruction do
|
|
transitions from: :en_construction, to: :en_instruction
|
|
end
|
|
|
|
event :repasser_en_construction, after: :after_repasser_en_construction do
|
|
transitions from: :en_instruction, to: :en_construction
|
|
end
|
|
|
|
event :accepter, after: :after_accepter do
|
|
transitions from: :en_instruction, to: :accepte
|
|
end
|
|
|
|
event :accepter_automatiquement, after: :after_accepter_automatiquement do
|
|
transitions from: :en_construction, to: :accepte
|
|
end
|
|
|
|
event :refuser, after: :after_refuser do
|
|
transitions from: :en_instruction, to: :refuse
|
|
end
|
|
|
|
event :classer_sans_suite, after: :after_classer_sans_suite do
|
|
transitions from: :en_instruction, to: :sans_suite
|
|
end
|
|
|
|
event :repasser_en_instruction, after: :after_repasser_en_instruction do
|
|
transitions from: :refuse, to: :en_instruction
|
|
transitions from: :sans_suite, to: :en_instruction
|
|
end
|
|
end
|
|
|
|
default_scope { where(hidden_at: nil) }
|
|
scope :state_brouillon, -> { where(state: states.fetch(:brouillon)) }
|
|
scope :state_not_brouillon, -> { where.not(state: states.fetch(:brouillon)) }
|
|
scope :state_en_construction, -> { where(state: states.fetch(:en_construction)) }
|
|
scope :state_en_instruction, -> { where(state: states.fetch(:en_instruction)) }
|
|
scope :state_en_construction_ou_instruction, -> { where(state: EN_CONSTRUCTION_OU_INSTRUCTION) }
|
|
scope :state_instruction_commencee, -> { where(state: INSTRUCTION_COMMENCEE) }
|
|
scope :state_termine, -> { where(state: TERMINE) }
|
|
|
|
scope :archived, -> { where(archived: true) }
|
|
scope :not_archived, -> { where(archived: false) }
|
|
|
|
scope :order_by_updated_at, -> (order = :desc) { order(updated_at: order) }
|
|
scope :order_for_api, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
|
|
|
|
scope :all_state, -> { not_archived.state_not_brouillon }
|
|
scope :en_construction, -> { not_archived.state_en_construction }
|
|
scope :en_instruction, -> { not_archived.state_en_instruction }
|
|
scope :termine, -> { not_archived.state_termine }
|
|
scope :downloadable_sorted, -> { state_not_brouillon.includes(:etablissement, :user, :individual, :followers_gestionnaires, :avis, champs: { etablissement: [:champ], type_de_champ: :drop_down_list }, champs_private: { etablissement: [:champ], type_de_champ: :drop_down_list }).order(en_construction_at: 'asc') }
|
|
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
|
|
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
|
|
scope :followed_by, -> (gestionnaire) { joins(:follows).where(follows: { gestionnaire: gestionnaire }) }
|
|
scope :with_champs, -> { includes(champs: :type_de_champ) }
|
|
scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) }
|
|
scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
|
|
scope :for_api, -> {
|
|
includes(commentaires: { piece_jointe_attachment: :blob },
|
|
champs: [
|
|
:geo_areas,
|
|
:etablissement,
|
|
piece_justificative_file_attachment: :blob
|
|
],
|
|
champs_private: [
|
|
:geo_areas,
|
|
:etablissement,
|
|
piece_justificative_file_attachment: :blob
|
|
],
|
|
pieces_justificatives: [],
|
|
avis: [],
|
|
etablissement: [],
|
|
individual: [],
|
|
user: [])
|
|
}
|
|
|
|
accepts_nested_attributes_for :individual
|
|
|
|
delegate :siret, :siren, to: :etablissement, allow_nil: true
|
|
delegate :types_de_piece_justificative, to: :procedure
|
|
delegate :types_de_champ, to: :procedure
|
|
delegate :france_connect_information, to: :user
|
|
|
|
before_validation :update_state_dates, if: -> { state_changed? }
|
|
|
|
before_save :build_default_champs, if: Proc.new { procedure_id_changed? }
|
|
before_save :build_default_individual, if: Proc.new { procedure.for_individual? }
|
|
before_save :update_search_terms
|
|
|
|
after_save :send_dossier_received
|
|
after_save :send_web_hook
|
|
after_create :send_draft_notification_email
|
|
|
|
validates :user, presence: true
|
|
|
|
def update_search_terms
|
|
self.search_terms = [
|
|
user&.email,
|
|
*champs.flat_map(&:search_terms),
|
|
*etablissement&.search_terms,
|
|
individual&.nom,
|
|
individual&.prenom
|
|
].compact.join(' ')
|
|
self.private_search_terms = champs_private.flat_map(&:search_terms).compact.join(' ')
|
|
end
|
|
|
|
def was_piece_justificative_uploaded_for_type_id?(type_id)
|
|
pieces_justificatives.where(type_de_piece_justificative_id: type_id).count > 0
|
|
end
|
|
|
|
def retrieve_last_piece_justificative_by_type(type)
|
|
pieces_justificatives.where(type_de_piece_justificative_id: type).last
|
|
end
|
|
|
|
def retrieve_all_piece_justificative_by_type(type)
|
|
pieces_justificatives.where(type_de_piece_justificative_id: type).order(created_at: :DESC)
|
|
end
|
|
|
|
def build_default_champs
|
|
procedure.build_champs.each do |champ|
|
|
champs << champ
|
|
end
|
|
procedure.build_champs_private.each do |champ|
|
|
champs_private << champ
|
|
end
|
|
end
|
|
|
|
def build_default_individual
|
|
if Individual.where(dossier_id: self.id).count == 0
|
|
build_individual
|
|
end
|
|
end
|
|
|
|
def en_construction_ou_instruction?
|
|
EN_CONSTRUCTION_OU_INSTRUCTION.include?(state)
|
|
end
|
|
|
|
def termine?
|
|
TERMINE.include?(state)
|
|
end
|
|
|
|
def instruction_commencee?
|
|
INSTRUCTION_COMMENCEE.include?(state)
|
|
end
|
|
|
|
def reset!
|
|
etablissement.destroy
|
|
|
|
update_columns(autorisation_donnees: false)
|
|
end
|
|
|
|
def read_only?
|
|
en_instruction? || accepte? || refuse? || sans_suite?
|
|
end
|
|
|
|
def can_transition_to_en_construction?
|
|
!procedure.archivee? && brouillon?
|
|
end
|
|
|
|
def can_be_updated_by_user?
|
|
brouillon? || en_construction?
|
|
end
|
|
|
|
def can_be_deleted_by_user?
|
|
brouillon? || en_construction?
|
|
end
|
|
|
|
def messagerie_available?
|
|
!brouillon? && !archived
|
|
end
|
|
|
|
def retention_end_date
|
|
if instruction_commencee?
|
|
en_instruction_at + procedure.duree_conservation_dossiers_dans_ds.months
|
|
end
|
|
end
|
|
|
|
def retention_expired?
|
|
instruction_commencee? && retention_end_date <= Time.zone.now
|
|
end
|
|
|
|
def text_summary
|
|
if brouillon?
|
|
parts = [
|
|
"Dossier en brouillon répondant à la démarche ",
|
|
procedure.libelle,
|
|
" gérée par l'organisme ",
|
|
procedure.organisation_name
|
|
]
|
|
else
|
|
parts = [
|
|
"Dossier déposé le ",
|
|
en_construction_at.strftime("%d/%m/%Y"),
|
|
" sur la démarche ",
|
|
procedure.libelle,
|
|
" gérée par l'organisme ",
|
|
procedure.organisation_name
|
|
]
|
|
end
|
|
|
|
parts.join
|
|
end
|
|
|
|
def avis_for(gestionnaire)
|
|
if gestionnaire.dossiers.include?(self)
|
|
avis.order(created_at: :asc)
|
|
else
|
|
avis
|
|
.where(confidentiel: false)
|
|
.or(avis.where(claimant: gestionnaire))
|
|
.or(avis.where(gestionnaire: gestionnaire))
|
|
.order(created_at: :asc)
|
|
end
|
|
end
|
|
|
|
def owner_name
|
|
if etablissement.present?
|
|
etablissement.entreprise_raison_sociale
|
|
elsif individual.present?
|
|
"#{individual.nom} #{individual.prenom}"
|
|
end
|
|
end
|
|
|
|
def expose_legacy_carto_api?
|
|
procedure.expose_legacy_carto_api?
|
|
end
|
|
|
|
def geo_position
|
|
if etablissement.present?
|
|
point = ApiAdresse::PointAdapter.new(etablissement.geo_adresse).geocode
|
|
end
|
|
|
|
lon = "2.428462"
|
|
lat = "46.538192"
|
|
zoom = "13"
|
|
|
|
if point.present?
|
|
lon = point.x.to_s
|
|
lat = point.y.to_s
|
|
end
|
|
|
|
{ lon: lon, lat: lat, zoom: zoom }
|
|
end
|
|
|
|
def unspecified_attestation_champs
|
|
attestation_template = procedure.attestation_template
|
|
|
|
if attestation_template&.activated?
|
|
attestation_template.unspecified_champs_for_dossier(self)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def build_attestation
|
|
if procedure.attestation_template&.activated?
|
|
procedure.attestation_template.attestation_for(self)
|
|
end
|
|
end
|
|
|
|
def delete_and_keep_track(author)
|
|
deleted_dossier = DeletedDossier.create_from_dossier(self)
|
|
update(hidden_at: deleted_dossier.deleted_at)
|
|
|
|
if en_construction?
|
|
administration_emails = followers_gestionnaires.present? ? followers_gestionnaires.pluck(:email) : procedure.administrateurs.pluck(:email)
|
|
administration_emails.each do |email|
|
|
DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
|
|
end
|
|
end
|
|
DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later
|
|
|
|
log_dossier_operation(author, :supprimer, self)
|
|
end
|
|
|
|
def after_passer_en_instruction(gestionnaire)
|
|
gestionnaire.follow(self)
|
|
|
|
log_dossier_operation(gestionnaire, :passer_en_instruction)
|
|
end
|
|
|
|
def after_passer_automatiquement_en_instruction
|
|
log_automatic_dossier_operation(:passer_en_instruction)
|
|
end
|
|
|
|
def after_repasser_en_construction(gestionnaire)
|
|
self.en_instruction_at = nil
|
|
|
|
save!
|
|
log_dossier_operation(gestionnaire, :repasser_en_construction)
|
|
end
|
|
|
|
def after_repasser_en_instruction(gestionnaire)
|
|
self.processed_at = nil
|
|
self.motivation = nil
|
|
attestation&.destroy
|
|
|
|
save!
|
|
DossierMailer.notify_revert_to_instruction(self).deliver_later
|
|
log_dossier_operation(gestionnaire, :repasser_en_instruction)
|
|
end
|
|
|
|
def after_accepter(gestionnaire, motivation, justificatif = nil)
|
|
self.motivation = motivation
|
|
|
|
if justificatif
|
|
self.justificatif_motivation.attach(justificatif)
|
|
end
|
|
|
|
if attestation.nil?
|
|
self.attestation = build_attestation
|
|
end
|
|
|
|
save!
|
|
NotificationMailer.send_closed_notification(self).deliver_later
|
|
log_dossier_operation(gestionnaire, :accepter, self)
|
|
end
|
|
|
|
def after_accepter_automatiquement
|
|
self.en_instruction_at ||= Time.zone.now
|
|
|
|
if attestation.nil?
|
|
self.attestation = build_attestation
|
|
end
|
|
|
|
save!
|
|
NotificationMailer.send_closed_notification(self).deliver_later
|
|
log_automatic_dossier_operation(:accepter, self)
|
|
end
|
|
|
|
def after_refuser(gestionnaire, motivation, justificatif = nil)
|
|
self.motivation = motivation
|
|
|
|
if justificatif
|
|
self.justificatif_motivation.attach(justificatif)
|
|
end
|
|
|
|
save!
|
|
NotificationMailer.send_refused_notification(self).deliver_later
|
|
log_dossier_operation(gestionnaire, :refuser, self)
|
|
end
|
|
|
|
def after_classer_sans_suite(gestionnaire, motivation, justificatif = nil)
|
|
self.motivation = motivation
|
|
|
|
if justificatif
|
|
self.justificatif_motivation.attach(justificatif)
|
|
end
|
|
|
|
save!
|
|
NotificationMailer.send_without_continuation_notification(self).deliver_later
|
|
log_dossier_operation(gestionnaire, :classer_sans_suite, self)
|
|
end
|
|
|
|
def check_mandatory_champs
|
|
(champs + champs.select(&:repetition?).flat_map(&:champs))
|
|
.select(&:mandatory_and_blank?)
|
|
.map do |champ|
|
|
"Le champ #{champ.libelle.truncate(200)} doit être rempli."
|
|
end
|
|
end
|
|
|
|
def modifier_annotations!(gestionnaire)
|
|
champs_private.select(&:value_previously_changed?).each do |champ|
|
|
log_dossier_operation(gestionnaire, :modifier_annotation, champ)
|
|
end
|
|
end
|
|
|
|
def demander_un_avis!(avis)
|
|
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
|
|
end
|
|
|
|
def spreadsheet_columns
|
|
[
|
|
['ID', id.to_s],
|
|
['Email', user.email],
|
|
['Civilité', individual&.gender],
|
|
['Nom', individual&.nom],
|
|
['Prénom', individual&.prenom],
|
|
['Date de naissance', individual&.birthdate],
|
|
['Archivé', :archived],
|
|
['État du dossier', I18n.t(state, scope: [:activerecord, :attributes, :dossier, :state])],
|
|
['Dernière mise à jour le', :updated_at],
|
|
['Passé en construction le', :en_construction_at],
|
|
['Passé en instruction le', :en_instruction_at],
|
|
['Traité le', :processed_at],
|
|
['Motivation de la décision', :motivation],
|
|
['Instructeurs', followers_gestionnaires.map(&:email).join(' ')]
|
|
] + champs_for_export + annotations_for_export
|
|
end
|
|
|
|
def champs_for_export
|
|
champs.reject(&:exclude_from_export?).map do |champ|
|
|
[champ.libelle, champ.for_export]
|
|
end
|
|
end
|
|
|
|
def annotations_for_export
|
|
champs_private.reject(&:exclude_from_export?).map do |champ|
|
|
[champ.libelle, champ.for_export]
|
|
end
|
|
end
|
|
|
|
def attachments_downloadable?
|
|
!PiecesJustificativesService.liste_pieces_justificatives(self).empty? && PiecesJustificativesService.pieces_justificatives_total_size(self) < 50.megabytes
|
|
end
|
|
|
|
private
|
|
|
|
def log_dossier_operation(author, operation, subject = nil)
|
|
DossierOperationLog.create_and_serialize(
|
|
dossier: self,
|
|
operation: DossierOperationLog.operations.fetch(operation),
|
|
author: author,
|
|
subject: subject
|
|
)
|
|
end
|
|
|
|
def log_automatic_dossier_operation(operation, subject = nil)
|
|
DossierOperationLog.create_and_serialize(
|
|
dossier: self,
|
|
operation: DossierOperationLog.operations.fetch(operation),
|
|
automatic_operation: true,
|
|
subject: subject
|
|
)
|
|
end
|
|
|
|
def update_state_dates
|
|
if en_construction? && !self.en_construction_at
|
|
self.en_construction_at = Time.zone.now
|
|
elsif en_instruction? && !self.en_instruction_at
|
|
self.en_instruction_at = Time.zone.now
|
|
elsif TERMINE.include?(state)
|
|
self.processed_at = Time.zone.now
|
|
end
|
|
end
|
|
|
|
def send_dossier_received
|
|
if saved_change_to_state? && en_instruction? && !procedure.declarative_accepte?
|
|
NotificationMailer.send_dossier_received(self).deliver_later
|
|
end
|
|
end
|
|
|
|
def send_draft_notification_email
|
|
if brouillon? && !procedure.declarative?
|
|
DossierMailer.notify_new_draft(self).deliver_later
|
|
end
|
|
end
|
|
|
|
def send_web_hook
|
|
if saved_change_to_state? && !brouillon? && procedure.web_hook_url
|
|
WebHookJob.perform_later(
|
|
procedure,
|
|
self
|
|
)
|
|
end
|
|
end
|
|
end
|