demarches-normaliennes/app/models/dossier.rb

1165 lines
39 KiB
Ruby
Raw Normal View History

2020-08-06 16:35:45 +02:00
# == Schema Information
#
# Table name: dossiers
#
# id :integer not null, primary key
# api_entreprise_job_exceptions :string is an Array
2020-08-06 16:35:45 +02:00
# archived :boolean default(FALSE)
# autorisation_donnees :boolean
# brouillon_close_to_expiration_notice_sent_at :datetime
# conservation_extension :interval default(0 seconds)
# declarative_triggered_at :datetime
2021-05-01 12:20:24 +02:00
# deleted_user_email_never_send :string
2020-08-06 16:35:45 +02:00
# en_construction_at :datetime
# en_construction_close_to_expiration_notice_sent_at :datetime
# en_instruction_at :datetime
# groupe_instructeur_updated_at :datetime
# hidden_at :datetime
# identity_updated_at :datetime
2020-08-06 16:35:45 +02:00
# last_avis_updated_at :datetime
# last_champ_private_updated_at :datetime
# last_champ_updated_at :datetime
# last_commentaire_updated_at :datetime
# motivation :text
# private_search_terms :text
# processed_at :datetime
# search_terms :text
# state :string
# termine_close_to_expiration_notice_sent_at :datetime
# created_at :datetime
# updated_at :datetime
# dossier_transfer_id :bigint
2020-08-06 16:35:45 +02:00
# groupe_instructeur_id :bigint
# revision_id :bigint
# user_id :integer
#
2018-03-06 13:44:29 +01:00
class Dossier < ApplicationRecord
self.ignored_columns = [:en_construction_conservation_extension]
include DossierFilteringConcern
include DossierRebaseConcern
2020-02-05 16:09:03 +01:00
include Discard::Model
self.discard_column = :hidden_at
default_scope -> { kept }
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
2020-05-13 20:45:21 +02:00
TAILLE_MAX_ZIP = 100.megabytes
REMAINING_DAYS_BEFORE_CLOSING = 2
INTERVAL_BEFORE_CLOSING = "#{REMAINING_DAYS_BEFORE_CLOSING} days"
2021-11-17 10:53:43 +01:00
REMAINING_WEEKS_BEFORE_EXPIRATION = 2
INTERVAL_BEFORE_EXPIRATION = "#{REMAINING_WEEKS_BEFORE_EXPIRATION} weeks"
MONTHS_AFTER_EXPIRATION = 1
DAYS_AFTER_EXPIRATION = 5
INTERVAL_EXPIRATION = "#{MONTHS_AFTER_EXPIRATION} month #{DAYS_AFTER_EXPIRATION} days"
2015-09-24 11:45:00 +02:00
has_one :etablissement, dependent: :destroy
has_one :individual, validate: false, dependent: :destroy
has_one :attestation, dependent: :destroy
2021-04-13 20:24:02 +02:00
# FIXME: some dossiers have more than one attestation
has_many :attestations, dependent: :destroy
has_one_attached :justificatif_motivation
has_many :champs, -> { root.public_ordered }, inverse_of: false, dependent: :destroy
has_many :champs_private, -> { root.private_ordered }, class_name: 'Champ', inverse_of: false, 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_instructeurs, through: :follows, source: :instructeur
has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur
has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :experts, through: :avis
has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy do
def passer_en_construction(instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:en_construction),
instructeur_email: instructeur&.email,
process_expired: false,
processed_at: processed_at)
end
def passer_en_instruction(instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:en_instruction),
instructeur_email: instructeur&.email,
process_expired: false,
processed_at: processed_at)
end
def accepter_automatiquement(processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:accepte),
process_expired: proxy_association.owner.procedure.feature_enabled?(:procedure_process_expired_dossiers_termine),
processed_at: processed_at)
end
def accepter(motivation: nil, instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:accepte),
instructeur_email: instructeur&.email,
motivation: motivation,
process_expired: proxy_association.owner.procedure.feature_enabled?(:procedure_process_expired_dossiers_termine),
processed_at: processed_at)
end
def refuser(motivation: nil, instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:refuse),
instructeur_email: instructeur&.email,
motivation: motivation,
process_expired: proxy_association.owner.procedure.feature_enabled?(:procedure_process_expired_dossiers_termine),
processed_at: processed_at)
end
def classer_sans_suite(motivation: nil, instructeur: nil, processed_at: Time.zone.now)
build(state: Dossier.states.fetch(:sans_suite),
instructeur_email: instructeur&.email,
motivation: motivation,
process_expired: proxy_association.owner.procedure.feature_enabled?(:procedure_process_expired_dossiers_termine),
processed_at: processed_at)
end
end
has_one :traitement, -> { order(processed_at: :desc) }, inverse_of: false
has_many :dossier_operation_logs, -> { order(:created_at) }, inverse_of: :dossier
2018-11-23 21:02:18 +01:00
belongs_to :groupe_instructeur, optional: true
2020-08-27 19:55:10 +02:00
belongs_to :revision, class_name: 'ProcedureRevision', optional: false
2021-05-11 17:49:29 +02:00
belongs_to :user, optional: true
2021-05-12 19:04:31 +02:00
has_one :france_connect_information, through: :user
2020-08-27 19:55:10 +02:00
has_one :procedure, through: :revision
has_many :types_de_champ, through: :revision
has_many :types_de_champ_private, through: :revision
belongs_to :transfer, class_name: 'DossierTransfer', foreign_key: 'dossier_transfer_id', optional: true, inverse_of: :dossiers
has_many :transfer_logs, class_name: 'DossierTransferLog', dependent: :destroy
2017-08-02 14:56:08 +02:00
accepts_nested_attributes_for :champs
2017-08-02 15:33:23 +02:00
accepts_nested_attributes_for :champs_private
2017-08-02 14:56:08 +02:00
2019-07-02 15:38:23 +02:00
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
2021-05-01 12:20:24 +02:00
transitions from: :refuse, to: :en_instruction, guard: :can_repasser_en_instruction?
transitions from: :sans_suite, to: :en_instruction, guard: :can_repasser_en_instruction?
transitions from: :accepte, to: :en_instruction, guard: :can_repasser_en_instruction?
2019-07-02 15:38:23 +02:00
end
end
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)) }
2017-07-11 16:09:03 +02:00
scope :state_en_construction_ou_instruction, -> { where(state: EN_CONSTRUCTION_OU_INSTRUCTION) }
scope :state_instruction_commencee, -> { where(state: INSTRUCTION_COMMENCEE) }
2017-07-11 16:09:03 +02:00
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) }
2019-09-26 14:57:58 +02:00
scope :order_by_created_at, -> (order = :asc) { order(en_construction_at: order, created_at: order, id: order) }
scope :updated_since, -> (since) { where('dossiers.updated_at >= ?', since) }
scope :created_since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
2020-10-30 15:01:13 +01:00
scope :with_type_de_champ, -> (stable_id) {
joins('INNER JOIN champs ON champs.dossier_id = dossiers.id INNER JOIN types_de_champ ON types_de_champ.id = champs.type_de_champ_id')
.where('types_de_champ.private = FALSE AND types_de_champ.stable_id = ?', stable_id)
}
scope :with_type_de_champ_private, -> (stable_id) {
joins('INNER JOIN champs ON champs.dossier_id = dossiers.id INNER JOIN types_de_champ ON types_de_champ.id = champs.type_de_champ_id')
.where('types_de_champ.private = TRUE AND types_de_champ.stable_id = ?', stable_id)
}
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 :processed_in_month, -> (month) do
state_termine
.joins(:traitements)
.where(traitements: { processed_at: month.beginning_of_month..month.end_of_month })
end
scope :downloadable_sorted, -> {
2019-07-30 15:39:20 +02:00
state_not_brouillon
.includes(
:user,
:individual,
:followers_instructeurs,
:traitement,
2021-03-25 12:56:42 +01:00
:groupe_instructeur,
procedure: [
:groupe_instructeurs,
:draft_types_de_champ,
:draft_types_de_champ_private,
:published_types_de_champ,
:published_types_de_champ_private
],
avis: [:claimant, :expert],
etablissement: :champ
2019-07-30 15:39:20 +02:00
).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 :with_champs, -> { includes(champs: :type_de_champ) }
2018-11-01 14:04:32 +01:00
scope :for_api, -> {
includes(commentaires: { piece_jointe_attachment: :blob },
2018-11-01 14:04:32 +01:00
champs: [
:geo_areas,
:etablissement,
2019-07-11 10:28:44 +02:00
piece_justificative_file_attachment: :blob,
champs: [
piece_justificative_file_attachment: :blob
]
2018-11-01 14:04:32 +01:00
],
champs_private: [
:geo_areas,
:etablissement,
2019-07-11 10:28:44 +02:00
piece_justificative_file_attachment: :blob,
champs: [
piece_justificative_file_attachment: :blob
]
2018-11-01 14:04:32 +01:00
],
2019-07-31 16:09:28 +02:00
justificatif_motivation_attachment: :blob,
attestation: [],
avis: { piece_justificative_file_attachment: :blob },
traitement: [],
2018-11-01 14:04:32 +01:00
etablissement: [],
individual: [],
user: [])
}
scope :with_notifiable_procedure, -> (opts = { notify_on_closed: false }) do
states = opts[:notify_on_closed] ? [:publiee, :close, :depubliee] : [:publiee, :depubliee]
joins(:procedure)
.where(procedures: { aasm_state: states })
2021-05-01 12:20:24 +02:00
.where.not(user_id: nil)
end
scope :brouillon_close_to_expiration, -> do
state_brouillon
.joins(:procedure)
.where("dossiers.created_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
2020-02-26 17:36:24 +01:00
end
scope :en_construction_close_to_expiration, -> do
state_en_construction
2020-02-26 17:36:24 +01:00
.joins(:procedure)
.where("dossiers.en_construction_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
2020-02-26 17:36:24 +01:00
scope :en_instruction_close_to_expiration, -> do
state_en_instruction
2020-02-26 17:36:24 +01:00
.joins(:procedure)
.where("dossiers.en_instruction_at + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
2020-02-26 17:36:24 +01:00
end
scope :termine_close_to_expiration, -> do
state_termine
.joins(:procedure)
2021-11-17 10:53:43 +01:00
.where("dossiers.processed_at + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
2020-04-02 15:04:12 +02:00
end
2020-02-26 17:36:24 +01:00
scope :brouillon_expired, -> do
state_brouillon
.where("brouillon_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
end
scope :en_construction_expired, -> do
state_en_construction
.where("en_construction_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
end
2020-04-02 15:04:12 +02:00
scope :termine_expired, -> do
state_termine
.where("termine_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
end
2020-02-26 17:36:24 +01:00
scope :without_brouillon_expiration_notice_sent, -> { where(brouillon_close_to_expiration_notice_sent_at: nil) }
scope :without_en_construction_expiration_notice_sent, -> { where(en_construction_close_to_expiration_notice_sent_at: nil) }
2020-04-02 15:04:12 +02:00
scope :without_termine_expiration_notice_sent, -> { where(termine_close_to_expiration_notice_sent_at: nil) }
2020-03-19 13:11:45 +01:00
scope :discarded_brouillon_expired, -> do
with_discarded
.discarded
.state_brouillon
2020-11-26 15:13:32 +01:00
.where('hidden_at < ?', 1.week.ago)
2020-03-19 13:11:45 +01:00
end
scope :discarded_en_construction_expired, -> do
with_discarded
.discarded
.state_en_construction
2020-11-26 15:13:32 +01:00
.where('dossiers.hidden_at < ?', 1.week.ago)
end
scope :discarded_termine_expired, -> do
with_discarded
.discarded
.state_termine
.where('dossiers.hidden_at < ?', 1.week.ago)
2020-03-19 13:11:45 +01:00
end
scope :brouillon_near_procedure_closing_date, -> do
# select users who have submitted dossier for the given 'procedures.id'
users_who_submitted =
state_not_brouillon
.joins(:revision)
.where("procedure_revisions.procedure_id = procedures.id")
.select(:user_id)
# select dossier in brouillon where procedure closes in two days and for which the user has not submitted a Dossier
state_brouillon
.with_notifiable_procedure
.where("procedures.auto_archive_on - INTERVAL :before_closing = :now", { now: Time.zone.today, before_closing: INTERVAL_BEFORE_CLOSING })
.where.not(user: users_who_submitted)
end
scope :for_api_v2, -> { includes(procedure: [:administrateurs, :attestation_template], etablissement: [], individual: [], traitement: []) }
2020-09-18 15:40:26 +02:00
scope :with_notifications, -> do
joins(:follows)
.where('last_champ_updated_at > follows.demande_seen_at' \
' OR identity_updated_at > follows.demande_seen_at' \
' OR groupe_instructeur_updated_at > follows.demande_seen_at' \
' OR last_champ_private_updated_at > follows.annotations_privees_seen_at' \
' OR last_avis_updated_at > follows.avis_seen_at' \
' OR last_commentaire_updated_at > follows.messagerie_seen_at')
.distinct
end
2016-08-30 11:18:43 +02:00
accepts_nested_attributes_for :individual
delegate :siret, :siren, to: :etablissement, allow_nil: true
2021-05-12 19:04:31 +02:00
delegate :france_connect_information, to: :user, allow_nil: true
before_save :build_default_champs, if: Proc.new { revision_id_was.nil? }
before_save :update_search_terms
2017-03-01 09:51:55 +01:00
2018-03-01 17:04:05 +01:00
after_save :send_web_hook
2020-09-28 17:38:49 +02:00
after_create_commit :send_draft_notification_email
2021-05-11 17:49:29 +02:00
validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }
2020-08-27 19:55:10 +02:00
validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
validates :groupe_instructeur, presence: true, if: -> { !brouillon? }
2015-08-21 11:37:13 +02:00
EXPORT_BATCH_SIZE = 5000
def self.downloadable_sorted_batch
dossiers = downloadable_sorted.to_a
(dossiers.size.to_f / EXPORT_BATCH_SIZE).ceil.times do |i|
start_index = i * EXPORT_BATCH_SIZE
end_index = start_index + EXPORT_BATCH_SIZE - 1
load_champs(dossiers[start_index..end_index])
end
dossiers
end
def self.load_champs(dossiers)
::ActiveRecord::Associations::Preloader.new.preload(dossiers, {
champs: {
type_de_champ: [],
etablissement: :champ,
piece_justificative_file_attachment: :blob,
champs: [
type_de_champ: [],
piece_justificative_file_attachment: :blob
]
},
champs_private: {
type_de_champ: [],
etablissement: :champ,
piece_justificative_file_attachment: :blob,
champs: [
type_de_champ: [],
piece_justificative_file_attachment: :blob
]
}
})
end
2021-05-01 12:20:24 +02:00
def user_deleted?
2021-09-08 09:21:06 +02:00
persisted? && user_id.nil?
2021-05-01 12:20:24 +02:00
end
def user_email_for(use)
if user_deleted?
if use == :display
deleted_user_email_never_send
else
raise "Can not send email to discarded user"
end
else
user.email
end
end
def motivation
return nil if !termine?
traitement&.motivation || read_attribute(:motivation)
end
def processed_at
return nil if !termine?
traitement&.processed_at || read_attribute(:processed_at)
end
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 build_default_champs
2020-08-27 19:55:10 +02:00
revision.build_champs.each do |champ|
2019-01-30 16:14:15 +01:00
champs << champ
end
2020-08-27 19:55:10 +02:00
revision.build_champs_private.each do |champ|
2019-02-07 10:44:15 +01:00
champs_private << champ
end
end
2016-08-30 11:18:43 +02:00
def build_default_individual
if procedure.for_individual? && individual.blank?
self.individual = if france_connect_information.present?
Individual.from_france_connect(france_connect_information)
else
Individual.new
end
end
2016-08-30 11:18:43 +02:00
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
2018-02-08 17:13:15 +01:00
update_columns(autorisation_donnees: false)
end
2016-09-13 12:17:56 +02:00
def read_only?
en_instruction? || accepte? || refuse? || sans_suite?
2016-09-13 12:17:56 +02:00
end
def can_transition_to_en_construction?
2019-12-04 15:45:06 +01:00
brouillon? && procedure.dossier_can_transition_to_en_construction?
end
2021-05-01 12:20:24 +02:00
def can_repasser_en_instruction?
termine? && !user_deleted?
end
2019-02-06 18:20:35 +01:00
def can_be_updated_by_user?
brouillon? || en_construction?
end
def can_be_deleted_by_user?
brouillon? || en_construction?
end
def can_be_deleted_by_manager?
kept? && can_be_deleted_by_user?
end
def messagerie_available?
2021-05-01 12:20:24 +02:00
!brouillon? && !user_deleted? && !archived
end
2021-11-17 10:53:43 +01:00
def close_to_expiration_at
if brouillon?
created_at
elsif en_construction?
en_construction
elsif en_instruction?
en_instruction
else
processed_at
end + conservation_extension + duree_conservation_dossiers_dans_ds.months - REMAINING_WEEKS_BEFORE_EXPIRATION.weeks
end
2021-11-17 10:53:43 +01:00
def duration_after_notice
MONTHS_AFTER_EXPIRATION.month + DAYS_AFTER_EXPIRATION.days
end
def expiration_date
if brouillon? && brouillon_close_to_expiration_notice_sent_at.present?
brouillon_close_to_expiration_notice_sent_at + duration_after_notice
elsif en_construction? && en_construction_close_to_expiration_notice_sent_at.present?
en_construction_close_to_expiration_notice_sent_at + duration_after_notice
elsif termine? && termine_close_to_expiration_notice_sent_at.present?
termine_close_to_expiration_notice_sent_at + duration_after_notice
end
end
def close_to_expiration?
2021-11-17 10:53:43 +01:00
!en_instruction? && close_to_expiration_at < Time.zone.now
end
def close_to_expiration_notice_sent?
expiration_date.present?
end
def expiration_can_be_extended?
brouillon? || en_construction?
end
def show_groupe_instructeur_details?
procedure.routee? && groupe_instructeur.present? && (!procedure.feature_enabled?(:procedure_routage_api) || !defaut_groupe_instructeur?)
end
def show_groupe_instructeur_selector?
procedure.routee? && !procedure.feature_enabled?(:procedure_routage_api)
end
def assign_to_groupe_instructeur(groupe_instructeur, author = nil)
if (groupe_instructeur.nil? || groupe_instructeur.procedure == procedure) && self.groupe_instructeur != groupe_instructeur
if update(groupe_instructeur: groupe_instructeur, groupe_instructeur_updated_at: Time.zone.now)
unfollow_stale_instructeurs
if author.present?
log_dossier_operation(author, :changer_groupe_instructeur, self)
end
true
end
else
false
end
end
def archiver!(author)
update!(archived: true)
log_dossier_operation(author, :archiver)
end
def desarchiver!(author)
update!(archived: false)
log_dossier_operation(author, :desarchiver)
end
2017-04-18 17:31:01 +02:00
def text_summary
if brouillon?
parts = [
2018-09-05 14:48:42 +02:00
"Dossier en brouillon répondant à la démarche ",
2017-04-18 17:31:01 +02:00
procedure.libelle,
" gérée par l'organisme ",
procedure.organisation_name
2017-04-18 17:31:01 +02:00
]
else
parts = [
"Dossier déposé le ",
en_construction_at.strftime("%d/%m/%Y"),
2018-09-05 14:48:42 +02:00
" sur la démarche ",
2017-04-18 17:31:01 +02:00
procedure.libelle,
" gérée par l'organisme ",
procedure.organisation_name
2017-04-18 17:31:01 +02:00
]
end
parts.join
end
def avis_for_instructeur(instructeur)
if instructeur.dossiers.include?(self)
2017-09-08 11:52:20 +02:00
avis.order(created_at: :asc)
else
avis
.where(confidentiel: false)
2021-04-07 19:53:18 +02:00
.or(avis.where(claimant: instructeur))
2017-09-08 11:52:20 +02:00
.order(created_at: :asc)
end
end
def avis_for_expert(expert)
Avis
.where(dossier_id: id, confidentiel: false)
2021-05-20 16:15:59 +02:00
.or(Avis.where(id: expert.avis, dossier_id: id))
.order(created_at: :asc)
end
2017-11-17 23:40:51 +01:00
def owner_name
if etablissement.present?
etablissement.entreprise_raison_sociale
2017-11-17 23:40:51 +01:00
elsif individual.present?
"#{individual.nom} #{individual.prenom}"
end
end
def log_operations?
!procedure.brouillon? && !brouillon?
end
def keep_track_on_deletion?
!procedure.brouillon? && !brouillon?
end
2018-10-31 13:28:39 +01:00
def expose_legacy_carto_api?
procedure.expose_legacy_carto_api?
end
2018-10-10 19:58:51 +02:00
def geo_position
if etablissement.present?
2020-01-14 19:00:17 +01:00
point = Geocoder.search(etablissement.geo_adresse).first
2018-10-10 19:58:51 +02:00
end
lon = Champs::CarteChamp::DEFAULT_LON.to_s
lat = Champs::CarteChamp::DEFAULT_LAT.to_s
2018-10-10 19:58:51 +02:00
zoom = "13"
if point.present?
2020-01-14 19:00:17 +01:00
lat, lon = point.coordinates.map(&:to_s)
2018-10-10 19:58:51 +02:00
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 expired_keep_track_and_destroy!
transaction do
if keep_track_on_deletion?
DeletedDossier.create_from_dossier(self, :expired)
dossier_operation_logs.destroy_all
log_automatic_dossier_operation(:supprimer, self)
end
destroy!
end
true
rescue
false
end
2020-11-26 15:13:32 +01:00
def discard_and_keep_track!(author, reason)
user_email = user_deleted? ? nil : user_email_for(:notification)
deleted_dossier = nil
transaction do
if keep_track_on_deletion?
log_dossier_operation(author, :supprimer, self)
2020-11-26 15:13:32 +01:00
deleted_dossier = DeletedDossier.create_from_dossier(self, reason)
end
update!(dossier_transfer_id: nil)
discard!
end
if deleted_dossier.present?
if en_construction?
2020-11-26 15:13:32 +01:00
administration_emails = followers_instructeurs.present? ? followers_instructeurs.map(&:email) : procedure.administrateurs.map(&:email)
administration_emails.each do |email|
DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
end
end
2021-05-01 12:20:24 +02:00
if user_email.present?
if reason == :user_request
DossierMailer.notify_deletion_to_user(deleted_dossier, user_email).deliver_later
else
DossierMailer.notify_instructeur_deletion_to_user(deleted_dossier, user_email).deliver_later
2021-05-01 12:20:24 +02:00
end
2020-11-26 15:13:32 +01:00
end
end
end
def restore(author)
if discarded?
transaction do
if undiscard && keep_track_on_deletion?
deleted_dossier&.destroy!
log_dossier_operation(author, :restaurer, self)
end
end
end
end
def restore_if_discarded_with_procedure(author)
if deleted_dossier&.procedure_removed?
restore(author)
end
end
def after_passer_en_construction
self.conservation_extension = 0.days
self.en_construction_at = self.traitements
.passer_en_construction
.processed_at
save!
end
def after_passer_en_instruction(instructeur, disable_notification: false)
instructeur.follow(self)
2018-11-26 21:29:06 +01:00
self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = self.traitements
.passer_en_instruction(instructeur: instructeur)
.processed_at
save!
if !procedure.declarative_accepte? && !disable_notification
NotificationMailer.send_en_instruction_notification(self).deliver_later
end
log_dossier_operation(instructeur, :passer_en_instruction)
end
2019-07-02 15:38:23 +02:00
def after_passer_automatiquement_en_instruction
self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = self.declarative_triggered_at = self.traitements
.passer_en_instruction
.processed_at
save!
log_automatic_dossier_operation(:passer_en_instruction)
end
def after_repasser_en_construction(instructeur)
self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_construction_at = self.traitements
.passer_en_construction(instructeur: instructeur)
.processed_at
save!
log_dossier_operation(instructeur, :repasser_en_construction)
end
def after_repasser_en_instruction(instructeur, disable_notification: false)
self.archived = false
self.termine_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days
self.en_instruction_at = self.traitements
.passer_en_instruction(instructeur: instructeur)
.processed_at
attestation&.destroy
2019-07-02 15:38:23 +02:00
save!
if !disable_notification
DossierMailer.notify_revert_to_instruction(self).deliver_later
end
log_dossier_operation(instructeur, :repasser_en_instruction)
end
def after_accepter(instructeur, motivation, justificatif: nil, disable_notification: false)
self.processed_at = self.traitements
.accepter(motivation: motivation, instructeur: instructeur)
.processed_at
save!
2019-07-02 15:38:23 +02:00
if justificatif
self.justificatif_motivation.attach(justificatif)
end
if attestation.nil?
2019-07-02 15:38:23 +02:00
self.attestation = build_attestation
end
2019-07-02 15:38:23 +02:00
save!
remove_titres_identite!
if !disable_notification
NotificationMailer.send_accepte_notification(self).deliver_later
end
send_dossier_decision_to_experts(self)
log_dossier_operation(instructeur, :accepter, self)
end
2019-07-02 15:38:23 +02:00
def after_accepter_automatiquement
self.processed_at = self.en_instruction_at = self.declarative_triggered_at = self.traitements
.accepter_automatiquement
.processed_at
save!
2019-01-16 11:00:25 +01:00
if attestation.nil?
2019-07-02 15:38:23 +02:00
self.attestation = build_attestation
2019-01-16 11:00:25 +01:00
end
2019-07-02 15:38:23 +02:00
save!
remove_titres_identite!
2021-04-29 19:10:22 +02:00
NotificationMailer.send_accepte_notification(self).deliver_later
log_automatic_dossier_operation(:accepter, self)
2019-01-16 11:00:25 +01:00
end
def after_refuser(instructeur, motivation, justificatif: nil, disable_notification: false)
self.processed_at = self.traitements
.refuser(motivation: motivation, instructeur: instructeur)
.processed_at
save!
2019-07-02 15:38:23 +02:00
if justificatif
self.justificatif_motivation.attach(justificatif)
end
2019-07-02 15:38:23 +02:00
save!
remove_titres_identite!
if !disable_notification
NotificationMailer.send_refuse_notification(self).deliver_later
end
send_dossier_decision_to_experts(self)
log_dossier_operation(instructeur, :refuser, self)
end
def after_classer_sans_suite(instructeur, motivation, justificatif: nil, disable_notification: false)
self.processed_at = self.traitements
.classer_sans_suite(motivation: motivation, instructeur: instructeur)
.processed_at
save!
2019-07-02 15:38:23 +02:00
if justificatif
self.justificatif_motivation.attach(justificatif)
end
2019-07-02 15:38:23 +02:00
save!
remove_titres_identite!
if !disable_notification
NotificationMailer.send_sans_suite_notification(self).deliver_later
end
send_dossier_decision_to_experts(self)
log_dossier_operation(instructeur, :classer_sans_suite, self)
end
def remove_titres_identite!
champs.filter(&:titre_identite?).map(&:piece_justificative_file).each(&:purge_later)
end
2019-01-30 16:14:15 +01:00
def check_mandatory_champs
2019-09-12 11:26:22 +02:00
(champs + champs.filter(&:repetition?).flat_map(&:champs))
.filter(&:mandatory_and_blank?)
2019-01-30 16:14:15 +01:00
.map do |champ|
"Le champ #{champ.libelle.truncate(200)} doit être rempli."
end
end
def log_modifier_annotations!(instructeur)
2019-09-12 11:26:22 +02:00
champs_private.filter(&:value_previously_changed?).each do |champ|
log_dossier_operation(instructeur, :modifier_annotation, champ)
end
end
2019-05-02 16:24:24 +02:00
2020-07-28 18:16:03 +02:00
def log_modifier_annotation!(champ, instructeur)
log_dossier_operation(instructeur, :modifier_annotation, champ)
end
2019-05-02 16:24:24 +02:00
def demander_un_avis!(avis)
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
def spreadsheet_columns_csv(types_de_champ:)
spreadsheet_columns(with_etablissement: true, types_de_champ: types_de_champ)
end
def spreadsheet_columns_xlsx(types_de_champ:)
spreadsheet_columns(types_de_champ: types_de_champ)
end
def spreadsheet_columns_ods(types_de_champ:)
spreadsheet_columns(types_de_champ: types_de_champ)
end
def spreadsheet_columns(with_etablissement: false, types_de_champ:)
columns = [
2019-04-03 14:29:30 +02:00
['ID', id.to_s],
2021-05-01 12:20:24 +02:00
['Email', user_email_for(:display)]
]
if procedure.for_individual?
columns += [
['Civilité', individual&.gender],
['Nom', individual&.nom],
['Prénom', individual&.prenom]
]
if procedure.ask_birthday
columns += [['Date de naissance', individual&.birthdate]]
end
elsif with_etablissement
columns += [
['Établissement SIRET', etablissement&.siret],
['Établissement siège social', etablissement&.siege_social],
['Établissement NAF', etablissement&.naf],
['Établissement libellé NAF', etablissement&.libelle_naf],
['Établissement Adresse', etablissement&.adresse],
['Établissement numero voie', etablissement&.numero_voie],
['Établissement type voie', etablissement&.type_voie],
['Établissement nom voie', etablissement&.nom_voie],
['Établissement complément adresse', etablissement&.complement_adresse],
['Établissement code postal', etablissement&.code_postal],
['Établissement localité', etablissement&.localite],
['Établissement code INSEE localité', etablissement&.code_insee_localite],
['Entreprise SIREN', etablissement&.entreprise_siren],
['Entreprise capital social', etablissement&.entreprise_capital_social],
['Entreprise numero TVA intracommunautaire', etablissement&.entreprise_numero_tva_intracommunautaire],
['Entreprise forme juridique', etablissement&.entreprise_forme_juridique],
['Entreprise forme juridique code', etablissement&.entreprise_forme_juridique_code],
['Entreprise nom commercial', etablissement&.entreprise_nom_commercial],
['Entreprise raison sociale', etablissement&.entreprise_raison_sociale],
['Entreprise SIRET siège social', etablissement&.entreprise_siret_siege_social],
['Entreprise code effectif entreprise', etablissement&.entreprise_code_effectif_entreprise],
['Entreprise date de création', etablissement&.entreprise_date_creation],
['Entreprise nom', etablissement&.entreprise_nom],
['Entreprise prénom', etablissement&.entreprise_prenom],
['Association RNA', etablissement&.association_rna],
['Association titre', etablissement&.association_titre],
['Association objet', etablissement&.association_objet],
['Association date de création', etablissement&.association_date_creation],
['Association date de déclaration', etablissement&.association_date_declaration],
['Association date de publication', etablissement&.association_date_publication]
]
else
columns << ['Entreprise raison sociale', etablissement&.entreprise_raison_sociale]
end
columns += [
2019-04-03 14:29:30 +02:00
['Archivé', :archived],
2021-05-10 11:00:57 +02:00
['État du dossier', Dossier.human_attribute_name("state.#{state}")],
2019-04-03 14:29:30 +02:00
['Dernière mise à jour le', :updated_at],
['Déposé le', :en_construction_at],
2019-07-04 15:02:25 +02:00
['Passé en instruction le', :en_instruction_at],
2019-04-03 14:29:30 +02:00
['Traité le', :processed_at],
['Motivation de la décision', :motivation],
['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
]
if procedure.routee?
columns << ['Groupe instructeur', groupe_instructeur.label]
end
columns + self.class.champs_for_export(champs + champs_private, types_de_champ)
2019-04-03 14:29:30 +02:00
end
def self.champs_for_export(champs, types_de_champ)
# Index values by stable_id
values = champs.reject(&:exclude_from_export?)
2021-08-18 14:02:40 +02:00
.index_by(&:stable_id)
.transform_values(&:for_export)
# Get all the champs values for the types de champ in the final list.
# Dossier might not have corresponding champ display nil.
2021-08-18 14:02:40 +02:00
types_de_champ.flat_map do |type_de_champ|
Array.wrap(values[type_de_champ.stable_id] || [nil]).map.with_index do |champ_value, index|
[type_de_champ.libelle_for_export(index), champ_value]
2021-08-18 14:02:40 +02:00
end
2019-04-03 14:29:30 +02:00
end
end
def export_and_attachments_downloadable?
PiecesJustificativesService.pieces_justificatives_total_size(self) < Dossier::TAILLE_MAX_ZIP
end
def linked_dossiers_for(instructeur_or_expert)
dossier_ids = champs.filter(&:dossier_link?).filter_map(&:value)
instructeur_or_expert.dossiers.where(id: dossier_ids)
end
2019-11-28 18:03:23 +01:00
def hash_for_deletion_mail
{ id: self.id, procedure_libelle: self.procedure.libelle }
end
def geo_data?
geo_areas.present?
end
def to_feature_collection
{
type: 'FeatureCollection',
id: id,
bbox: bounding_box,
features: geo_areas.map(&:to_feature)
}
end
def log_api_entreprise_job_exception(exception)
exceptions = self.api_entreprise_job_exceptions ||= []
exceptions << exception.inspect
update_column(:api_entreprise_job_exceptions, exceptions)
end
def user_locale
user&.locale || I18n.default_locale
end
2021-10-21 13:29:47 +02:00
def self.purge_discarded
discarded_brouillon_expired.destroy_all
transaction do
DossierOperationLog.discarded_en_construction_expired.destroy_all
Avis.discarded_en_construction_expired.destroy_all
2021-10-21 13:29:47 +02:00
discarded_en_construction_expired.destroy_all
end
transaction do
DossierOperationLog.discarded_termine_expired.destroy_all
Avis.discarded_termine_expired.destroy_all
discarded_termine_expired.destroy_all
end
end
2017-12-05 17:43:32 +01:00
private
def deleted_dossier
@deleted_dossier ||= DeletedDossier.find_by(dossier_id: id)
end
def defaut_groupe_instructeur?
groupe_instructeur == procedure.defaut_groupe_instructeur
end
def geo_areas
champs.includes(:geo_areas).flat_map(&:geo_areas) + champs_private.includes(:geo_areas).flat_map(&:geo_areas)
end
def bounding_box
factory = RGeo::Geographic.simple_mercator_factory
bounding_box = RGeo::Cartesian::BoundingBox.new(factory)
geo_areas.filter_map(&:rgeo_geometry).each do |geometry|
bounding_box.add(geometry)
end
[bounding_box.max_point, bounding_box.min_point].compact.flat_map(&:coordinates)
end
def log_dossier_operation(author, operation, subject = nil)
if log_operations?
DossierOperationLog.create_and_serialize(
dossier: self,
operation: DossierOperationLog.operations.fetch(operation),
author: author,
subject: subject
)
end
2018-11-26 21:29:06 +01:00
end
def log_automatic_dossier_operation(operation, subject = nil)
if log_operations?
DossierOperationLog.create_and_serialize(
dossier: self,
operation: DossierOperationLog.operations.fetch(operation),
automatic_operation: true,
subject: subject
)
end
end
def send_draft_notification_email
if brouillon? && !procedure.declarative?
DossierMailer.notify_new_draft(self).deliver_later
end
end
2018-03-01 17:04:05 +01:00
def send_web_hook
if saved_change_to_state? && !brouillon? && procedure.web_hook_url.present?
2018-03-01 17:04:05 +01:00
WebHookJob.perform_later(
procedure,
self
)
end
end
def unfollow_stale_instructeurs
followers_instructeurs.each do |instructeur|
if instructeur.groupe_instructeurs.exclude?(groupe_instructeur)
instructeur.unfollow(self)
if kept?
DossierMailer.notify_groupe_instructeur_changed(instructeur, self).deliver_later
end
end
end
end
def self.notify_draft_not_submitted
brouillon_near_procedure_closing_date
.includes(:user)
.find_each do |dossier|
2020-03-19 03:46:12 +01:00
DossierMailer.notify_brouillon_not_submitted(dossier).deliver_later
end
end
def send_dossier_decision_to_experts(dossier)
avis_experts_procedures_ids = Avis
.joins(:experts_procedure)
.where(dossier: dossier, experts_procedures: { allow_decision_access: true })
.with_answer
.distinct
.pluck('avis.id, experts_procedures.id')
# rubocop:disable Lint/UnusedBlockArgument
avis_ids = avis_experts_procedures_ids
.uniq { |(avis_id, experts_procedures_id)| experts_procedures_id }
.map { |(avis_id, _)| avis_id }
# rubocop:enable Lint/UnusedBlockArgument
avis_ids.each { |avis_id| ExpertMailer.send_dossier_decision(avis_id).deliver_later }
end
2015-08-10 11:05:06 +02:00
end