Ruby gem for importing and exporting Penelop XML files, a French financial data exchange standard used for life insurance, capitalization contracts, PEA (stock savings plans), and securities accounts.
Add this line to your Gemfile:
gem 'fluence-penelop'Then run:
bundle installOr install directly:
gem install fluence-peneloprequire 'fluence/penelop'
# Import from a file
document = Fluence::Penelop.import('/path/to/file.xml')
# Import from an XML string
document = Fluence::Penelop.import(xml_string)
# Import from an IO object
document = Fluence::Penelop.import(io_object)# Export to an XML string
xml_string = Fluence::Penelop.export(document)
# Export to a file
Fluence::Penelop.export_to_file(document, '/path/to/output.xml')
# Export options
xml = Fluence::Penelop.export(document, pretty: true, indent: 2)# Access main data
document.version_penelop # => 1
document.expediteur # => "ALLIANZ"
document.date_creation # => Time
# Persons
document.personnes # => Array<Personne>
personne = document.find_personne('ID123')
personne.nom_patronymique # => "DUPONT"
personne.prenoms # => "Jean"
personne.date_naissance # => Date
personne.age # => 45
personne.legal_entity? # => false
# Contracts
document.contrats # => Array<Contrat>
contrat = document.find_contrat('CTR001')
contrat.num_contrat # => "123456789"
contrat.valeur_acquise # => BigDecimal
contrat.active? # => true
contrat.state # => :active
# Active contracts only
document.active_contrats # => Array<Contrat>
# Products
document.produits # => Array<Produit>
produit = document.find_produit('PRD001')
produit.designation # => "Multi-support Life Insurance"
produit.multisupport? # => trueModels have direct access to their relations via belongs_to:
contrat = document.contrats.first
# Direct relation access
contrat.produit # => <Produit>
contrat.produit.fournisseur # => <FournisseurConsomme>
contrat.conseiller # => <IntermediaireConsomme>
# Roles and persons
role = contrat.roles.first
role.personne # => <Personne>
role.personne.nom_patronymique # => "DUPONT"
# Supports (fund holdings)
support = contrat.support_composants.first
support.support_info # => <SupportInfoGenConsomme>
support.support_info.fournisseur # => <FournisseurConsomme>
support.support_info.categorisation # => <CodeCategorisationSupport>
# Events
evt = contrat.evt_globaux.first
evt.evt_details.first.support_info # => <SupportInfoGenConsomme>
# Basket links
contrat.basket_links.first.contrat # => <Contrat> (linked contract)The document exposes find_* methods for ID lookups:
document.find_personne('ID123')
document.find_contrat('CTR001')
document.find_produit('PRD001')
document.find_fournisseur('FRN001')
document.find_intermediaire('INT001')
document.find_support_info('SUP001')
document.find_categorisation_support('CAT001')Roles can be combined (e.g., 'A S' = Insured + Subscriber):
role = contrat.roles.first
# Role codes
role.code_role # => "A S"
role.role_codes # => ["A", "S"]
role.role_types # => [:insured, :subscriber]
role.combined? # => true
# Role checks
role.subscriber? # => true (S)
role.insured? # => true (A)
role.beneficiary? # => false (B)
role.full_owner? # => false (PP - Full ownership)
role.usufructuary? # => false (U)
role.bare_owner? # => false (N - Bare ownership)
role.has_role?('S') # => true
# Available codes
# S = Subscriber (Souscripteur)
# A = Insured (Assuré)
# B = Beneficiary (Bénéficiaire)
# CO = Co-subscriber (Co-souscripteur)
# CA = Co-insured (Co-assuré)
# U = Usufructuary (Usufruitier)
# N = Bare owner (Nu-propriétaire)
# PP = Full owner (Pleine Propriété)
# G = Manager (Gestionnaire)contrat.evt_globaux.each do |evt|
evt.code_type_evt_global # => "VRS" (payment), "RCH" (surrender), etc.
evt.date_effet_evt # => Date
evt.montant_total_brut # => BigDecimal
evt.validated? # => true
# Details by support
evt.evt_details.each do |detail|
detail.nombre_de_parts # => BigDecimal
detail.montant_en_euro # => BigDecimal
detail.support_info # => <SupportInfoGenConsomme>
end
endThe info_complementaire fields can contain key-value pairs:
# Format: "key1=value1;key2=value2;"
contrat.info_complementaire # => "type=UC;origin=transfer;"
contrat.parsed_info # => { type: "UC", origin: "transfer" }
# Automatic type conversion
# "count=42" => { count: 42 }
# "rate=3.5" => { rate: BigDecimal("3.5") }
# "active=OUI" => { active: true }
# "active=NON" => { active: false }stats = document.statistics
# => {
# version: 1,
# date_creation: Time,
# expediteur: "ALLIANZ",
# nombre_personnes: 2,
# nombre_contrats: 1,
# nombre_produits: 1,
# valeur_totale: BigDecimal,
# contrats_actifs: 1
# }Document
├── donnee_reguliere
│ └── destinataire
│ ├── personnes[]
│ └── contrats[]
│ ├── roles[]
│ ├── support_composants[]
│ ├── evt_globaux[]
│ │ └── evt_details[]
│ └── basket_links[]
└── donnee_specifique
├── produits[]
├── fournisseurs_consommes[]
├── intermediaires_consommes[]
├── supports_info_gen_consommes[]
├── codes_categorisation_support[]
└── codes_evenement_expediteur[]
XML ↔ Ruby mappings are defined in Fluence::Penelop::Schema:
# Access mappings
Schema::DOCUMENT # => { version_penelop: 'version_penelop', ... }
Schema::CONTRAT # => { id_contrat: 'ID_contrat', ... }
# Invert for parsing
Schema.invert(Schema::DOCUMENT) # => { 'version_penelop' => :version_penelop, ... }This gem supports all 1.x versions of the Penelop format (1.17, 1.21, 1.24, 1.25, etc.).
# Install dependencies
bin/setup
# Interactive console
bin/console
# Run tests
bundle exec rspec
# Local installation
bundle exec rake installMIT License - see LICENSE