Merge pull request #11099 from colinux/many-siret-champ-columns

ETQ instructeur je peux ajouter une colonne ou filtrer par des sous-valeurs d'un champ SIRET
This commit is contained in:
Colin Darie 2024-12-06 14:43:27 +00:00 committed by GitHub
commit 6ed6d1e13e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 246 additions and 54 deletions

1
.gitignore vendored
View file

@ -29,6 +29,7 @@ storage/
yarn-debug.log*
.yarn-integrity
/.vscode
/.zed
/.idea
/public/assets
/spec/support/spec_config.local.rb

View file

@ -1497,8 +1497,7 @@ Style/WhileUntilModifier:
Enabled: false
Style/WordArray:
Enabled: true
EnforcedStyle: brackets
Enabled: false
Style/YodaCondition:
Enabled: true

View file

@ -374,7 +374,11 @@ ul.dropdown-items {
padding: 2 * $default-spacer;
&.large {
width: 340px;
width: 90vw;
@media (min-width: 62em) {
width: 40vw;
}
}
ul {

View file

@ -5,7 +5,7 @@
= current_filter_tags
.fr-select-group
= label_tag :column, t('.column'), class: 'fr-label fr-m-0', id: 'instructeur-filter-combo-label', for: 'search-filter'
= label_tag :column, t('.column'), class: 'fr-label fr-mb-1w', id: 'instructeur-filter-combo-label', for: 'search-filter'
%react-fragment
= render ReactComponent.new "ComboBox/SingleComboBox", **filter_react_props

View file

@ -5,5 +5,6 @@ class APIEntreprise::EntrepriseJob < APIEntreprise::Job
find_etablissement(etablissement_id)
etablissement_params = APIEntreprise::EntrepriseAdapter.new(etablissement.siret, procedure_id).to_params
etablissement.update!(etablissement_params)
etablissement.update_champ_value_json!
end
end

View file

@ -50,6 +50,7 @@ class Column
def dossier_column? = false
def champ_column? = false
def filterable? = filterable
def label_for_value(value)
if options_for_select.present?

View file

@ -3,7 +3,7 @@
class Columns::JSONPathColumn < Columns::ChampColumn
attr_reader :jsonpath
def initialize(procedure_id:, label:, stable_id:, tdc_type:, jsonpath:, options_for_select: [], displayable:, type: :text)
def initialize(procedure_id:, label:, stable_id:, tdc_type:, jsonpath:, options_for_select: [], displayable:, filterable: true, type: :text)
@jsonpath = quote_string(jsonpath)
super(
@ -12,6 +12,7 @@ class Columns::JSONPathColumn < Columns::ChampColumn
stable_id:,
tdc_type:,
displayable:,
filterable:,
type:,
options_for_select:
)

View file

@ -6,10 +6,10 @@ module AddressableColumnConcern
included do
def addressable_columns(procedure:, displayable: true, prefix: nil)
[
["code postal (5 chiffres)", '$.postal_code', :text, []],
["commune", '$.city_name', :text, []],
["département", '$.departement_code', :enum, APIGeoService.departement_options],
["region", '$.region_name', :enum, APIGeoService.region_options]
["Code postal (5 chiffres)", '$.postal_code', :text, []],
["Commune", '$.city_name', :text, []],
["Département", '$.departement_code', :enum, APIGeoService.departement_options],
["gion", '$.region_name', :enum, APIGeoService.region_options]
].map do |(label, jsonpath, type, options_for_select)|
Columns::JSONPathColumn.new(
procedure_id: procedure.id,

View file

@ -154,17 +154,41 @@ module ColumnsConcern
end
def moral_columns
etablissements = ['entreprise_forme_juridique', 'entreprise_siren', 'entreprise_nom_commercial', 'entreprise_raison_sociale', 'entreprise_siret_siege_social']
.map { |column| dossier_col(table: 'etablissement', column:) }
siret_column = dossier_col(table: 'etablissement', column: :siret)
etablissement_dates = ['entreprise_date_creation'].map { |column| dossier_col(table: 'etablissement', column:, type: :date) }
etablissements = Etablissement::DISPLAYABLE_COLUMNS.map do |(column, attributes)|
dossier_col(table: 'etablissement', column:, type: attributes[:type], filterable: attributes.fetch(:filterable, true))
end
for_export = ["siege_social", "naf", "adresse", "numero_voie", "type_voie", "nom_voie", "complement_adresse", "localite", "code_insee_localite", "entreprise_siren", "entreprise_capital_social", "entreprise_numero_tva_intracommunautaire", "entreprise_forme_juridique_code", "entreprise_code_effectif_entreprise", "entreprise_etat_administratif", "entreprise_nom", "entreprise_prenom", "association_rna", "association_titre", "association_objet", "association_date_creation", "association_date_declaration", "association_date_publication"]
.map { |column| dossier_col(table: 'etablissement', column:, displayable: false, filterable: false) }
others = %w[code_postal].map { |column| dossier_col(table: 'etablissement', column:) }
other = ['siret', 'libelle_naf', 'code_postal'].map { |column| dossier_col(table: 'etablissement', column:) }
for_export = %w[
siege_social
code_naf
adresse
numero_voie
type_voie
nom_voie
complement_adresse
localite
code_insee_localite
entreprise_capital_social
entreprise_numero_tva_intracommunautaire
entreprise_forme_juridique_code
entreprise_code_effectif_entreprise
entreprise_etat_administratif
entreprise_siret_siege_social
entreprise_nom
entreprise_prenom
association_rna
association_titre
association_objet
association_date_creation
association_date_declaration
association_date_publication
].map { |column| dossier_col(table: 'etablissement', column:, displayable: false, filterable: false) }
[etablissements, etablissement_dates, other, for_export].flatten
[siret_column, etablissements, others, for_export].flatten
end
def types_de_champ_columns

View file

@ -21,6 +21,19 @@ class Etablissement < ApplicationRecord
after_commit -> { dossier&.index_search_terms_later }
alias_attribute :code_naf, :naf
# See https://github.com/demarches-simplifiees/demarches-simplifiees.fr/pull/10591#discussion_r1819399688
# SIRET is already exposed as base column.
DISPLAYABLE_COLUMNS = {
"entreprise_raison_sociale" => { type: :text },
"entreprise_siren" => { type: :text },
"entreprise_nom_commercial" => { type: :text },
"entreprise_forme_juridique" => { type: :text },
"entreprise_date_creation" => { type: :date, filterable: false },
"libelle_naf" => { type: :text }
}.freeze
def entreprise_raison_sociale
read_attribute(:entreprise_raison_sociale).presence || raison_sociale_for_ei
end
@ -193,6 +206,21 @@ class Etablissement < ApplicationRecord
adresse.nil? # TOOD: maybe dedicated column or more robust way
end
def update_champ_value_json!
return if champ.nil?
champ.update!(value_json: champ_value_json)
end
def champ_value_json
address_data = APIGeoService.parse_etablissement_address(self)
DISPLAYABLE_COLUMNS.keys.each_with_object(address_data) do |attr, hash|
value = public_send(attr)
hash[attr.to_sym] = value if value.present?
end
end
private
def bilans_new_keys

View file

@ -10,6 +10,27 @@ class TypesDeChamp::SiretTypeDeChamp < TypesDeChamp::TypeDeChampBase
def champ_blank_or_invalid?(champ) = Siret.new(siret: champ.value).invalid?
def columns(procedure:, displayable: true, prefix: nil)
super.concat(addressable_columns(procedure:, displayable:, prefix:))
super
.concat(etablissement_columns(procedure:, displayable:, prefix:))
.concat(addressable_columns(procedure:, displayable:, prefix:))
end
private
def etablissement_columns(procedure:, displayable:, prefix:)
i18n_scope = [:activerecord, :attributes, :procedure_presentation, :fields, :etablissement]
Etablissement::DISPLAYABLE_COLUMNS.map do |(column, attributes)|
Columns::JSONPathColumn.new(
procedure_id: procedure.id,
stable_id:,
tdc_type: type_champ,
label: [prefix, libelle, I18n.t(column, scope: i18n_scope)].compact.join(' '),
type: attributes[:type],
jsonpath: "$.#{column}",
displayable: true,
filterable: attributes.fetch(:filterable, true)
)
end
end
end

View file

@ -116,7 +116,12 @@ class TypesDeChamp::TypeDeChampBase
private
def libelle_with_prefix(prefix)
[prefix, libelle].compact.join(' ')
# SIRET needs to be explicit in listings for better UI readability
if type_champ == "siret" && !libelle.upcase.include?("SIRET")
[prefix, libelle, "SIRET"].compact.join(' ')
else
[prefix, libelle].compact.join(' ')
end
end
def paths

View file

@ -22,10 +22,7 @@ class APIEntrepriseService
etablissement = dossier_or_champ.build_etablissement(etablissement_params)
etablissement.save!
if dossier_or_champ.is_a?(Champ)
dossier_or_champ.update!(value_json: APIGeoService.parse_etablissement_address(etablissement))
end
etablissement.update_champ_value_json!
perform_later_fetch_jobs(etablissement, procedure_id, user_id)
@ -49,10 +46,7 @@ class APIEntrepriseService
return nil if etablissement_params.empty?
etablissement.update!(etablissement_params)
if etablissement.champ.present?
etablissement.champ.update!(value_json: APIGeoService.parse_etablissement_address(etablissement))
end
etablissement.update_champ_value_json!
etablissement
end

View file

@ -4,15 +4,20 @@
# la normalisation des adresses des champs RNA/RNF/SIRET
# le fait de stocker ces données normalisées dans le champs.value_json (un jsonb)
# le backfill les anciens champs RNA/RNF/SIRET
# A (re)jouer après déploiement de PR #11013 https://github.com/demarches-simplifiees/demarches-simplifiees.fr/pull/11013
module Maintenance
class PopulateSiretValueJSONTask < MaintenanceTasks::Task
include RunnableOnDeployConcern
run_on_first_deploy
def collection
Champs::SiretChamp.where.not(value: nil)
end
def process(champ)
return if champ.etablissement.blank?
champ.update!(value_json: APIGeoService.parse_etablissement_address(champ.etablissement))
champ.update!(value_json: champ.etablissement.champ_value_json)
rescue ActiveRecord::RecordInvalid
# noop, just a champ without dossier
end

View file

@ -43,7 +43,7 @@
- c.with_value do
%p= etablissement.libelle_naf
= render Dossiers::RowShowComponent.new(label: "Libellé NAF") do |c|
= render Dossiers::RowShowComponent.new(label: "Code NAF") do |c|
- c.with_value do
%p= etablissement.naf

View file

@ -47,13 +47,10 @@ en:
question_answer: Opinion yes/no
etablissement:
entreprise_etat_administratif: 'Entreprise état administratif'
entreprise_forme_juridique: Forme juridique
entreprise_nom_commercial: Commercial name
entreprise_raison_sociale: Raison sociale
entreprise_siret_siege_social: SIRET siège social
entreprise_date_creation: Entreprise date de création
siret: Établissement SIRET
libelle_naf: Libellé NAF
code_naf: Code NAF
siege_social: "Établissement siège social"
naf: "Établissement NAF"
adresse: "Établissement Adresse"

View file

@ -51,13 +51,10 @@ fr:
question_answer: Avis oui/non
etablissement:
entreprise_etat_administratif: 'Entreprise état administratif'
entreprise_forme_juridique: Forme juridique
entreprise_nom_commercial: Nom commercial
entreprise_raison_sociale: Raison sociale
entreprise_siret_siege_social: SIRET siège social
entreprise_date_creation: Entreprise date de création
siret: Établissement SIRET
libelle_naf: Libellé NAF
code_naf: Code NAF
siege_social: "Établissement siège social"
naf: "Établissement NAF"
adresse: "Établissement Adresse"

View file

@ -165,7 +165,7 @@ FactoryBot.define do
factory :champ_do_not_use_siret, class: 'Champs::SiretChamp' do
association :etablissement, factory: [:etablissement]
value { '44011762001530' }
value_json { AddressProxy::ADDRESS_PARTS.index_by(&:itself) }
value_json { etablissement.champ_value_json }
end
factory :champ_do_not_use_rna, class: 'Champs::RNAChamp' do

View file

@ -19,7 +19,21 @@ describe Columns::ChampColumn do
expect_type_de_champ_values('pays', eq(['France']))
expect_type_de_champ_values('epci', eq([nil]))
expect_type_de_champ_values('iban', eq([nil]))
expect_type_de_champ_values('siret', eq(["44011762001530", "postal_code", "city_name", "departement_code", "region_name"]))
expect_type_de_champ_values('siret', match_array(
[
"44011762001530",
"SA à conseil d'administration (s.a.i.)",
"440117620",
"GRTGAZ",
"GRTGAZ",
"1990-04-24",
"Transports par conduites",
"92270",
"Bois-Colombes",
"92",
"Île-de-France"
]
))
expect_type_de_champ_values('text', eq(['text']))
expect_type_de_champ_values('textarea', eq(['textarea']))
expect_type_de_champ_values('number', eq(['42']))

View file

@ -36,9 +36,9 @@ describe Columns::DossierColumn do
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: "Code NAF").value(dossier)).to eq('4950Z')
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')

View file

@ -99,9 +99,9 @@ describe ColumnsConcern do
end
context 'with rna' do
let(:types_de_champ_public) { [{ type: :rna, libelle: 'rna' }] }
let(:types_de_champ_public) { [{ type: :rna, libelle: 'RNA' }] }
let(:types_de_champ_private) { [] }
it { expect(subject.map(&:label)).to include('rna commune') }
it { expect(subject.map(&:label)).to include('RNA Commune') }
end
context 'with linked drop down list' do
@ -187,8 +187,8 @@ describe ColumnsConcern do
procedure.find_column(label: "France connecté ?"),
procedure.find_column(label: "Établissement SIRET"),
procedure.find_column(label: "Établissement siège social"),
procedure.find_column(label: "Établissement NAF"),
procedure.find_column(label: "Libellé NAF"),
procedure.find_column(label: "Code NAF"),
procedure.find_column(label: "Établissement Adresse"),
procedure.find_column(label: "Établissement numero voie"),
procedure.find_column(label: "Établissement type voie"),

View file

@ -130,6 +130,43 @@ describe Etablissement do
end
end
describe '#update_champ_value_json!' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret }]) }
let(:dossier) { create(:dossier, procedure:) }
let(:etablissement) { create(:etablissement) }
let(:champ) { dossier.champs[0] }
let(:address_data) do
{
"street_number" => "6",
"street_name" => "RAOUL NORDLING",
"postal_code" => "92270",
"city_name" => "BOIS COLOMBES"
}
end
before do
allow(APIGeoService).to receive(:parse_etablissement_address)
.with(etablissement)
.and_return(address_data.dup)
etablissement.champ = champ
end
subject(:value_json) {
etablissement.update_champ_value_json!
champ.reload.value_json
}
it 'updates the associated champ value_json with geocoded address' do
expect(value_json).to include(address_data.stringify_keys)
end
it 'includes jsonpathables columns entreprise attributes' do
expect(value_json["entreprise_siren"]).to eq(etablissement.siren)
expect(value_json["entreprise_raison_sociale"]).to eq(etablissement.entreprise_raison_sociale)
end
end
private
def csv_to_array_of_hash(lines)

View file

@ -123,11 +123,20 @@ describe ExportTemplate do
context 'when procedure has a TypeDeChamp::Siret' do
let(:types_de_champ_public) do
[
{ type: :siret, libelle: 'siret', stable_id: 20 }
{ type: :siret, libelle: 'SIRET', stable_id: 20 }
]
end
it 'is able to resolve stable_id' do
expect(export_template.columns_for_stable_id(20).size).to eq(5)
columns = export_template.columns_for_stable_id(20)
expect(columns.find { _1.libelle == "SIRET" }).to be_present
%w[
$.entreprise_nom_commercial
$.entreprise_raison_sociale
].each do |jsonpath|
expect(columns.find { _1.column.respond_to?(:jsonpath) && _1.column.jsonpath == jsonpath }).to be_present
end
end
end
context 'when procedure has a TypeDeChamp::Text' do

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
describe TypesDeChamp::SiretTypeDeChamp do
let(:tdc_siret) { build(:type_de_champ_siret, libelle: 'Numéro SIRET') }
let(:procedure) { build(:procedure) }
describe "#columns" do
subject(:columns) { tdc_siret.columns(procedure: procedure) }
it "returns base column without duplicating SIRET when already in libelle" do
expect(columns[0].label).to eq("Numéro SIRET")
end
it "returns base column with SIRET when libelle doesn't contain SIRET" do
tdc_siret.update(libelle: "Identification de l'entreprise")
expect(columns[0].label).to eq("Identification de l'entreprise SIRET")
end
it "includes required jsonpaths" do
expected_paths = [
"$.entreprise_raison_sociale",
"$.entreprise_siren"
]
json_columns = columns.filter { _1.is_a?(Columns::JSONPathColumn) }
expect(json_columns.map(&:jsonpath)).to include(*expected_paths)
end
it "includes address columns" do
address_columns = columns.filter { _1.is_a?(Columns::JSONPathColumn) && _1.jsonpath.match?(/adresse|postal_code/) }
expect(address_columns).not_to be_empty
end
it "does not include jsonpath SIRET column" do
expect(columns.find { |c| c.is_a?(Columns::JSONPathColumn) && c.jsonpath == "$.siret" }).to be_nil
end
end
end

View file

@ -572,7 +572,7 @@ describe DossierFilterService do
context "when searching by postal_code (text)" do
let(:value) { "60580" }
let(:filter) { ["rna code postal (5 chiffres)", value] }
let(:filter) { ["rna Code postal (5 chiffres)", value] }
before do
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "postal_code" => value })
@ -589,7 +589,7 @@ describe DossierFilterService do
context "when searching by departement_code (enum)" do
let(:value) { "99" }
let(:filter) { ["rna département", value] }
let(:filter) { ["rna Département", value] }
before do
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "departement_code" => value })
@ -606,7 +606,7 @@ describe DossierFilterService do
context "when searching by region_name" do
let(:value) { "60" }
let(:filter) { ["rna region", value] }
let(:filter) { ["rna gion", value] }
before do
kept_dossier.project_champs_public.find { _1.stable_id == 1 }.update(value_json: { "region_name" => value })

View file

@ -239,9 +239,9 @@ describe DossierProjectionService do
end
context 'for a json column' do
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret, libelle: 'siret' }]) }
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :siret, libelle: 'SIRET' }]) }
let(:dossier) { create(:dossier, procedure:) }
let(:label) { "siret département" }
let(:label) { "SIRET Département" }
before do
dossier.project_champs_public.first.update(value_json: { 'departement_code': '38' })

View file

@ -154,7 +154,7 @@ describe "procedure filters" do
rna_champ.reload
champ_select_value = "37 Indre-et-Loire"
add_filter("#{rna_champ.libelle} département", champ_select_value, type: :enum)
add_filter("#{rna_champ.libelle} Département", champ_select_value, type: :enum)
expect(page).to have_link(new_unfollow_dossier.id.to_s)
end
end

View file

@ -10,6 +10,10 @@ module Maintenance
let(:element) { dossier.champs.first }
subject(:process) { described_class.process(element) }
before do
element.update!(value_json: nil)
end
it 'updates value_json' do
expect { subject }.to change { element.reload.value_json }
.from(anything)
@ -25,7 +29,14 @@ module Maintenance
"departement_code" => "92",
"department_code" => "92",
"departement_name" => "Hauts-de-Seine",
"department_name" => "Hauts-de-Seine"
"department_name" => "Hauts-de-Seine",
"entreprise_date_creation" => "1990-04-24",
"entreprise_forme_juridique" => "SA à conseil d'administration (s.a.i.)",
"entreprise_nom_commercial" => "GRTGAZ",
"entreprise_raison_sociale" => "GRTGAZ",
"entreprise_siren" => "440117620",
"libelle_naf" => "Transports par conduites"
})
end
end

View file

@ -9,9 +9,12 @@ describe 'shared/dossiers/normalized_address', type: :view do
let(:address) { AddressProxy.new(dossier.champs.first) }
it 'render address' do
AddressProxy::ADDRESS_PARTS.each do |address_part|
expect(subject).to have_text(address_part)
end
expect(subject).to have_text("6 RUE RAOUL NORDLING")
expect(subject).to have_text("Bois-Colombes")
expect(subject).to have_text("92270")
expect(subject).to have_text("92009")
expect(subject).to have_text("Hauts-de-Seine 92")
expect(subject).to have_text("Île-de-France 11")
end
end