diff --git a/README.md b/README.md index 059f3fdf9..668d3409d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ L'application tourne à l'adresse `http://localhost:3000`. Un utilisateur de tes AutoReceiveDossiersForProcedureJob.set(cron: "* * * * *").perform_later(procedure_declaratoire_id, Dossier.states.fetch(:en_instruction)) FindDubiousProceduresJob.set(cron: "0 0 * * *").perform_later Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later + WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later ## Mise à jour de l'application diff --git a/app/jobs/warn_expiring_dossiers_job.rb b/app/jobs/warn_expiring_dossiers_job.rb new file mode 100644 index 000000000..c20789133 --- /dev/null +++ b/app/jobs/warn_expiring_dossiers_job.rb @@ -0,0 +1,12 @@ +class WarnExpiringDossiersJob < ApplicationJob + queue_as :cron + + def perform(*args) + expiring, expired = Dossier + .includes(:procedure) + .nearing_end_of_retention + .partition(&:retention_expired?) + + AdministrationMailer.dossier_expiration_summary(expiring, expired).deliver_later + end +end diff --git a/app/mailers/administration_mailer.rb b/app/mailers/administration_mailer.rb index e84f70cb6..36bb5d477 100644 --- a/app/mailers/administration_mailer.rb +++ b/app/mailers/administration_mailer.rb @@ -36,4 +36,22 @@ class AdministrationMailer < ApplicationMailer mail(to: EQUIPE_EMAIL, subject: subject) end + + def dossier_expiration_summary(expiring_dossiers, expired_dossiers) + subject = + if expired_dossiers.present? && expiring_dossiers.present? + "Des dossiers ont dépassé leur délai de conservation, et d’autres en approchent" + elsif expired_dossiers.present? + "Des dossiers ont dépassé leur délai de conservation" + elsif expiring_dossiers.present? + "Des dossiers approchent de la fin de leur délai de conservation" + else + "Aucun dossier en fin de délai de conservation" + end + + @expiring_dossiers = expiring_dossiers + @expired_dossiers = expired_dossiers + + mail(to: TECH_EMAIL, subject: subject) + end end diff --git a/app/models/champs/number_champ.rb b/app/models/champs/number_champ.rb index 8903284e2..8f3f089fd 100644 --- a/app/models/champs/number_champ.rb +++ b/app/models/champs/number_champ.rb @@ -1,2 +1,6 @@ class Champs::NumberChamp < Champ + validates :value, numericality: { message: Proc.new { |champ, _| "#{champ.libelle} doit être un nombre" } } + validates :value, + numericality: { only_integer: true, message: Proc.new { |champ, _| "#{champ.libelle} doit être un nombre entier" } }, + if: Proc.new{ |object| object.errors.empty? } end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 48ab975db..ef4ad39b6 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -61,6 +61,7 @@ class Dossier < ApplicationRecord 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) } accepts_nested_attributes_for :individual @@ -182,6 +183,16 @@ class Dossier < ApplicationRecord brouillon? || en_construction? 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 <= DateTime.now + end + def text_summary if brouillon? parts = [ diff --git a/app/views/administration_mailer/dossier_expiration_summary.html.haml b/app/views/administration_mailer/dossier_expiration_summary.html.haml new file mode 100644 index 000000000..dc9488ca3 --- /dev/null +++ b/app/views/administration_mailer/dossier_expiration_summary.html.haml @@ -0,0 +1,22 @@ +- content_for(:title, 'Expiration du délai de conservation des dossiers') + +- if @expired_dossiers.present? + %h1= t('mail.administration.dossier_expiration_summary.expired_dossiers', count: @expired_dossiers.count) + - @expired_dossiers.group_by(&:procedure).each do |procedure, dossiers| + %dl + %dt + #{procedure.libelle} (#{link_to(procedure.id, manager_procedure_url(procedure))}) : + %dd + = dossiers.map { |d| link_to(d.id, manager_dossier_url(d)) }.join(', ').html_safe + +- if @expiring_dossiers.present? + %h1= t('mail.administration.dossier_expiration_summary.expiring_dossiers', count: @expiring_dossiers.count) + - @expiring_dossiers.group_by(&:procedure).each do |procedure, dossiers| + %dl + %dt + #{procedure.libelle} (#{link_to(procedure.id, manager_procedure_url(procedure))}) : + %dd + = dossiers.map { |d| link_to(d.id, manager_dossier_url(d)) }.join(', ').html_safe + +- if @expired_dossiers.empty? && @expiring_dossiers.empty? + Il n’y a pas de dossier expiré ou sur le point d’expirer. diff --git a/app/views/new_gestionnaire/dossiers/print.html.haml b/app/views/new_gestionnaire/dossiers/print.html.haml index 9bcb22e0d..4e0c5e62f 100644 --- a/app/views/new_gestionnaire/dossiers/print.html.haml +++ b/app/views/new_gestionnaire/dossiers/print.html.haml @@ -43,7 +43,7 @@ %td - pj = @dossier.retrieve_last_piece_justificative_by_type(type_de_piece_justificative.id) - if pj.present? - Pièce fournie + #{pj.original_filename} - else Pièce non fournie diff --git a/app/views/new_user/dossiers/brouillon.html.haml b/app/views/new_user/dossiers/brouillon.html.haml index 77dec858b..03296a576 100644 --- a/app/views/new_user/dossiers/brouillon.html.haml +++ b/app/views/new_user/dossiers/brouillon.html.haml @@ -3,9 +3,8 @@ - content_for :footer do = render partial: "new_user/dossiers/dossier_footer", locals: { dossier: @dossier } -.dossier-edit - .dossier-header.sub-header - .container - = render partial: "shared/dossiers/header", locals: { dossier: @dossier, apercu: false } +.dossier-header.sub-header + .container + = render partial: "shared/dossiers/header", locals: { dossier: @dossier, apercu: false } - = render partial: "shared/dossiers/edit", locals: { dossier: @dossier, apercu: false } += render partial: "shared/dossiers/edit", locals: { dossier: @dossier, apercu: false } diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index dec841f67..9dc1faa5d 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -1,4 +1,4 @@ -.container +.dossier-edit.container = render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier } - if apercu diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 638b058ab..444191bca 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -36,6 +36,16 @@ fr: apipie: api_documentation: "Documentation de l'API demarches-simplifiees.fr" + mail: + administration: + dossier_expiration_summary: + expired_dossiers: + one: "Un dossier a passé sa date limite de conservation" + other: "%{count} dossiers ont passé leur date limite de conservation" + expiring_dossiers: + one: "Un dossier est sur le point de passer sa date limite de conservation" + other: "%{count} dossiers sont sur le point de passer leur date limite de conservation" + modal: publish: title: @@ -142,7 +152,9 @@ fr: taken: déjà utilisé password: too_short: ': Le mot de passe est trop court' - + attributes: + champs: + value: Le champ errors: messages: already_confirmed: "a déjà été validé(e), veuillez essayer de vous connecter" diff --git a/config/locales/models/type_de_champ/fr.yml b/config/locales/models/type_de_champ/fr.yml index aa6a33b41..655429494 100644 --- a/config/locales/models/type_de_champ/fr.yml +++ b/config/locales/models/type_de_champ/fr.yml @@ -9,7 +9,7 @@ fr: textarea: 'Zone de texte' date: 'Date' datetime: 'Date et Heure' - number: 'Nombre' + number: 'Nombre entier' checkbox: 'Case à cocher' civilite: 'Civilité' email: 'Email' diff --git a/lib/tasks/2018_08_31_monthly_dossier_expiration_summary.rake b/lib/tasks/2018_08_31_monthly_dossier_expiration_summary.rake new file mode 100644 index 000000000..7eadfff34 --- /dev/null +++ b/lib/tasks/2018_08_31_monthly_dossier_expiration_summary.rake @@ -0,0 +1,7 @@ +require Rails.root.join("lib", "tasks", "task_helper") + +namespace :'2018_08_31_monthly_dossier_expiration_summary' do + task enable: :environment do + WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later + end +end diff --git a/spec/factories/dossier.rb b/spec/factories/dossier.rb index 8c2fe14fa..7449915f8 100644 --- a/spec/factories/dossier.rb +++ b/spec/factories/dossier.rb @@ -80,7 +80,7 @@ FactoryBot.define do trait :en_construction do after(:create) do |dossier, _evaluator| dossier.state = Dossier.states.fetch(:en_construction) - dossier.en_construction_at = dossier.created_at + 1.minute + dossier.en_construction_at ||= dossier.created_at + 1.minute dossier.save! end end @@ -88,8 +88,8 @@ FactoryBot.define do trait :en_instruction do after(:create) do |dossier, _evaluator| dossier.state = Dossier.states.fetch(:en_instruction) - dossier.en_construction_at = dossier.created_at + 1.minute - dossier.created_at = dossier.created_at + 2.minutes + dossier.en_construction_at ||= dossier.created_at + 1.minute + dossier.en_instruction_at ||= dossier.en_construction_at + 1.minute dossier.save! end end @@ -97,9 +97,9 @@ FactoryBot.define do trait :accepte do after(:create) do |dossier, _evaluator| dossier.state = Dossier.states.fetch(:accepte) - dossier.processed_at ||= dossier.created_at + 1.minute - dossier.en_construction_at ||= dossier.created_at + 2.minutes - dossier.created_at ||= dossier.created_at + 3.minutes + dossier.en_construction_at ||= dossier.created_at + 1.minute + dossier.en_instruction_at ||= dossier.en_construction_at + 1.minute + dossier.processed_at ||= dossier.en_instruction_at + 1.minute dossier.save! end end @@ -107,9 +107,9 @@ FactoryBot.define do trait :refuse do after(:create) do |dossier, _evaluator| dossier.state = Dossier.states.fetch(:refuse) - dossier.processed_at = dossier.created_at + 1.minute - dossier.en_construction_at = dossier.created_at + 2.minutes - dossier.created_at = dossier.created_at + 3.minutes + dossier.en_construction_at ||= dossier.created_at + 1.minute + dossier.en_instruction_at ||= dossier.en_construction_at + 1.minute + dossier.processed_at ||= dossier.en_instruction_at + 1.minute dossier.save! end end @@ -117,9 +117,9 @@ FactoryBot.define do trait :sans_suite do after(:create) do |dossier, _evaluator| dossier.state = Dossier.states.fetch(:sans_suite) - dossier.processed_at = dossier.created_at + 1.minute - dossier.en_construction_at = dossier.created_at + 2.minutes - dossier.created_at = dossier.created_at + 3.minutes + dossier.en_construction_at ||= dossier.created_at + 1.minute + dossier.en_instruction_at ||= dossier.en_construction_at + 1.minute + dossier.processed_at ||= dossier.en_instruction_at + 1.minute dossier.save! end end diff --git a/spec/jobs/auto_receive_dossiers_for_procedure_job_spec.rb b/spec/jobs/auto_receive_dossiers_for_procedure_job_spec.rb index 99c58c1d9..e7fa7b6f4 100644 --- a/spec/jobs/auto_receive_dossiers_for_procedure_job_spec.rb +++ b/spec/jobs/auto_receive_dossiers_for_procedure_job_spec.rb @@ -3,12 +3,15 @@ require 'rails_helper' RSpec.describe AutoReceiveDossiersForProcedureJob, type: :job do describe "perform" do let(:date) { Time.utc(2017, 9, 1, 10, 5, 0) } + let(:instruction_date) { date + 120 } + + before do + Timecop.freeze(date) + AutoReceiveDossiersForProcedureJob.new.perform(procedure_id, state) + end - before { Timecop.freeze(date) } after { Timecop.return } - subject { AutoReceiveDossiersForProcedureJob.new.perform(procedure_id, state) } - context "with some dossiers" do let(:nouveau_dossier1) { create(:dossier, :en_construction) } let(:nouveau_dossier2) { create(:dossier, :en_construction, procedure: nouveau_dossier1.procedure) } @@ -19,43 +22,37 @@ RSpec.describe AutoReceiveDossiersForProcedureJob, type: :job do context "en_construction" do let(:state) { Dossier.states.fetch(:en_instruction) } - it do - subject - expect(nouveau_dossier1.reload.en_instruction?).to be true - expect(nouveau_dossier1.reload.en_instruction_at).to eq(date) + it { expect(nouveau_dossier1.reload.en_instruction?).to be true } + it { expect(nouveau_dossier1.reload.en_instruction_at).to eq(date) } - expect(nouveau_dossier2.reload.en_instruction?).to be true - expect(nouveau_dossier2.reload.en_instruction_at).to eq(date) + it { expect(nouveau_dossier2.reload.en_instruction?).to be true } + it { expect(nouveau_dossier2.reload.en_instruction_at).to eq(date) } - expect(dossier_recu.reload.en_instruction?).to be true - expect(dossier_recu.reload.en_instruction_at).to eq(date) + it { expect(dossier_recu.reload.en_instruction?).to be true } + it { expect(dossier_recu.reload.en_instruction_at).to eq(instruction_date) } - expect(dossier_brouillon.reload.brouillon?).to be true - expect(dossier_brouillon.reload.en_instruction_at).to eq(nil) - end + it { expect(dossier_brouillon.reload.brouillon?).to be true } + it { expect(dossier_brouillon.reload.en_instruction_at).to eq(nil) } end context "accepte" do let(:state) { Dossier.states.fetch(:accepte) } - it do - subject - expect(nouveau_dossier1.reload.accepte?).to be true - expect(nouveau_dossier1.reload.en_instruction_at).to eq(date) - expect(nouveau_dossier1.reload.processed_at).to eq(date) + it { expect(nouveau_dossier1.reload.accepte?).to be true } + it { expect(nouveau_dossier1.reload.en_instruction_at).to eq(date) } + it { expect(nouveau_dossier1.reload.processed_at).to eq(date) } - expect(nouveau_dossier2.reload.accepte?).to be true - expect(nouveau_dossier2.reload.en_instruction_at).to eq(date) - expect(nouveau_dossier2.reload.processed_at).to eq(date) + it { expect(nouveau_dossier2.reload.accepte?).to be true } + it { expect(nouveau_dossier2.reload.en_instruction_at).to eq(date) } + it { expect(nouveau_dossier2.reload.processed_at).to eq(date) } - expect(dossier_recu.reload.en_instruction?).to be true - expect(dossier_recu.reload.en_instruction_at).to eq(date) - expect(dossier_recu.reload.processed_at).to eq(nil) + it { expect(dossier_recu.reload.en_instruction?).to be true } + it { expect(dossier_recu.reload.en_instruction_at).to eq(instruction_date) } + it { expect(dossier_recu.reload.processed_at).to eq(nil) } - expect(dossier_brouillon.reload.brouillon?).to be true - expect(dossier_brouillon.reload.en_instruction_at).to eq(nil) - expect(dossier_brouillon.reload.processed_at).to eq(nil) - end + it { expect(dossier_brouillon.reload.brouillon?).to be true } + it { expect(dossier_brouillon.reload.en_instruction_at).to eq(nil) } + it { expect(dossier_brouillon.reload.processed_at).to eq(nil) } end end end diff --git a/spec/mailers/administration_mailer_spec.rb b/spec/mailers/administration_mailer_spec.rb index 49c39479c..9c009be48 100644 --- a/spec/mailers/administration_mailer_spec.rb +++ b/spec/mailers/administration_mailer_spec.rb @@ -33,4 +33,36 @@ RSpec.describe AdministrationMailer, type: :mailer do it { expect(subject.subject).not_to be_empty } end + + describe '#dossier_expiration_summary' do + subject { described_class.dossier_expiration_summary(expiring, expired) } + + context 'with expiring dossiers only' do + let(:expiring) { [create(:dossier)] } + let(:expired) { [] } + + it { expect(subject.subject).to eq("Des dossiers approchent de la fin de leur délai de conservation") } + end + + context 'with expired dossiers only' do + let(:expiring) { [] } + let(:expired) { [create(:dossier)] } + + it { expect(subject.subject).to eq("Des dossiers ont dépassé leur délai de conservation") } + end + + context 'with both expiring and expired dossiers' do + let(:expiring) { [create(:dossier)] } + let(:expired) { [create(:dossier)] } + + it { expect(subject.subject).to eq("Des dossiers ont dépassé leur délai de conservation, et d’autres en approchent") } + end + + context 'with neither expiring nor expired dossiers' do + let(:expiring) { [] } + let(:expired) { [] } + + it { expect(subject.subject).to eq("Aucun dossier en fin de délai de conservation") } + end + end end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 17a75bf2d..f2a1be974 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -27,6 +27,32 @@ describe Dossier do end end + describe 'nearing_end_of_retention' do + let(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) } + let!(:young_dossier) { create(:dossier, procedure: procedure) } + let!(:expiring_dossier) { create(:dossier, :en_instruction, en_instruction_at: 170.days.ago, procedure: procedure) } + let!(:just_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: (6.months + 1.second).ago, procedure: procedure) } + let!(:long_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: 1.year.ago, procedure: procedure) } + + context 'with default delay to end of retention' do + subject { Dossier.nearing_end_of_retention } + + it { is_expected.not_to include(young_dossier) } + it { is_expected.to include(expiring_dossier) } + it { is_expected.to include(just_expired_dossier) } + it { is_expected.to include(long_expired_dossier) } + end + + context 'with custom delay to end of retention' do + subject { Dossier.nearing_end_of_retention('0') } + + it { is_expected.not_to include(young_dossier) } + it { is_expected.not_to include(expiring_dossier) } + it { is_expected.to include(just_expired_dossier) } + it { is_expected.to include(long_expired_dossier) } + end + end + describe 'methods' do let(:dossier) { create(:dossier, :with_entreprise, user: user) } let(:etablissement) { dossier.etablissement } @@ -976,4 +1002,30 @@ describe Dossier do it { is_expected.to be false } end end + + context "retention date" do + let(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) } + let(:uninstructed_dossier) { create(:dossier, :en_construction, procedure: procedure) } + let(:young_dossier) { create(:dossier, :en_instruction, en_instruction_at: DateTime.now, procedure: procedure) } + let(:just_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: 6.months.ago, procedure: procedure) } + let(:long_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: 1.year.ago, procedure: procedure) } + let(:modif_date) { DateTime.parse('01/01/2100') } + + before { Timecop.freeze(modif_date) } + after { Timecop.return } + + describe "#retention_end_date" do + it { expect(uninstructed_dossier.retention_end_date).to be_nil } + it { expect(young_dossier.retention_end_date).to eq(6.months.from_now) } + it { expect(just_expired_dossier.retention_end_date).to eq(DateTime.now) } + it { expect(long_expired_dossier.retention_end_date).to eq(6.months.ago) } + end + + describe "#retention_expired?" do + it { expect(uninstructed_dossier).not_to be_retention_expired } + it { expect(young_dossier).not_to be_retention_expired } + it { expect(just_expired_dossier).to be_retention_expired } + it { expect(long_expired_dossier).to be_retention_expired } + end + end end