#!/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;
