--- /dev/null
+#!/usr/bin/perl
+# DESCRIPTION: import from [Oxygène](http://www.memsoft.fr) to [hledger](http://hledger.org)
+# AUTHOR: Julien Moutinho <julm+hledger@autogeree.net>
+# LICENSE: [GPLv3+](https://www.gnu.org/licenses/gpl-3.0.html)
+# NOTE: should be easily hackable to import from other .csv
+# USAGE:
+# % hledger-print-csv -f Chart_of_accounts.hledger >Chart_of_accounts.csv
+# % iconv -f latin1 -t utf8 <EXPORT.oxygen.csv |
+# perl hledger-of-oxygen-csv.pl \
+# Chart_of_accounts.csv \
+# >EXPORT.oxygen.hledger
+#
+# FORMAT of EXPORT.oxygen.csv:
+# ----
+# NUMJL;LIBJL;DTOPE;NPIEC;NUMCP;LIBCP;CODCP;LIBEC;MTDEB;MTCRE;COTVA;TXTVA
+# 60;Achats;01/01/2012;ACH01/76;401REGIEQUART;REGIE DE QUARTIER;REGIE DE QUARTI;LOYER LOCAL DEC. 2011+1T 2012 REGIE QUAR;0,00;1410,91;;0,00
+# 60;Achats;01/01/2012;ACH01/76;6132000;LOYER LOCAL 15 rue P. BONNARD;LOYER BONNARD;LOYER LOCAL DEC. 2011+1T 2012 REGIE QUAR;1076,25;0,00;;0,00
+# 60;Achats;01/01/2012;ACH01/76;6165000;Responsabilité civile;;ASSURANCE LOCAL 2012 VIA REGIEQUARTIER;86,47;0,00;;0,00
+# 60;Achats;01/01/2012;ACH01/76;6140000;Charges locatives et de copropriété;;CHARGES LOCAL 1T 2012;248,19;0,00;;0,00
+# ; ... And so on.. and so forth..
+# ----
+#
+# FORMAT of Chart_of_accounts.hledger:
+#
+# Pattern:
+# ----
+# 01/01
+# 0.ZZZ:1.YYY:2.XXX 0; 012. Description
+# 0.ZZZ:1.YYY:3.WWW 0; 013. Description
+# 0.ZZZ:1.YYY:3.WWW.4.VVV 0; 0134. Description
+# ; ... And so on.. and so forth..
+# ----
+#
+# For exemple:
+# ----
+# 01/01 Plan comptable des associations
+# 1.Capital 0 ; 1. COMPTES DE CCOAITAUX
+# 1.Capital:0.Fonds 0 ; 10. Fonds associatifs et reserves
+# 2.Immobilisation 0 ; 2. COMPTES D'IMMOBILISATIONS
+# 2.Immobilisation:1.Corporelle 0 ; 21. Immobilisations corporelles
+# 4.Tiers 0 ; 4. COMPTES TIERS
+# 4.Tiers:0.Fournisseur 0 ; 40. Fournisseurs et comptes rattachés
+# 5.Finance 0 ; 5. COMPTES FINANCIERS
+# 5.Finance:1.Etablissement 0 ; 51. Banques, établissements financiers et assimilés
+# 5.Finance:1.Etablissement:1.Valeur 0 ; 511. Valeurs à l’encaissement
+# 5.Finance:1.Etablissement:1.Valeur:2.Chèque_à_encaisser 0 ; 5112. Chèques à encaisser
+# 5.Finance:1.Etablissement:2.Banque:001.Courant 0 ; 512001. Crédit Coopératif - Compte courant
+# 5.Finance:1.Etablissement:2.Banque:002.Livret 0 ; 512002. Crédit coopératif - Livret
+# 6.Charge 0 ; 6. COMPTES D'ACHATS
+# 6.Charge:1.Service 0 ; 61. Services extérieurs
+# 6.Charge:2.Autre_service 0 ; 62. Autres services extérieurs
+# 6.Charge:3.Impôt 0 ; 63. Impôts, taxes et versements assimilés
+# 6.Charge:4.Personnel 0 ; 64. Charges de personnel
+# 6.Charge:5.Gestion 0 ; 65. Autres charges de gestion courantes
+# 6.Charge:8.Dotation 0 ; 68. Dotations aux amortissements, dépréciations, provisions et engagements
+# 7.Produit 0 ; 7. COMPTES DE PRODUITS
+# 7.Produit:0.Vente 0 ; 70. ventes de produits finis, prestations de services, marchandises
+# ; ... And so on.. and so forth..
+# ----
+
+our $VERSION = '2014.07.22';
+use strict;
+use warnings FATAL => qw(all);
+use utf8;
+use open qw/:std :utf8/;
+require Data::Dumper;
+require Encode;
+require IO::Wrap;
+require Text::CSV;
+#require Text::CSV::Encoded;
+require Text::Trim;
+
+sub parse_date (@) {
+ ($_) = @_;
+ my ($jj,$mm,undef,$yy) = ($_ =~ m{^\s*([0-3]?[0-9])\s*/\s*([0-1]?[0-9])\s*/\s*(20)?([0-9][0-9])\s*$});
+ return "20$yy/$mm/$jj";
+ }
+sub parse_code (@) {
+ ($_) = @_;
+ my ($code) = ($_ =~ m{^([0-9]*?)0*$});
+ return (defined $code ? $code : $_);
+ }
+sub parse_journal (@) {
+ ($_) = @_;
+ $_ = Text::Trim::trim($_);
+ s/\s/_/g;
+ return $_;
+ }
+
+sub parse_csv_line (@) {
+ my ($nth, $h, $c) = @_;
+ #print STDERR ("parse_csv_line: csv_line($nth)=".Data::Dumper::Dumper($c));
+ my $date = parse_date($c->{date});
+ $h->{$date} = {}
+ unless defined $h->{$date};
+ my $t;
+ if (exists $h->{$date}->{$c->{transaction}}) {
+ $t = $h->{$date}->{$c->{transaction}};
+ }
+ else {
+ $t =
+ { journal => parse_journal($c->{journal})
+ , journal_code => $c->{journal_code}
+ , postings => []
+ };
+ $h->{$date}->{$c->{transaction}} = $t;
+ }
+
+ my $amount;
+ if (defined $c->{debit} and $c->{debit} eq '0,00') {
+ $amount = "-$c->{credit}";
+ }
+ elsif (defined $c->{credit} and $c->{credit} eq '0,00') {
+ $amount = "$c->{debit}";
+ }
+ else { die "ERROR: wrong credit/debit: CSV#$nth: ".Data::Dumper::Dumper($c); }
+
+ push $t->{postings},
+ { account => parse_code($c->{account})
+ , amount => $amount
+ , comment => $c->{account_name}
+ , csv_nth => $nth+2
+ }
+ }
+sub print_hledger (@) {
+ my ($h, $ap) = @_;
+ foreach my $date (sort {$a cmp $b} (keys %$h)) {
+ my $transactions = $h->{$date};
+ while (my ($transaction, $t) = each %$transactions) {
+ print STDOUT "$date $transaction ; Journal:$t->{journal}\n";
+ my $wmax = 0;
+ foreach my $a (@{$t->{postings}}) {
+ if (not defined $a->{account}) {
+ print STDERR Data::Dumper::Dumper($t);
+ die "ERROR: wrong account in t=$transaction";
+ }
+ if (defined $ap->{$a->{account}}) {
+ $a->{account} = $ap->{$a->{account}}->{account}
+ }
+ my $w = 0 + length ($a->{account});
+ $wmax = $w
+ if $wmax < $w;
+ }
+ my $p = $t->{postings};
+ foreach my $a (sort {$a->{account} cmp $b->{account}} @$p) {
+ print STDOUT "\t$a->{account} $a->{amount} ; $a->{comment} CSV#.$a->{csv_nth}\n";
+ }
+ }
+ }
+ }
+sub parse_chart_of_accounts (@) {
+ my ($coa_file) = @_;
+ my %ap = ();
+ my $csv = Text::CSV->new
+ ({binary => 1
+ , eol => $/
+ , sep_char => ','
+ });
+ print STDERR ("Chart_of_accounts: ", $coa_file, "\n");
+ open (my $COA, "<:encoding(utf8)", $coa_file)
+ or die "ERROR: opening accounting plan given as first argument";
+ #my $coa_in = IO::Wrap::wraphandle($COA);
+ my $coa_head = $csv->getline($COA);
+ print STDERR ("coa_head: ", join("|", @$coa_head), "\n");
+ $csv->column_names(@$coa_head);
+ my $nth = 1;
+ while (my $csv_line = $csv->getline_hr($COA)) {
+ $nth++;
+ my $post_cmt = $csv_line->{'posting-comment'};
+ die "ERROR: no posting-comment COA#$nth: ".Data::Dumper::Dumper($csv_line)
+ if not defined $post_cmt;
+ my ($code, $description) = ($post_cmt =~ m{^([0-9]+)\.\s*(.*)$});
+ die "ERROR: cannot extract code in accounting plan: posting-comment COA#$nth: $csv_line->{'posting-comment'}"
+ if not defined $code;
+ $ap{$code} =
+ { account => $csv_line->{account}
+ , description => $description
+ };
+ }
+ print STDERR "Chart_of_accounts: ".Data::Dumper::Dumper(\%ap);
+ return \%ap;
+ }
+
+sub main () {
+ my $ap = parse_chart_of_accounts($ARGV[0]);
+ my $csv = Text::CSV->new
+ ({binary => 1
+ , eol => $/
+ , sep_char => ';'
+ });
+ my $in = IO::Wrap::wraphandle(\*STDIN);
+ binmode STDOUT, ':utf8';
+ my $csv_head = $csv->getline($in);
+ #print STDERR ("head: ", join("|", @$csv_head), "\n");
+ #$csv->column_names(@$csv_head);
+ $csv->column_names (qw (
+ journal_code
+ journal
+ date
+ transaction
+ account
+ account_name
+ account_code
+ description
+ debit
+ credit
+ COTVA
+ TXTVA
+ ));
+ my $hledger_data = {};
+
+ my $members = {};
+ my $nth = 0;
+ while (my $csv_line = $csv->getline_hr($in)) {
+ #print STDERR ("csv_line: ", join("|", @$csv_line), "\n");
+ parse_csv_line(2 + $nth++, $hledger_data, $csv_line);
+ }
+ #print STDERR "hledger_data=".Data::Dumper::Dumper($hledger_data);
+ print_hledger($hledger_data, $ap);
+ #my $out = IO::Wrap::wraphandle(\*STDERR);
+ }
+
+main;