From c209cac62f8aad175abdb444bd9d8fa33f0cd889 Mon Sep 17 00:00:00 2001 From: mfo Date: Mon, 4 Nov 2024 17:00:25 +0100 Subject: [PATCH] feat(export_template): use in export service --- app/models/concerns/dossier_export_concern.rb | 7 +- app/models/exported_column.rb | 4 + app/services/procedure_export_service.rb | 4 +- spec/models/columns/dossier_column_spec.rb | 63 ++++++ .../procedure_export_service_tabular_spec.rb | 206 ++++++++++++++++++ .../procedure_archive_and_export_spec.rb | 5 +- 6 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 spec/services/procedure_export_service_tabular_spec.rb diff --git a/app/models/concerns/dossier_export_concern.rb b/app/models/concerns/dossier_export_concern.rb index bb5449816..fe2f5bdc6 100644 --- a/app/models/concerns/dossier_export_concern.rb +++ b/app/models/concerns/dossier_export_concern.rb @@ -19,8 +19,9 @@ module DossierExportConcern types_de_champ.flat_map do |type_de_champ| champ = filled_champ(type_de_champ, row_id) if export_template.present? - columns = export_template.columns_for_stable_id(type_de_champ.stable_id) - columns.map { [_1.libelle, type_de_champ.champ_value_for_export(champ, _1.column)] } + export_template + .columns_for_stable_id(type_de_champ.stable_id) + .map { |exported_column| exported_column.libelle_with_value(champ) } else type_de_champ.libelles_for_export.map do |(libelle, path)| [libelle, type_de_champ.champ_value_for_export(champ, path)] @@ -37,7 +38,7 @@ module DossierExportConcern def dossier_values_for_export(with_etablissement: false, export_template: nil) if export_template.present? - return export_template.dossier_exported_columns.map { [_1.libelle, _1.column.get_value(self)] } + return export_template.dossier_exported_columns.map { _1.libelle_with_value(self) } end columns = [ diff --git a/app/models/exported_column.rb b/app/models/exported_column.rb index 5f754c98f..23c091c98 100644 --- a/app/models/exported_column.rb +++ b/app/models/exported_column.rb @@ -9,4 +9,8 @@ class ExportedColumn end def id = { id: column.id, libelle: }.to_json + + def libelle_with_value(champ_or_dossier) + [libelle, column.value(champ_or_dossier)] + end end diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb index 9482cc045..03b3281de 100644 --- a/app/services/procedure_export_service.rb +++ b/app/services/procedure_export_service.rb @@ -115,7 +115,7 @@ class ProcedureExportService { sheet_name: type_de_champ_repetition.libelle_for_export, instances: rows, - spreadsheet_columns: Proc.new { |instance| instance.spreadsheet_columns(types_de_champ) } + spreadsheet_columns: Proc.new { |instance| instance.spreadsheet_columns(types_de_champ, export_template: @export_template) } } end end @@ -152,7 +152,7 @@ class ProcedureExportService types_de_champ = procedure.types_de_champ_for_procedure_export.to_a Proc.new do |instance| - instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ) + instance.send(:"spreadsheet_columns_#{format}", types_de_champ: types_de_champ, export_template: @export_template) end end end diff --git a/spec/models/columns/dossier_column_spec.rb b/spec/models/columns/dossier_column_spec.rb index 5b8651d2a..6574732c0 100644 --- a/spec/models/columns/dossier_column_spec.rb +++ b/spec/models/columns/dossier_column_spec.rb @@ -14,6 +14,9 @@ describe Columns::DossierColumn do expect(procedure.find_column(label: "Prénom").value(dossier)).to eq("Paul") expect(procedure.find_column(label: "Nom").value(dossier)).to eq("Sim") expect(procedure.find_column(label: "Civilité").value(dossier)).to eq("M.") + expect(procedure.find_column(label: "Dépôt pour un tiers").value(dossier)).to eq(true) + expect(procedure.find_column(label: "Nom du mandataire").value(dossier)).to eq("Christophe") + expect(procedure.find_column(label: "Prénom du mandataire").value(dossier)).to eq("Martin") end end @@ -22,7 +25,67 @@ describe Columns::DossierColumn do let(:dossier) { create(:dossier, :en_instruction, :with_entreprise, procedure:) } it 'retrieve entreprise information' do + expect(procedure.find_column(label: "Nº dossier").value(dossier)).to eq(dossier.id) + expect(procedure.find_column(label: "Email").value(dossier)).to eq(dossier.user_email_for(:display)) + expect(procedure.find_column(label: "France connecté ?").value(dossier)).to eq(false) + expect(procedure.find_column(label: "Entreprise forme juridique").value(dossier)).to eq("SA à conseil d'administration (s.a.i.)") + expect(procedure.find_column(label: "Entreprise SIREN").value(dossier)).to eq('440117620') + expect(procedure.find_column(label: "Entreprise nom commercial").value(dossier)).to eq('GRTGAZ') + expect(procedure.find_column(label: "Entreprise raison sociale").value(dossier)).to eq('GRTGAZ') + expect(procedure.find_column(label: "Entreprise SIRET siège social").value(dossier)).to eq('44011762001530') + expect(procedure.find_column(label: "Date de création").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone) + expect(procedure.find_column(label: "Établissement SIRET").value(dossier)).to eq('44011762001530') expect(procedure.find_column(label: "Libellé NAF").value(dossier)).to eq('Transports par conduites') + expect(procedure.find_column(label: "Établissement code postal").value(dossier)).to eq('92270') + expect(procedure.find_column(label: "Établissement siège social").value(dossier)).to eq(true) + expect(procedure.find_column(label: "Établissement NAF").value(dossier)).to eq('4950Z') + expect(procedure.find_column(label: "Établissement Adresse").value(dossier)).to eq("GRTGAZ\r IMMEUBLE BORA\r 6 RUE RAOUL NORDLING\r 92270 BOIS COLOMBES\r") + expect(procedure.find_column(label: "Établissement numero voie").value(dossier)).to eq('6') + expect(procedure.find_column(label: "Établissement type voie").value(dossier)).to eq('RUE') + expect(procedure.find_column(label: "Établissement nom voie").value(dossier)).to eq('RAOUL NORDLING') + expect(procedure.find_column(label: "Établissement complément adresse").value(dossier)).to eq('IMMEUBLE BORA') + expect(procedure.find_column(label: "Établissement localité").value(dossier)).to eq('BOIS COLOMBES') + expect(procedure.find_column(label: "Établissement code INSEE localité").value(dossier)).to eq('92009') + expect(procedure.find_column(label: "Entreprise SIREN").value(dossier)).to eq('440117620') + expect(procedure.find_column(label: "Entreprise capital social").value(dossier)).to eq(537_100_000) + expect(procedure.find_column(label: "Entreprise numero TVA intracommunautaire").value(dossier)).to eq('FR27440117620') + expect(procedure.find_column(label: "Entreprise forme juridique code").value(dossier)).to eq('5599') + expect(procedure.find_column(label: "Entreprise code effectif entreprise").value(dossier)).to eq('51') + expect(procedure.find_column(label: "Entreprise état administratif").value(dossier)).to eq("actif") + expect(procedure.find_column(label: "Entreprise nom").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Entreprise prénom").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Association RNA").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Association titre").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Association objet").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Association date de création").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Association date de déclaration").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Association date de publication").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Date de création").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone) + expect(procedure.find_column(label: "Date du dernier évènement").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone) + expect(procedure.find_column(label: "Date de dépot").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone) + expect(procedure.find_column(label: "Date de passage en construction").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone) + expect(procedure.find_column(label: "Date de passage en instruction").value(dossier)).to be_an_instance_of(ActiveSupport::TimeWithZone) + expect(procedure.find_column(label: "Date de traitement").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "État du dossier").value(dossier)).to eq('en_instruction') + expect(procedure.find_column(label: "Archivé").value(dossier)).to eq(false) + expect(procedure.find_column(label: "Motivation de la décision").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Date de dernière modification (usager)").value(dossier)).to eq(nil) + expect(procedure.find_column(label: "Instructeurs").value(dossier)).to eq('') + end + end + + context 'when procedure for entreprise which is also an association' do + let(:procedure) { create(:procedure, for_individual: false, groupe_instructeurs: [groupe_instructeur]) } + let(:etablissement) { create(:etablissement, :is_association) } + let(:dossier) { create(:dossier, :en_instruction, procedure:, etablissement:) } + + it 'retrieve also association information' do + expect(procedure.find_column(label: "Association RNA").value(dossier)).to eq("W072000535") + expect(procedure.find_column(label: "Association titre").value(dossier)).to eq("ASSOCIATION POUR LA PROMOTION DE SPECTACLES AU CHATEAU DE ROCHEMAURE") + expect(procedure.find_column(label: "Association objet").value(dossier)).to eq("mise en oeuvre et réalisation de spectacles au chateau de rochemaure") + expect(procedure.find_column(label: "Association date de création").value(dossier)).to eq(Date.parse("1990-04-24")) + expect(procedure.find_column(label: "Association date de déclaration").value(dossier)).to eq(Date.parse("2014-11-28")) + expect(procedure.find_column(label: "Association date de publication").value(dossier)).to eq(Date.parse("1990-05-16")) end end diff --git a/spec/services/procedure_export_service_tabular_spec.rb b/spec/services/procedure_export_service_tabular_spec.rb new file mode 100644 index 000000000..6d994d22b --- /dev/null +++ b/spec/services/procedure_export_service_tabular_spec.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +require 'csv' + +describe ProcedureExportService do + let(:instructeur) { create(:instructeur) } + let(:procedure) { create(:procedure, types_de_champ_public:, for_individual:, ask_birthday: true, instructeurs: [instructeur]) } + let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } + let(:export_template) { create(:export_template, kind:, exported_columns:, groupe_instructeur: procedure.defaut_groupe_instructeur) } + let(:for_individual) { true } + let(:types_de_champ_public) do + [ + { type: :text, libelle: "first champ", mandatory: true, stable_id: 1 }, + { type: :communes, libelle: "Commune", mandatory: true, stable_id: 17 }, + { type: :piece_justificative, libelle: "PJ", stable_id: 30 }, + { + type: :repetition, mandatory: true, stable_id: 7, libelle: "Champ répétable", children: + [ + { type: 'text', libelle: 'child first champ', stable_id: 8 }, + { type: 'text', libelle: 'child second champ', stable_id: 9 } + ] + } + ] + end + let(:exported_columns) { [] } + + describe 'to_xlsx' do + subject do + service + .to_xlsx + .open { |f| SimpleXlsxReader.open(f.path) } + end + + let(:kind) { 'xlsx' } + let(:dossiers_sheet) { subject.sheets.first } + let(:etablissements_sheet) { subject.sheets.second } + let(:avis_sheet) { subject.sheets.third } + let(:repetition_sheet) { subject.sheets.fourth } + + describe 'sheets' do + it 'should have a sheet for each record type' do + expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis']) + end + end + + describe 'Dossiers sheet' do + let(:exported_columns) do + [ + ExportedColumn.new(libelle: 'Date du dernier évènement', column: procedure.find_column(label: 'Date du dernier évènement')), + ExportedColumn.new(libelle: 'Email', column: procedure.find_column(label: 'Email')), + ExportedColumn.new(libelle: 'Groupe instructeur', column: procedure.find_column(label: 'Groupe instructeur')), + ExportedColumn.new(libelle: 'État du dossier', column: procedure.dossier_state_column), + ExportedColumn.new(libelle: 'first champ', column: procedure.find_column(label: 'first champ')), + ExportedColumn.new(libelle: 'Commune (Code INSEE)', column: procedure.find_column(label: 'Commune (Code INSEE)')), + ExportedColumn.new(libelle: 'PJ', column: procedure.find_column(label: 'PJ')) + ] + end + + let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) } + let(:selected_headers) { ["Email", "first champ", "Commune (Code INSEE)", "Groupe instructeur", "Date du dernier évènement", "État du dossier", "PJ"] } + + it 'should have only headers from export template' do + expect(dossiers_sheet.headers).to match_array(selected_headers) + end + + it 'should have data' do + expect(procedure.dossiers.count).to eq 1 + expect(dossiers_sheet.data.size).to eq 1 + + expect(dossiers_sheet.data).to match_array([[anything, dossier.user_email_for_display, "défaut", "En instruction", "text", "60172", "toto.txt"]]) + end + + context 'with a procedure routee' do + let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:) } + before { create(:groupe_instructeur, label: '2', procedure:) } + + it 'find groupe instructeur data' do + expect(dossiers_sheet.headers).to include('Groupe instructeur') + expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Groupe instructeur')]).to eq('défaut') + end + end + + context 'with a dossier having multiple pjs' do + let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) } + let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) } + let!(:dossier_2) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) } + before do + dossier_2.filled_champs_public + .find { _1.is_a? Champs::PieceJustificativeChamp } + .piece_justificative_file + .attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") + end + it { expect(dossiers_sheet.data.last.last).to eq "toto.txt, toto.txt" } + end + end + + describe 'Etablissement sheet' do + let(:types_de_champ_public) { [{ type: :siret, libelle: 'siret', stable_id: 40 }] } + let(:exported_columns) do + [ + ExportedColumn.new(libelle: "Nº dossier", column: procedure.find_column(label: "Nº dossier")), + ExportedColumn.new(libelle: "Demandeur", column: procedure.find_column(label: "Demandeur")), + ExportedColumn.new(libelle: "siret", column: procedure.find_column(label: "siret")) + ] + end + let(:procedure) { create(:procedure, :published, types_de_champ_public:) } + let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_entreprise, procedure: procedure) } + + let(:dossier_etablissement) { etablissements_sheet.data[1] } + let(:champ_etablissement) { etablissements_sheet.data[0] } + + it 'should have siret header in dossiers sheet' do + expect(dossiers_sheet.headers).to include('siret') + end + + it 'should have headers in etablissement sheet' do + expect(etablissements_sheet.headers).to eq([ + "Dossier ID", + "Champ", + "Établissement SIRET", + "Etablissement enseigne", + "Établissement siège social", + "Établissement NAF", + "Établissement libellé NAF", + "Établissement Adresse", + "Établissement numero voie", + "Établissement type voie", + "Établissement nom voie", + "Établissement complément adresse", + "Établissement code postal", + "Établissement localité", + "Établissement code INSEE localité", + "Entreprise SIREN", + "Entreprise capital social", + "Entreprise numero TVA intracommunautaire", + "Entreprise forme juridique", + "Entreprise forme juridique code", + "Entreprise nom commercial", + "Entreprise raison sociale", + "Entreprise SIRET siège social", + "Entreprise code effectif entreprise", + "Entreprise date de création", + "Entreprise état administratif", + "Entreprise nom", + "Entreprise prénom", + "Association RNA", + "Association titre", + "Association objet", + "Association date de création", + "Association date de déclaration", + "Association date de publication" + ]) + end + end + + describe 'Avis sheet' do + let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) } + let!(:avis) { create(:avis, :with_answer, dossier: dossier) } + + it 'should have headers and data' do + expect(avis_sheet.headers).to eq([ + "Dossier ID", + "Introduction", + "Réponse", + "Question", + "Réponse oui/non", + "Créé le", + "Répondu le", + "Instructeur", + "Expert" + ]) + expect(avis_sheet.data.size).to eq(1) + end + end + + describe 'Repetitions sheet' do + let(:exported_columns) do + [ + ExportedColumn.new(libelle: "Champ répétable – child second champ", column: procedure.find_column(label: "Champ répétable – child second champ")) + ] + end + let!(:dossiers) do + [ + create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure), + create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) + ] + end + + describe 'sheets' do + it 'should have a sheet for repetition' do + expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', '(7) Champ repetable']) + end + end + + it 'should have headers' do + expect(repetition_sheet.headers).to eq([ + "Dossier ID", "Ligne", "Champ répétable – child second champ" + ]) + end + + it 'should have data' do + expect(repetition_sheet.data.size).to eq 4 + end + end + end +end diff --git a/spec/system/administrateurs/procedure_archive_and_export_spec.rb b/spec/system/administrateurs/procedure_archive_and_export_spec.rb index 3b1b85c9c..eb868f788 100644 --- a/spec/system/administrateurs/procedure_archive_and_export_spec.rb +++ b/spec/system/administrateurs/procedure_archive_and_export_spec.rb @@ -41,7 +41,10 @@ describe 'Creating a new procedure', js: true do click_on "Télécharger tous les dossiers" expect { - click_on "Demander un export au format .xlsx" + within(:css, '#tabpanel-standard-panel') do + choose "Fichier xlsx", allow_label_click: true + click_on "Demander l'export" + end expect(page).to have_content("Nous générons cet export. Veuillez revenir dans quelques minutes pour le télécharger.") }.to have_enqueued_job(ExportJob).with(an_instance_of(Export)) end