Set billing_date when directly paying an item
[infodrom.org/service.infodrom.org] / src / InfoCon / buch / infocon
1 #! /usr/bin/perl
2
3 #  infocon - Administration tool for InfoCon
4 #  Copyright (c) 1998-2003,2005-8,10,11,12,14,15,16  Martin Schulze <joey@infodrom.org>
5 #
6 #  This program is free software; you can redistribute it and/or modify
7 #  it under the terms of the GNU General Public License as published by
8 #  the Free Software Foundation; either version 2 of the License, or
9 #  (at your option) any later version.
10 #
11 #  This program is distributed in the hope that it will be useful,
12 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #  GNU General Public License for more details.
15 #
16 #  You should have received a copy of the GNU General Public License
17 #  along with this program; if not, write to the Free Software
18 #  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA.
19
20 use strict;
21 use warnings;
22
23 use DBI;
24 use Scalar::Util qw/reftype/;
25 use Term::ReadLine;
26 use Getopt::Long;
27
28 my $table = "sales";
29 my $engine  = "dbi:Pg:dbname=infocon";
30 my $dbh = DBI->connect($engine);
31 if (!$dbh) {
32     print "Access to database denied!\n";
33     return 1;
34 }
35
36 my %data;
37 my @categories = ();
38 my $term = undef;
39 my $opt_all = 0;
40 my $opt_verbose = 0;
41 my $opt_year = 0;
42 my $opt_direction = undef;
43
44 sub sdate
45 {
46     $_[0] =~ /\d{2}(\d{2})(\d{2})(\d{2})/;
47     return sprintf ("%d.%02d.%02d", $3,$2,$1);
48 }
49
50 # Wandelt einen lesbaren Datumsstring in die Form um, in der er in der
51 # Datenbank gespeichert werden kann.
52 #
53 sub date_to_string
54 {
55     my ($day,$mon,$year);
56
57     return "" if (!$_[0]);
58
59     my ($date_sec,$date_min,$date_hour,$date_mday,$date_mon,$date_year,$date_wday,$date_isdst)
60         = localtime;
61
62     if ($_[0] eq "heute" || $_[0] eq "sofort" || $_[0] eq "pronto" || $_[0] eq "today" || $_[0] eq "now") {
63         $day = $date_mday;
64         $mon = $date_mon+1;
65         $year = $date_year;
66     } elsif ($_[0] eq "gestern" || $_[0] eq "yesterday") {
67         $day = $date_mday-1 if ($date_mday);
68         $mon = $date_mon+1;
69         $year = $date_year;
70     } elsif ($_[0] eq "morgen" || $_[0] eq "tomorrow") {
71         $day = $date_mday+1;
72         $mon = $date_mon+1;
73         $year = $date_year;
74     } else {
75         ($day,$mon,$year) = split(/\./, $_[0]);
76         if (!$year) {    
77             $year = $date_year;
78         }
79     }
80
81     if ($year < 70) {
82         $year += 2000;
83     } elsif ($year < 100) {
84         $year += 1900;
85     }
86     return sprintf("%4d%02d%02d", $year,$mon,$day);
87 }
88
89 sub pay_invoice
90 {
91     my $nr = shift;
92     my $pay = shift;
93     my $query;
94     my $sth;
95
96     if ($pay) {
97         $query  = "UPDATE sales SET paid=1,billing_date=now() WHERE nr = $nr";
98     } else {
99         $query  = "UPDATE sales SET paid=0,billing_date=NULL WHERE nr = $nr";
100     }
101     $sth = $dbh->do($query);
102 }
103
104 sub hide_invoice
105 {
106     my $nr = shift;
107     my $hide = shift;
108     my $value = $hide==1?0:1;
109     my $query;
110     my $sth;
111
112     $query  = "UPDATE sales SET visible=$value WHERE nr = $nr";
113     $sth = $dbh->do($query);
114 }
115
116 sub sales_list
117 {
118     my $where = shift;
119     my $descr;
120     my $sum_pos=0;
121     my $sum_neg=0;
122     my $query;
123     my @row;
124     my $sth;
125     my $d;
126
127     if ($where && $where !~ /visible/ && (!$opt_all || $opt_all == 0)) {
128         if ($where) {
129             $where .= " AND visible = 1";
130         } else {
131             $where .= "visible = 1";
132         }
133     }
134
135     if ($opt_year) {
136         $where .= " AND " if $where;
137         $where .= sprintf("year = %d", $opt_year);
138     }
139
140     if ($opt_direction) {
141         if ($opt_direction eq "in") {
142             $d = "price >= 0"
143         } elsif ($opt_direction eq "out") {
144             $d = "price <= 0"
145         }
146
147         if ($where) {
148             $where .= " AND $d";
149         } else {
150             $where .= "$d";
151         }
152     }
153
154     if ($where !~ /visible/) {
155         $where .= " AND " if $where;
156         $where .= "visible = 1";
157     }
158
159     $query  = "SELECT nr,date,description,price FROM $table";
160     $query .= " WHERE $where" if ($where);
161     $query .= " ORDER by date,nr";
162
163     $sth = $dbh->prepare($query);
164     if ($sth && (my $rc = $sth->execute) > 0) {
165         print " Nr.   Datum  Bezeichnung                                           Betrag\n";
166         print "----------------------------------------------------------------------------\n";
167         while (@row = $sth->fetchrow_array) {
168             $descr = substr($row[2],0,50);
169             printf "%4d %8s %-50s  %9.2f\n", $row[0], sdate($row[1]), $descr, $row[3];
170             if ($row[3] < 0.0) {
171                 $sum_neg -= $row[3];
172             } else {
173                 $sum_pos += $row[3];
174             }
175         }
176         print "---------------------------------------------------------------------------\n"
177             if ($sum_neg > 0 || $sum_pos > 0) ;
178         printf " Zahlungseingänge                                                 %9.2f\n", $sum_pos
179             if ($sum_pos > 0);
180         printf " Zahlungsausgänge                                                 %9.2f\n", -$sum_neg
181             if ($sum_neg > 0);
182         print "============================================================================\n";
183         printf " Summe                                                            %9.2f\n\n", $sum_pos - $sum_neg;
184     }
185     $data{'done'} = 1;
186 }
187
188 sub get_categories
189 {
190     my $query;
191     my @row;
192     my $sth;
193     my @arr = ();
194
195     $query  = "SELECT DISTINCT category FROM $table ORDER by category";
196     $sth = $dbh->prepare($query);
197     if ($sth && (my $rc = $sth->execute) > 0) {
198         while (@row = $sth->fetchrow_array) {
199             push(@arr, $row[0]) if ($row[0]);
200         }
201     }
202     return @arr;
203 }
204
205 sub list_categories
206 {
207     my $field = shift;
208     my $answers = shift;
209
210     @categories = get_categories unless @categories;
211
212     printf "%s\n", join (", ",@categories);
213
214     exit unless $field;
215 }
216
217 sub validate_date
218 {
219     my $ans = shift;
220     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
221
222     return sprintf("%d.%d.%d", $mday, $mon+1, $year+1900) unless length $ans;
223
224     my @arr = split(/\./, $ans);
225
226     return sprintf("%d.%d.%d", $ans, $mon+1, $year+1900) if scalar @arr == 1;
227     return sprintf("%d.%d.%d", $arr[0], $arr[1], $year+1900) if scalar @arr == 2;
228     return sprintf("%d.%d.%d", $arr[0], $arr[1],
229                    length $arr[2] > 2 ? $arr[2] : $arr[2] + 2000);
230 }
231
232 my $answers;
233 sub calculate_price
234 {
235     my $ans = shift;
236
237     if (!defined $answers->{tax_assigned} || !length $answers->{tax_assigned}) {
238         $answers->{tax_assigned} = $ans - ($ans / ((100+$answers->{tax_percent})/100));
239     }
240
241     if ($answers->{einaus} =~ /[ei]/i) {
242         $answers->{tax_assigned} *= -1 if $answers->{tax_assigned} < 0;
243         $ans *= -1 if $ans < 0;
244     } else {
245         $answers->{tax_assigned} *= -1 if $answers->{tax_assigned} > 0;
246         $ans *= -1 if $ans > 0;
247     }
248
249     return $ans;
250 }
251
252 sub complete_category
253 {
254     my ($text, $line, $start) = @_;
255
256     return () unless exists $answers->{category} && length $answers->{category};
257
258     my $sql = sprintf("SELECT DISTINCT description FROM %s WHERE category = '%s' AND description LIKE '%s%%' ORDER BY description",
259                       $table,
260                       $answers->{category},
261                       $line);
262     my $sth = $dbh->prepare($sql);
263     $sth->execute;
264     my @complete;
265     while (my $row = $sth->fetchrow_hashref) {
266         $row->{description} = substr $row->{description}, $start if $start;
267         push @complete, $row->{description};
268     }
269
270     return @complete;
271 }
272
273 sub complete_categoryname
274 {
275     my ($text, $line, $start) = @_;
276
277     return () unless length $line;
278
279     my $sql = sprintf("SELECT DISTINCT category FROM %s WHERE category LIKE '%s%%' ORDER BY category",
280                       $table,
281                       $line);
282     my $sth = $dbh->prepare($sql);
283     $sth->execute;
284     my @complete;
285     while (my $row = $sth->fetchrow_hashref) {
286         $row->{category} = substr $row->{category}, $start if $start;
287         push @complete, $row->{category};
288     }
289
290     return @complete;
291 }
292
293 sub default_year
294 {
295     my $answers = shift;
296
297     my @arr = split(/\./, $answers->{date});
298     return $arr[2];
299 }
300
301 sub read_input
302 {
303     my $name = shift;
304     my $info = shift;
305     my $default;
306     my $ans;
307
308     if (exists $info->{default}) {
309         if ($info->{default} eq 'last') {
310             $default = $answers->{$name} if $answers->{$name};
311         } elsif (reftype $info->{default} && reftype $info->{default} eq 'CODE') {
312             $default = $info->{default}($answers);
313         } elsif ($info->{type} && $info->{type} eq 'boolean') {
314             if ($info->{default}) {
315                 $default = 'J';
316             } else {
317                 $default = 'N';
318             }
319         } else {
320             $default = $info->{default};
321         }
322     }
323
324     if ($info->{complete}) {
325         $term->{completion_function} = $info->{complete};
326     } else {
327         $term->{completion_function} = undef;
328     }
329
330     if ($default) {
331         $ans = $term->readline ($info->{title} . " [" . $default . "]: ");
332     } else {
333         $ans = $term->readline ($info->{title} . ": ");
334     }
335
336     exit unless defined $ans;
337
338     if (!length $ans && defined $default) {
339         $ans = $default;
340     } elsif ($ans eq ".") {
341         $ans = '';
342     }
343
344     return read_input($name, $info) unless length $ans || exists $info->{empty};
345
346     if ($ans eq '?' && exists $info->{lookup}) {
347         $info->{lookup}($name, $answers);
348         return read_input($name, $info);
349     }
350
351     if (exists $info->{type} && $info->{type} eq 'boolean') {
352         if ($ans =~ /[JY1]/i) {
353             $ans = 1;
354         } else {
355             $ans = 0;
356         }
357     } elsif (exists $info->{validate} && reftype $info->{validate} eq 'CODE') {
358         $ans = $info->{validate}($ans);
359     } else {
360         $ans =~ s/ *$// if $ans;
361     }
362
363     if (!exists $info->{type} || $info->{type} ne 'boolean') {
364         $term->addhistory($ans);
365     }
366
367     return $ans;
368 }
369
370 # Gibt die naechste freie Nr. in der Datenbank zurueck.  Wenn der
371 # INSERT nicht schnell darauf folgt, kann es passieren, dass die
372 # Nr. anschliessend bereits wieder vergeben ist.
373 #
374 sub get_next_nr
375 {
376     my $query;
377     my $sth;
378     my $rc;
379     my @row;
380
381     $query = "SELECT nr FROM $table ORDER BY nr DESC";
382     $sth = $dbh->prepare($query);
383     if ($sth) {
384         $rc = $sth->execute;
385         if ($rc > 0 && (@row = $sth->fetchrow_array)) {
386             return $row[0] + 1;
387         }
388     }
389     return 1;
390 }
391
392 sub buchung_input
393 {
394     my $weiter = 'y';
395     my $ans;
396     my ($date_sec,$date_min,$date_hour,$date_mday,$date_mon,$date_year,$date_wday,$date_isdst)
397         = localtime;
398
399     my $fields = {
400         'date' => {
401             'title' => 'Datum',
402             'validate' => \&validate_date,
403             'default' => 'last'},
404         'pdf' => {
405             'title' => 'PDF',
406             'type' => 'boolean',
407             'default' => 0},
408         'year' => {
409             'title' => 'Jahr',
410             'default' => \&default_year},
411         'category' => {
412             'title' => 'Kategorie',
413             'lookup' => \&list_categories,
414             'default' => 'last',
415             'complete' => \&complete_categoryname},
416         'description' => {
417             'title' => 'Beschreibung',
418             'default' => 'last',
419             'complete' => \&complete_category},
420         'einaus' => {
421             'title' => 'Ein/Aus',
422             'default' => 'a',
423             'save' => 0},
424         'tax_percent' => {
425             'title' => 'Steuersatz',
426             'default' => '19'},
427         'tax_assigned' => {
428             'title' => 'Umsatzsteuer',
429             'empty' => 1},
430         'price' => {
431             'title' => 'Betrag',
432             'validate' => \&calculate_price},
433         'paid' => {
434             'title' => 'bezahlt',
435             'type' => 'boolean',
436             'default' => 0},
437         'weiter' => {
438             'title' => 'Weiter',
439             'type' => 'boolean',
440             'default' => 1,
441             'save' => 0},
442     };
443     my @fields = ('date','year','pdf','category','description','einaus','tax_percent','tax_assigned','price','paid','weiter');
444
445     @categories = get_categories unless @categories;
446
447     $term = new Term::ReadLine '' unless $term;
448
449     my $sth = $dbh->prepare ("INSERT INTO $table (nr,date,pdf,year,category,description,tax_percent,tax_assigned,price,billing_date,paid) " .
450                              "VALUES (?,?,?,?,?,?,?,?,?,?,?)");
451
452     print "Buchungseingabe\n\n";
453     while ($weiter =~ /[JjYy1]/) {
454         foreach my $f (@fields) {
455             if ($f eq 'tax_assigned' && $answers->{'tax_percent'} == 0) {
456                 $answers->{$f} = 0;
457                 next;
458             } elsif ($f eq 'paid') {
459                 if ($answers->{paid}) {
460                     my @now = localtime(time);
461                     $answers->{billing_date} = sprintf('%04d-%02d-%02d', $now[5]+1900, $now[4]+1, $now[3]);
462                 } else {
463                     $answers->{billing_date} = undef;
464                 }
465             }
466             $ans = read_input($f, $fields->{$f});
467             $answers->{$f} = $ans;
468         }
469
470         $sth->execute(get_next_nr(),
471                       date_to_string($answers->{date}),
472                       $answers->{pdf},
473                       $answers->{year},
474                       $answers->{category},
475                       $answers->{description},
476                       $answers->{tax_percent},
477                       $answers->{tax_assigned},
478                       $answers->{price},
479                       $answers->{billing_date},
480                       $answers->{paid});
481
482         $weiter = $answers->{weiter};
483         $answers->{tax_assigned} = 0.0;
484     }
485
486     exit;
487 }
488
489 sub buchung_hidden
490 {
491     $table = "sales_dm";
492     sales_list("visible = 0");
493     $table = "sales";
494     sales_list("visible = 0");
495     exit;
496 }
497
498 sub buchung_unpaid
499 {
500     $table = "sales";
501     sales_list("paid = 0");
502     exit;
503 }
504
505 sub usage
506 {
507     print "infocon [options] [-h|--help] commands\n";
508     print "  --buchung-category|-bc [category]\n";
509     print "  --buchung-input|-bi\n";
510     print "  --buchung-unpaid\n";
511     print "  --buchung-hidden\n";
512     print "  --pay <nr> | --unpay <nr>\n";
513     print "  --hide <nr> | --unhide <nr>\n";
514     print "  --list-categories|-lc\n";
515     print "  Options:\n";
516     print "    --all|-a\n";
517     print "    --verbose|-v\n";
518     print "    --year|-y year\n";
519     print "    --direction|--dir|-d in|out\n";
520     print "    --dm\n";
521     exit 0;
522 }
523
524 %data = (
525     'category' => undef,
526     'done' => undef,
527     'pay' => undef,
528     'unpay' => undef,
529     'hide' => undef,
530     'unhide' => undef,
531     'mailto' => undef,
532     'buchung-input' => undef,
533     'buchung-unpaid' => undef,
534     'buchung-hidden' => undef,
535     'list-categories' => undef,
536     );
537 my %options = (
538     'buchung-category|bc:s' => \$data{category},
539     'pay=s' => \$data{pay},
540     'unpay=s' => \$data{unpay},
541     'hide=s' => \$data{hide},
542     'unhide=s' => \$data{unhide},
543     'year=i' => \$opt_year,
544     'direction|d=s' => \$opt_direction,
545     'mailto:s' => \$data{mailto},
546     'all' => \$opt_all,
547     'verbose' => \$opt_verbose,
548     'help' => \&usage,
549     'dm' => sub {$table = "sales_dm"},
550     'buchung-input|bi' => \$data{'buchung-input'},
551     'buchung-unpaid|bu' => \$data{'buchung-unpaid'},
552     'buchung-hidden|bh' => \$data{'buchung-hidden'},
553     'list-categories|lc' => \$data{'list-categories'},
554     );
555
556 my $cmdln = 'infocon ' . join (' ', @ARGV);
557 GetOptions(%options);
558
559 if ($opt_year != 0 && $opt_year < 2002) {
560     $table = "sales_dm";
561 }
562
563 if (defined $opt_direction) {
564     usage unless $opt_direction =~ /^(in|out)$/i;
565 }
566
567 if (defined $data{mailto}) {
568     if (open(STDOUT, "| /usr/sbin/sendmail -t")) {
569         print  "From: Joey Schulze <joey\@infodrom.org>\n";
570         printf "To: %s\n", length($data{mailto})?$data{mailto}:'Joey Schulze <joey@infodrom.org>';
571         printf "Subject: %s\n", $cmdln;
572         print  "MIME-Version: 1.0\n";
573         print  "Content-type: text/plain; charset=iso-8859-1\n";
574         print  "Content-Disposition: inline\n";
575         print  "Content-Transfer-Encoding: 8bit\n";
576         print  "\n";
577     }
578 }
579
580 if (defined $data{category}) {
581     if (length($data{category})) {
582         sales_list("category = '".$data{category}."'");
583     } else {
584         sales_list;
585     }
586     exit;
587 } elsif (defined $data{'buchung-input'}) {
588     buchung_input;
589 } elsif (defined $data{'buchung-unpaid'}) {
590     buchung_unpaid;
591 } elsif (defined $data{'buchung-hidden'}) {
592     buchung_hidden;
593 } elsif (defined $data{'list-categories'}) {
594     list_categories;
595 } elsif (defined $data{pay}) {
596     pay_invoice($data{pay}, 1);
597 } elsif (defined $data{unpay}) {
598     pay_invoice($data{unpay}, 0);
599 } elsif (defined $data{hide}) {
600     hide_invoice($data{hide}, 1);
601 } elsif (defined $data{unhide}) {
602     hide_invoice($data{unhide}, 0);
603 } else {
604     usage;
605 }