Individuelle Lieferadresse hinzufügen: cp_gender nicht auf leer prüfen …
[kivitendo-erp.git] / SL / Form.pm
1 #=====================================================================
2 # LX-Office ERP
3 # Copyright (C) 2004
4 # Based on SQL-Ledger Version 2.1.9
5 # Web http://www.lx-office.org
6 #
7 #=====================================================================
8 # SQL-Ledger Accounting
9 # Copyright (C) 1998-2002
10 #
11 #  Author: Dieter Simader
12 #   Email: dsimader@sql-ledger.org
13 #     Web: http://www.sql-ledger.org
14 #
15 # Contributors: Thomas Bayen <bayen@gmx.de>
16 #               Antti Kaihola <akaihola@siba.fi>
17 #               Moritz Bunkus (tex code)
18 #
19 # This program is free software; you can redistribute it and/or modify
20 # it under the terms of the GNU General Public License as published by
21 # the Free Software Foundation; either version 2 of the License, or
22 # (at your option) any later version.
23 #
24 # This program is distributed in the hope that it will be useful,
25 # but WITHOUT ANY WARRANTY; without even the implied warranty of
26 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27 # GNU General Public License for more details.
28 # You should have received a copy of the GNU General Public License
29 # along with this program; if not, write to the Free Software
30 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
31 # MA 02110-1335, USA.
32 #======================================================================
33 # Utilities for parsing forms
34 # and supporting routines for linking account numbers
35 # used in AR, AP and IS, IR modules
36 #
37 #======================================================================
38
39 package Form;
40
41 use Carp;
42 use Data::Dumper;
43
44 use Carp;
45 use Config;
46 use CGI;
47 use Cwd;
48 use Encode;
49 use File::Copy;
50 use IO::File;
51 use Math::BigInt;
52 use POSIX qw(strftime);
53 use SL::Auth;
54 use SL::Auth::DB;
55 use SL::Auth::LDAP;
56 use SL::AM;
57 use SL::Common;
58 use SL::CVar;
59 use SL::DB;
60 use SL::DBConnect;
61 use SL::DBUtils;
62 use SL::DB::Customer;
63 use SL::DB::Default;
64 use SL::DB::PaymentTerm;
65 use SL::DB::Vendor;
66 use SL::DO;
67 use SL::Helper::Flash qw();
68 use SL::IC;
69 use SL::IS;
70 use SL::Layout::Dispatcher;
71 use SL::Locale;
72 use SL::Locale::String;
73 use SL::Mailer;
74 use SL::Menu;
75 use SL::MoreCommon qw(uri_encode uri_decode);
76 use SL::OE;
77 use SL::PrefixedNumber;
78 use SL::Request;
79 use SL::Template;
80 use SL::User;
81 use SL::Util;
82 use SL::Version;
83 use SL::X;
84 use Template;
85 use URI;
86 use List::Util qw(first max min sum);
87 use List::MoreUtils qw(all any apply);
88 use SL::DB::Tax;
89 use SL::Helper::File qw(:all);
90 use SL::Helper::CreatePDF qw(merge_pdfs);
91
92 use strict;
93
94 sub read_version {
95   SL::Version->get_version;
96 }
97
98 sub new {
99   $main::lxdebug->enter_sub();
100
101   my $type = shift;
102
103   my $self = {};
104
105   no warnings 'once';
106   if ($LXDebug::watch_form) {
107     require SL::Watchdog;
108     tie %{ $self }, 'SL::Watchdog';
109   }
110
111   bless $self, $type;
112
113   $main::lxdebug->leave_sub();
114
115   return $self;
116 }
117
118 sub read_cgi_input {
119   my ($self) = @_;
120   SL::Request::read_cgi_input($self);
121 }
122
123 sub _flatten_variables_rec {
124   $main::lxdebug->enter_sub(2);
125
126   my $self   = shift;
127   my $curr   = shift;
128   my $prefix = shift;
129   my $key    = shift;
130
131   my @result;
132
133   if ('' eq ref $curr->{$key}) {
134     @result = ({ 'key' => $prefix . $key, 'value' => $curr->{$key} });
135
136   } elsif ('HASH' eq ref $curr->{$key}) {
137     foreach my $hash_key (sort keys %{ $curr->{$key} }) {
138       push @result, $self->_flatten_variables_rec($curr->{$key}, $prefix . $key . '.', $hash_key);
139     }
140
141   } else {
142     foreach my $idx (0 .. scalar @{ $curr->{$key} } - 1) {
143       my $first_array_entry = 1;
144
145       my $element = $curr->{$key}[$idx];
146
147       if ('HASH' eq ref $element) {
148         foreach my $hash_key (sort keys %{ $element }) {
149           push @result, $self->_flatten_variables_rec($element, $prefix . $key . ($first_array_entry ? '[+].' : '[].'), $hash_key);
150           $first_array_entry = 0;
151         }
152       } else {
153         push @result, { 'key' => $prefix . $key . '[]', 'value' => $element };
154       }
155     }
156   }
157
158   $main::lxdebug->leave_sub(2);
159
160   return @result;
161 }
162
163 sub flatten_variables {
164   $main::lxdebug->enter_sub(2);
165
166   my $self = shift;
167   my @keys = @_;
168
169   my @variables;
170
171   foreach (@keys) {
172     push @variables, $self->_flatten_variables_rec($self, '', $_);
173   }
174
175   $main::lxdebug->leave_sub(2);
176
177   return @variables;
178 }
179
180 sub flatten_standard_variables {
181   $main::lxdebug->enter_sub(2);
182
183   my $self      = shift;
184   my %skip_keys = map { $_ => 1 } (qw(login password header stylesheet titlebar), @_);
185
186   my @variables;
187
188   foreach (grep { ! $skip_keys{$_} } keys %{ $self }) {
189     push @variables, $self->_flatten_variables_rec($self, '', $_);
190   }
191
192   $main::lxdebug->leave_sub(2);
193
194   return @variables;
195 }
196
197 sub escape {
198   my ($self, $str) = @_;
199
200   return uri_encode($str);
201 }
202
203 sub unescape {
204   my ($self, $str) = @_;
205
206   return uri_decode($str);
207 }
208
209 sub quote {
210   $main::lxdebug->enter_sub();
211   my ($self, $str) = @_;
212
213   if ($str && !ref($str)) {
214     $str =~ s/\"/&quot;/g;
215   }
216
217   $main::lxdebug->leave_sub();
218
219   return $str;
220 }
221
222 sub unquote {
223   $main::lxdebug->enter_sub();
224   my ($self, $str) = @_;
225
226   if ($str && !ref($str)) {
227     $str =~ s/&quot;/\"/g;
228   }
229
230   $main::lxdebug->leave_sub();
231
232   return $str;
233 }
234
235 sub hide_form {
236   $main::lxdebug->enter_sub();
237   my $self = shift;
238
239   if (@_) {
240     map({ print($::request->{cgi}->hidden("-name" => $_, "-default" => $self->{$_}) . "\n"); } @_);
241   } else {
242     for (sort keys %$self) {
243       next if (($_ eq "header") || (ref($self->{$_}) ne ""));
244       print($::request->{cgi}->hidden("-name" => $_, "-default" => $self->{$_}) . "\n");
245     }
246   }
247   $main::lxdebug->leave_sub();
248 }
249
250 sub throw_on_error {
251   my ($self, $code) = @_;
252   local $self->{__ERROR_HANDLER} = sub { SL::X::FormError->throw(error => $_[0]) };
253   $code->();
254 }
255
256 sub error {
257   $main::lxdebug->enter_sub();
258
259   $main::lxdebug->show_backtrace();
260
261   my ($self, $msg) = @_;
262
263   if ($self->{__ERROR_HANDLER}) {
264     $self->{__ERROR_HANDLER}->($msg);
265
266   } elsif ($ENV{HTTP_USER_AGENT}) {
267     $msg =~ s/\n/<br>/g;
268     $self->show_generic_error($msg);
269
270   } else {
271     confess "Error: $msg\n";
272   }
273
274   $main::lxdebug->leave_sub();
275 }
276
277 sub info {
278   $main::lxdebug->enter_sub();
279
280   my ($self, $msg) = @_;
281
282   if ($ENV{HTTP_USER_AGENT}) {
283     $self->header;
284     print $self->parse_html_template('generic/form_info', { message => $msg });
285
286   } elsif ($self->{info_function}) {
287     &{ $self->{info_function} }($msg);
288   } else {
289     print "$msg\n";
290   }
291
292   $main::lxdebug->leave_sub();
293 }
294
295 # calculates the number of rows in a textarea based on the content and column number
296 # can be capped with maxrows
297 sub numtextrows {
298   $main::lxdebug->enter_sub();
299   my ($self, $str, $cols, $maxrows, $minrows) = @_;
300
301   $minrows ||= 1;
302
303   my $rows   = sum map { int((length() - 2) / $cols) + 1 } split /\r/, $str;
304   $maxrows ||= $rows;
305
306   $main::lxdebug->leave_sub();
307
308   return max(min($rows, $maxrows), $minrows);
309 }
310
311 sub dberror {
312   my ($self, $msg) = @_;
313
314   SL::X::DBError->throw(
315     msg      => $msg,
316     db_error => $DBI::errstr,
317   );
318 }
319
320 sub isblank {
321   $main::lxdebug->enter_sub();
322
323   my ($self, $name, $msg) = @_;
324
325   my $curr = $self;
326   foreach my $part (split m/\./, $name) {
327     if (!$curr->{$part} || ($curr->{$part} =~ /^\s*$/)) {
328       $self->error($msg);
329     }
330     $curr = $curr->{$part};
331   }
332
333   $main::lxdebug->leave_sub();
334 }
335
336 sub _get_request_uri {
337   my $self = shift;
338
339   return URI->new($ENV{HTTP_REFERER})->canonical() if $ENV{HTTP_X_FORWARDED_FOR};
340   return URI->new                                  if !$ENV{REQUEST_URI}; # for testing
341
342   my $scheme =  $::request->is_https ? 'https' : 'http';
343   my $port   =  $ENV{SERVER_PORT};
344   $port      =  undef if (($scheme eq 'http' ) && ($port == 80))
345                       || (($scheme eq 'https') && ($port == 443));
346
347   my $uri    =  URI->new("${scheme}://");
348   $uri->scheme($scheme);
349   $uri->port($port);
350   $uri->host($ENV{HTTP_HOST} || $ENV{SERVER_ADDR});
351   $uri->path_query($ENV{REQUEST_URI});
352   $uri->query('');
353
354   return $uri;
355 }
356
357 sub _add_to_request_uri {
358   my $self              = shift;
359
360   my $relative_new_path = shift;
361   my $request_uri       = shift || $self->_get_request_uri;
362   my $relative_new_uri  = URI->new($relative_new_path);
363   my @request_segments  = $request_uri->path_segments;
364
365   my $new_uri           = $request_uri->clone;
366   $new_uri->path_segments(@request_segments[0..scalar(@request_segments) - 2], $relative_new_uri->path_segments);
367
368   return $new_uri;
369 }
370
371 sub create_http_response {
372   $main::lxdebug->enter_sub();
373
374   my $self     = shift;
375   my %params   = @_;
376
377   my $cgi      = $::request->{cgi};
378
379   my $session_cookie;
380   if (defined $main::auth) {
381     my $uri      = $self->_get_request_uri;
382     my @segments = $uri->path_segments;
383     pop @segments;
384     $uri->path_segments(@segments);
385
386     my $session_cookie_value = $main::auth->get_session_id();
387
388     if ($session_cookie_value) {
389       $session_cookie = $cgi->cookie('-name'   => $main::auth->get_session_cookie_name(),
390                                      '-value'  => $session_cookie_value,
391                                      '-path'   => $uri->path,
392                                      '-secure' => $::request->is_https);
393     }
394   }
395
396   my %cgi_params = ('-type' => $params{content_type});
397   $cgi_params{'-charset'} = $params{charset} if ($params{charset});
398   $cgi_params{'-cookie'}  = $session_cookie  if ($session_cookie);
399
400   map { $cgi_params{'-' . $_} = $params{$_} if exists $params{$_} } qw(content_disposition content_length status);
401
402   my $output = $cgi->header(%cgi_params);
403
404   $main::lxdebug->leave_sub();
405
406   return $output;
407 }
408
409 sub header {
410   $::lxdebug->enter_sub;
411
412   my ($self, %params) = @_;
413   my @header;
414
415   $::lxdebug->leave_sub and return if !$ENV{HTTP_USER_AGENT} || $self->{header}++;
416
417   if ($params{no_layout}) {
418     $::request->{layout} = SL::Layout::Dispatcher->new(style => 'none');
419   }
420
421   my $layout = $::request->{layout};
422
423   # standard css for all
424   # this should gradually move to the layouts that need it
425   $layout->use_stylesheet("$_.css") for qw(
426     common main menu list_accounts jquery.autocomplete
427     jquery.multiselect2side
428     ui-lightness/jquery-ui
429     jquery-ui.custom
430     tooltipster themes/tooltipster-light
431   );
432
433   $layout->use_javascript("$_.js") for (qw(
434     jquery jquery-ui jquery.cookie jquery.checkall jquery.download
435     jquery/jquery.form jquery/fixes client_js
436     jquery/jquery.tooltipster.min
437     common part_selection
438   ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}");
439
440   $self->{favicon} ||= "favicon.ico";
441   $self->{titlebar} = join ' - ', grep $_, $self->{title}, $self->{login}, $::myconfig{dbname}, $self->read_version if $self->{title} || !$self->{titlebar};
442
443   # build includes
444   if ($self->{refresh_url} || $self->{refresh_time}) {
445     my $refresh_time = $self->{refresh_time} || 3;
446     my $refresh_url  = $self->{refresh_url}  || $ENV{REFERER};
447     push @header, "<meta http-equiv='refresh' content='$refresh_time;$refresh_url'>";
448   }
449
450   my $auto_reload_resources_param = $layout->auto_reload_resources_param;
451
452   push @header, map { qq|<link rel="stylesheet" href="${_}${auto_reload_resources_param}" type="text/css" title="Stylesheet">| } $layout->stylesheets;
453   push @header, "<style type='text/css'>\@page { size:landscape; }</style> "                     if $self->{landscape};
454   push @header, "<link rel='shortcut icon' href='$self->{favicon}' type='image/x-icon'>"         if -f $self->{favicon};
455   push @header, map { qq|<script type="text/javascript" src="${_}${auto_reload_resources_param}"></script>| }                    $layout->javascripts;
456   push @header, $self->{javascript} if $self->{javascript};
457   push @header, map { $_->show_javascript } @{ $self->{AJAX} || [] };
458
459   my  %doctypes = (
460     strict       => qq|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">|,
461     transitional => qq|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">|,
462     frameset     => qq|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">|,
463     html5        => qq|<!DOCTYPE html>|,
464   );
465
466   # output
467   print $self->create_http_response(content_type => 'text/html', charset => 'UTF-8');
468   print $doctypes{$params{doctype} || 'transitional'}, $/;
469   print <<EOT;
470 <html>
471  <head>
472   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
473   <title>$self->{titlebar}</title>
474 EOT
475   print "  $_\n" for @header;
476   print <<EOT;
477   <meta name="robots" content="noindex,nofollow">
478  </head>
479  <body>
480
481 EOT
482   print $::request->{layout}->pre_content;
483   print $::request->{layout}->start_content;
484
485   $layout->header_done;
486
487   $::lxdebug->leave_sub;
488 }
489
490 sub footer {
491   return unless $::request->{layout}->need_footer;
492
493   print $::request->{layout}->end_content;
494   print $::request->{layout}->post_content;
495
496   if (my @inline_scripts = $::request->{layout}->javascripts_inline) {
497     print "<script type='text/javascript'>" . join("; ", @inline_scripts) . "</script>\n";
498   }
499
500   print <<EOL
501  </body>
502 </html>
503 EOL
504 }
505
506 sub ajax_response_header {
507   $main::lxdebug->enter_sub();
508
509   my ($self) = @_;
510
511   my $output = $::request->{cgi}->header('-charset' => 'UTF-8');
512
513   $main::lxdebug->leave_sub();
514
515   return $output;
516 }
517
518 sub redirect_header {
519   my $self     = shift;
520   my $new_url  = shift;
521
522   my $base_uri = $self->_get_request_uri;
523   my $new_uri  = URI->new_abs($new_url, $base_uri);
524
525   die "Headers already sent" if $self->{header};
526   $self->{header} = 1;
527
528   return $::request->{cgi}->redirect($new_uri);
529 }
530
531 sub set_standard_title {
532   $::lxdebug->enter_sub;
533   my $self = shift;
534
535   $self->{titlebar}  = "kivitendo " . $::locale->text('Version') . " " . $self->read_version;
536   $self->{titlebar} .= "- $::myconfig{name}"   if $::myconfig{name};
537   $self->{titlebar} .= "- $::myconfig{dbname}" if $::myconfig{name};
538
539   $::lxdebug->leave_sub;
540 }
541
542 sub _prepare_html_template {
543   $main::lxdebug->enter_sub();
544
545   my ($self, $file, $additional_params) = @_;
546   my $language;
547
548   if (!%::myconfig || !$::myconfig{"countrycode"}) {
549     $language = $::lx_office_conf{system}->{language};
550   } else {
551     $language = $main::myconfig{"countrycode"};
552   }
553   $language = "de" unless ($language);
554
555   if (-f "templates/webpages/${file}.html") {
556     $file = "templates/webpages/${file}.html";
557
558   } elsif (ref $file eq 'SCALAR') {
559     # file is a scalarref, use inline mode
560   } else {
561     my $info = "Web page template '${file}' not found.\n";
562     $::form->header;
563     print qq|<pre>$info</pre>|;
564     $::dispatcher->end_request;
565   }
566
567   $additional_params->{AUTH}          = $::auth;
568   $additional_params->{INSTANCE_CONF} = $::instance_conf;
569   $additional_params->{LOCALE}        = $::locale;
570   $additional_params->{LXCONFIG}      = \%::lx_office_conf;
571   $additional_params->{LXDEBUG}       = $::lxdebug;
572   $additional_params->{MYCONFIG}      = \%::myconfig;
573
574   $main::lxdebug->leave_sub();
575
576   return $file;
577 }
578
579 sub parse_html_template {
580   $main::lxdebug->enter_sub();
581
582   my ($self, $file, $additional_params) = @_;
583
584   $additional_params ||= { };
585
586   my $real_file = $self->_prepare_html_template($file, $additional_params);
587   my $template  = $self->template;
588
589   map { $additional_params->{$_} ||= $self->{$_} } keys %{ $self };
590
591   my $output;
592   $template->process($real_file, $additional_params, \$output) || die $template->error;
593
594   $main::lxdebug->leave_sub();
595
596   return $output;
597 }
598
599 sub template { $::request->presenter->get_template }
600
601 sub show_generic_error {
602   $main::lxdebug->enter_sub();
603
604   my ($self, $error, %params) = @_;
605
606   if ($self->{__ERROR_HANDLER}) {
607     $self->{__ERROR_HANDLER}->($error);
608     $main::lxdebug->leave_sub();
609     return;
610   }
611
612   if ($::request->is_ajax) {
613     SL::ClientJS->new
614       ->error($error)
615       ->render(SL::Controller::Base->new);
616     $::dispatcher->end_request;
617   }
618
619   my $add_params = {
620     'title_error' => $params{title},
621     'label_error' => $error,
622   };
623
624   $self->{title} = $params{title} if $params{title};
625
626   for my $bar ($::request->layout->get('actionbar')) {
627     $bar->add(
628       action => [
629         t8('Back'),
630         call      => [ 'kivi.history_back' ],
631         accesskey => 'enter',
632       ],
633     );
634   }
635
636   $self->header();
637   print $self->parse_html_template("generic/error", $add_params);
638
639   print STDERR "Error: $error\n";
640
641   $main::lxdebug->leave_sub();
642
643   $::dispatcher->end_request;
644 }
645
646 sub show_generic_information {
647   $main::lxdebug->enter_sub();
648
649   my ($self, $text, $title) = @_;
650
651   my $add_params = {
652     'title_information' => $title,
653     'label_information' => $text,
654   };
655
656   $self->{title} = $title if ($title);
657
658   $self->header();
659   print $self->parse_html_template("generic/information", $add_params);
660
661   $main::lxdebug->leave_sub();
662
663   $::dispatcher->end_request;
664 }
665
666 sub _store_redirect_info_in_session {
667   my ($self) = @_;
668
669   return unless $self->{callback} =~ m:^ ( [^\?/]+ \.pl ) \? (.+) :x;
670
671   my ($controller, $params) = ($1, $2);
672   my $form                  = { map { map { $self->unescape($_) } split /=/, $_, 2 } split m/\&/, $params };
673   $self->{callback}         = "${controller}?RESTORE_FORM_FROM_SESSION_ID=" . $::auth->save_form_in_session(form => $form);
674 }
675
676 sub redirect {
677   $main::lxdebug->enter_sub();
678
679   my ($self, $msg) = @_;
680
681   if (!$self->{callback}) {
682     $self->info($msg);
683
684   } else {
685     SL::Helper::Flash::flash_later('info', $msg) if $msg;
686     $self->_store_redirect_info_in_session;
687     print $::form->redirect_header($self->{callback});
688   }
689
690   $::dispatcher->end_request;
691
692   $main::lxdebug->leave_sub();
693 }
694
695 # sort of columns removed - empty sub
696 sub sort_columns {
697   $main::lxdebug->enter_sub();
698
699   my ($self, @columns) = @_;
700
701   $main::lxdebug->leave_sub();
702
703   return @columns;
704 }
705 #
706 sub format_amount {
707   $main::lxdebug->enter_sub(2);
708
709   my ($self, $myconfig, $amount, $places, $dash) = @_;
710   $amount ||= 0;
711   $dash   ||= '';
712   my $neg = $amount < 0;
713   my $force_places = defined $places && $places >= 0;
714
715   $amount = $self->round_amount($amount, abs $places) if $force_places;
716   $neg    = 0 if $amount == 0; # don't show negative zero
717   $amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa
718
719   # before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl
720   # this is easy to confuse, so keep in mind: before this comment no s///, m//, concat or other strong ops on
721   # $amount. after this comment no +,-,*,/,abs. it will only introduce subtle bugs.
722
723   $amount =~ s/0*$// unless defined $places && $places == 0;             # cull trailing 0s
724
725   my @d = map { s/\d//g; reverse split // } my $tmp = $myconfig->{numberformat}; # get delim chars
726   my @p = split(/\./, $amount);                                          # split amount at decimal point
727
728   $p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1];                             # add 1,000 delimiters
729   $amount = $p[0];
730   if ($places || $p[1]) {
731     $amount .= $d[0]
732             .  ( $p[1] || '' )
733             .  (0 x max(abs($places || 0) - length ($p[1]||''), 0));     # pad the fraction
734   }
735
736   $amount = do {
737     ($dash =~ /-/)    ? ($neg ? "($amount)"                            : "$amount" )                              :
738     ($dash =~ /DRCR/) ? ($neg ? "$amount " . $main::locale->text('DR') : "$amount " . $main::locale->text('CR') ) :
739                         ($neg ? "-$amount"                             : "$amount" )                              ;
740   };
741
742   $main::lxdebug->leave_sub(2);
743   return $amount;
744 }
745
746 sub format_amount_units {
747   $main::lxdebug->enter_sub();
748
749   my $self             = shift;
750   my %params           = @_;
751
752   my $myconfig         = \%main::myconfig;
753   my $amount           = $params{amount} * 1;
754   my $places           = $params{places};
755   my $part_unit_name   = $params{part_unit};
756   my $amount_unit_name = $params{amount_unit};
757   my $conv_units       = $params{conv_units};
758   my $max_places       = $params{max_places};
759
760   if (!$part_unit_name) {
761     $main::lxdebug->leave_sub();
762     return '';
763   }
764
765   my $all_units        = AM->retrieve_all_units;
766
767   if (('' eq ref $conv_units) && ($conv_units =~ /convertible/)) {
768     $conv_units = AM->convertible_units($all_units, $part_unit_name, $conv_units eq 'convertible_not_smaller');
769   }
770
771   if (!scalar @{ $conv_units }) {
772     my $result = $self->format_amount($myconfig, $amount, $places, undef, $max_places) . " " . $part_unit_name;
773     $main::lxdebug->leave_sub();
774     return $result;
775   }
776
777   my $part_unit  = $all_units->{$part_unit_name};
778   my $conv_unit  = ($amount_unit_name && ($amount_unit_name ne $part_unit_name)) ? $all_units->{$amount_unit_name} : $part_unit;
779
780   $amount       *= $conv_unit->{factor};
781
782   my @values;
783   my $num;
784
785   foreach my $unit (@$conv_units) {
786     my $last = $unit->{name} eq $part_unit->{name};
787     if (!$last) {
788       $num     = int($amount / $unit->{factor});
789       $amount -= $num * $unit->{factor};
790     }
791
792     if ($last ? $amount : $num) {
793       push @values, { "unit"   => $unit->{name},
794                       "amount" => $last ? $amount / $unit->{factor} : $num,
795                       "places" => $last ? $places : 0 };
796     }
797
798     last if $last;
799   }
800
801   if (!@values) {
802     push @values, { "unit"   => $part_unit_name,
803                     "amount" => 0,
804                     "places" => 0 };
805   }
806
807   my $result = join " ", map { $self->format_amount($myconfig, $_->{amount}, $_->{places}, undef, $max_places), $_->{unit} } @values;
808
809   $main::lxdebug->leave_sub();
810
811   return $result;
812 }
813
814 sub format_string {
815   $main::lxdebug->enter_sub(2);
816
817   my $self  = shift;
818   my $input = shift;
819
820   $input =~ s/(^|[^\#]) \#  (\d+)  /$1$_[$2 - 1]/gx;
821   $input =~ s/(^|[^\#]) \#\{(\d+)\}/$1$_[$2 - 1]/gx;
822   $input =~ s/\#\#/\#/g;
823
824   $main::lxdebug->leave_sub(2);
825
826   return $input;
827 }
828
829 #
830
831 sub parse_amount {
832   $main::lxdebug->enter_sub(2);
833
834   my ($self, $myconfig, $amount) = @_;
835
836   if (!defined($amount) || ($amount eq '')) {
837     $main::lxdebug->leave_sub(2);
838     return 0;
839   }
840
841   if (   ($myconfig->{numberformat} eq '1.000,00')
842       || ($myconfig->{numberformat} eq '1000,00')) {
843     $amount =~ s/\.//g;
844     $amount =~ s/,/\./g;
845   }
846
847   if ($myconfig->{numberformat} eq "1'000.00") {
848     $amount =~ s/\'//g;
849   }
850
851   $amount =~ s/,//g;
852
853   $main::lxdebug->leave_sub(2);
854
855   # Make sure no code wich is not a math expression ends up in eval().
856   return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x;
857
858   # Prevent numbers from being parsed as octals;
859   $amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx;
860
861   return scalar(eval($amount)) * 1 ;
862 }
863
864 sub round_amount {
865   my ($self, $amount, $places, $adjust) = @_;
866
867   return 0 if !defined $amount;
868
869   $places //= 0;
870
871   if ($adjust) {
872     my $precision = $::instance_conf->get_precision || 0.01;
873     return $self->round_amount( $self->round_amount($amount / $precision, 0) * $precision, $places);
874   }
875
876   # We use Perl's knowledge of string representation for
877   # rounding. First, convert the floating point number to a string
878   # with a high number of places. Then split the string on the decimal
879   # sign and use integer calculation for rounding the decimal places
880   # part. If an overflow occurs then apply that overflow to the part
881   # before the decimal sign as well using integer arithmetic again.
882
883   my $int_amount = int(abs $amount);
884   my $str_places = max(min(10, 16 - length("$int_amount") - $places), $places);
885   my $amount_str = sprintf '%.*f', $places + $str_places, abs($amount);
886
887   return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$};
888
889   my ($pre, $post)      = ($1, $2);
890   my $decimals          = '1' . substr($post, 0, $places);
891
892   my $propagation_limit = $Config{i32size} == 4 ? 7 : 18;
893   my $add_for_rounding  = substr($post, $places, 1) >= 5 ? 1 : 0;
894
895   if ($places > $propagation_limit) {
896     $decimals = Math::BigInt->new($decimals)->badd($add_for_rounding);
897     $pre      = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2';
898
899   } else {
900     $decimals += $add_for_rounding;
901     $pre      += 1 if substr($decimals, 0, 1) eq '2';
902   }
903
904   $amount  = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0);
905
906   return $amount;
907 }
908
909 sub parse_template {
910   $main::lxdebug->enter_sub();
911
912   my ($self, $myconfig) = @_;
913   my ($out, $out_mode);
914
915   local (*IN, *OUT);
916
917   my $defaults  = SL::DB::Default->get;
918   my $userspath = $::lx_office_conf{paths}->{userspath};
919
920   $self->{"cwd"} = getcwd();
921   $self->{"tmpdir"} = $self->{cwd} . "/${userspath}";
922
923   my $ext_for_format;
924
925   my $template_type;
926   if ($self->{"format"} =~ /(opendocument|oasis)/i) {
927     $template_type  = 'OpenDocument';
928     $ext_for_format = $self->{"format"} =~ m/pdf/ ? 'pdf' : 'odt';
929
930   } elsif ($self->{"format"} =~ /(postscript|pdf)/i) {
931     $template_type    = 'LaTeX';
932     $ext_for_format   = 'pdf';
933
934   } elsif (($self->{"format"} =~ /html/i) || (!$self->{"format"} && ($self->{"IN"} =~ /html$/i))) {
935     $template_type  = 'HTML';
936     $ext_for_format = 'html';
937
938   } elsif (($self->{"format"} =~ /xml/i) || (!$self->{"format"} && ($self->{"IN"} =~ /xml$/i))) {
939     $template_type  = 'XML';
940     $ext_for_format = 'xml';
941
942   } elsif ( $self->{"format"} =~ /elster(?:winston|taxbird)/i ) {
943     $template_type = 'XML';
944
945   } elsif ( $self->{"format"} =~ /excel/i ) {
946     $template_type  = 'Excel';
947     $ext_for_format = 'xls';
948
949   } elsif ( defined $self->{'format'}) {
950     $self->error("Outputformat not defined. This may be a future feature: $self->{'format'}");
951
952   } elsif ( $self->{'format'} eq '' ) {
953     $self->error("No Outputformat given: $self->{'format'}");
954
955   } else { #Catch the rest
956     $self->error("Outputformat not defined: $self->{'format'}");
957   }
958
959   my $template = SL::Template::create(type      => $template_type,
960                                       file_name => $self->{IN},
961                                       form      => $self,
962                                       myconfig  => $myconfig,
963                                       userspath => $userspath,
964                                       %{ $self->{TEMPLATE_DRIVER_OPTIONS} || {} });
965
966   # Copy the notes from the invoice/sales order etc. back to the variable "notes" because that is where most templates expect it to be.
967   $self->{"notes"} = $self->{ $self->{"formname"} . "notes" } if exists $self->{ $self->{"formname"} . "notes" };
968
969   if (!$self->{employee_id}) {
970     $self->{"employee_${_}"} = $myconfig->{$_} for qw(email tel fax name signature);
971     $self->{"employee_${_}"} = $defaults->$_   for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
972   }
973
974   $self->{"myconfig_${_}"} = $myconfig->{$_} for grep { $_ ne 'dbpasswd' } keys %{ $myconfig };
975   $self->{$_}              = $defaults->$_   for qw(co_ustid);
976   $self->{"myconfig_${_}"} = $defaults->$_   for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
977   $self->{AUTH}            = $::auth;
978   $self->{INSTANCE_CONF}   = $::instance_conf;
979   $self->{LOCALE}          = $::locale;
980   $self->{LXCONFIG}        = $::lx_office_conf;
981   $self->{LXDEBUG}         = $::lxdebug;
982   $self->{MYCONFIG}        = \%::myconfig;
983
984   $self->{copies} = 1 if (($self->{copies} *= 1) <= 0);
985
986   # OUT is used for the media, screen, printer, email
987   # for postscript we store a copy in a temporary file
988   my $keep_temp_files = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files};
989
990   my ($temp_fh, $suffix);
991   $suffix =  $self->{IN};
992   $suffix =~ s/.*\.//;
993   ($temp_fh, $self->{tmpfile}) = File::Temp::tempfile(
994     strftime('kivitendo-print-%Y%m%d%H%M%S-XXXXXX', localtime()),
995     SUFFIX => '.' . ($suffix || 'tex'),
996     DIR    => $userspath,
997     UNLINK => $keep_temp_files ? 0 : 1,
998   );
999   close $temp_fh;
1000   chmod 0644, $self->{tmpfile} if $keep_temp_files;
1001   (undef, undef, $self->{template_meta}{tmpfile}) = File::Spec->splitpath( $self->{tmpfile} );
1002
1003   $out              = $self->{OUT};
1004   $out_mode         = $self->{OUT_MODE} || '>';
1005   $self->{OUT}      = "$self->{tmpfile}";
1006   $self->{OUT_MODE} = '>';
1007
1008   my $result;
1009   my $command_formatter = sub {
1010     my ($out_mode, $out) = @_;
1011     return $out_mode eq '|-' ? SL::Template::create(type => 'ShellCommand', form => $self)->parse($out) : $out;
1012   };
1013
1014   if ($self->{OUT}) {
1015     $self->{OUT} = $command_formatter->($self->{OUT_MODE}, $self->{OUT});
1016     open(OUT, $self->{OUT_MODE}, $self->{OUT}) or $self->error("error on opening $self->{OUT} with mode $self->{OUT_MODE} : $!");
1017   } else {
1018     *OUT = ($::dispatcher->get_standard_filehandles)[1];
1019     $self->header;
1020   }
1021
1022   if (!$template->parse(*OUT)) {
1023     $self->cleanup();
1024     $self->error("$self->{IN} : " . $template->get_error());
1025   }
1026
1027   close OUT if $self->{OUT};
1028   # check only one flag (webdav_documents)
1029   # therefore copy to webdav, even if we do not have the webdav feature enabled (just archive)
1030   my $copy_to_webdav =  $::instance_conf->get_webdav_documents && !$self->{preview} && $self->{tmpdir} && $self->{tmpfile} && $self->{type}
1031                         && $self->{type} ne 'statement';
1032   if ( $ext_for_format eq 'pdf' && $self->doc_storage_enabled ) {
1033     $self->append_general_pdf_attachments(filepath =>  $self->{tmpdir}."/".$self->{tmpfile},
1034                                           type     =>  $self->{type});
1035   }
1036   if ($self->{media} eq 'file') {
1037     copy(join('/', $self->{cwd}, $userspath, $self->{tmpfile}), $out =~ m|^/| ? $out : join('/', $self->{cwd}, $out)) if $template->uses_temp_file;
1038
1039     if ($copy_to_webdav) {
1040       if (my $error = Common::copy_file_to_webdav_folder($self)) {
1041         chdir("$self->{cwd}");
1042         $self->error($error);
1043       }
1044     }
1045
1046     if (!$self->{preview} && $self->doc_storage_enabled)
1047     {
1048       $self->{attachment_filename} ||= $self->generate_attachment_filename;
1049       $self->store_pdf($self);
1050     }
1051     $self->cleanup;
1052     chdir("$self->{cwd}");
1053
1054     $::lxdebug->leave_sub();
1055
1056     return;
1057   }
1058
1059   if ($copy_to_webdav) {
1060     if (my $error = Common::copy_file_to_webdav_folder($self)) {
1061       chdir("$self->{cwd}");
1062       $self->error($error);
1063     }
1064   }
1065
1066   if ( !$self->{preview} && $ext_for_format eq 'pdf' && $self->doc_storage_enabled) {
1067     $self->{attachment_filename} ||= $self->generate_attachment_filename;
1068     my $file_obj = $self->store_pdf($self);
1069     $self->{print_file_id} = $file_obj->id if $file_obj;
1070   }
1071   if ($self->{media} eq 'email') {
1072     if ( getcwd() eq $self->{"tmpdir"} ) {
1073       # in the case of generating pdf we are in the tmpdir, but WHY ???
1074       $self->{tmpfile} = $userspath."/".$self->{tmpfile};
1075       chdir("$self->{cwd}");
1076     }
1077     $self->send_email(\%::myconfig,$ext_for_format);
1078   }
1079   else {
1080     $self->{OUT}      = $out;
1081     $self->{OUT_MODE} = $out_mode;
1082     $self->output_file($template->get_mime_type,$command_formatter);
1083   }
1084   delete $self->{print_file_id};
1085
1086   $self->cleanup;
1087
1088   chdir("$self->{cwd}");
1089   $main::lxdebug->leave_sub();
1090 }
1091
1092 sub get_bcc_defaults {
1093   my ($self, $myconfig, $mybcc) = @_;
1094   if (SL::DB::Default->get->bcc_to_login) {
1095     $mybcc .= ", " if $mybcc;
1096     $mybcc .= $myconfig->{email};
1097   }
1098   my $otherbcc = SL::DB::Default->get->global_bcc;
1099   if ($otherbcc) {
1100     $mybcc .= ", " if $mybcc;
1101     $mybcc .= $otherbcc;
1102   }
1103   return $mybcc;
1104 }
1105
1106 sub send_email {
1107   $main::lxdebug->enter_sub();
1108   my ($self, $myconfig, $ext_for_format) = @_;
1109   my $mail = Mailer->new;
1110
1111   map { $mail->{$_} = $self->{$_} }
1112     qw(cc subject message format);
1113
1114   $mail->{bcc}    = $self->get_bcc_defaults($myconfig, $self->{bcc});
1115   $mail->{to}     = $self->{EMAIL_RECIPIENT} ? $self->{EMAIL_RECIPIENT} : $self->{email};
1116   $mail->{from}   = qq|"$myconfig->{name}" <$myconfig->{email}>|;
1117   $mail->{fileid} = time() . '.' . $$ . '.';
1118   my $full_signature     =  $self->create_email_signature();
1119   $full_signature        =~ s/\r//g;
1120
1121   $mail->{attachments} =  [];
1122   my @attfiles;
1123   # if we send html or plain text inline
1124   if (($self->{format} eq 'html') && ($self->{sendmode} eq 'inline')) {
1125     $mail->{content_type}   =  "text/html";
1126     $mail->{message}        =~ s/\r//g;
1127     $mail->{message}        =~ s/\n/<br>\n/g;
1128     $full_signature         =~ s/\n/<br>\n/g;
1129     $mail->{message}       .=  $full_signature;
1130
1131     open(IN, "<", $self->{tmpfile})
1132       or $self->error($self->cleanup . "$self->{tmpfile} : $!");
1133     $mail->{message} .= $_ while <IN>;
1134     close(IN);
1135
1136   } elsif (($self->{attachment_policy} // '') ne 'no_file') {
1137     my $attachment_name  =  $self->{attachment_filename}  || $self->{tmpfile};
1138     $attachment_name     =~ s/\.(.+?)$/.${ext_for_format}/ if ($ext_for_format);
1139
1140     if (($self->{attachment_policy} // '') eq 'old_file') {
1141       my ( $attfile ) = SL::File->get_all(object_id   => $self->{id},
1142                                           object_type => $self->{formname},
1143                                           file_type   => 'document');
1144
1145       if ($attfile) {
1146         $attfile->{override_file_name} = $attachment_name if $attachment_name;
1147         push @attfiles, $attfile;
1148       }
1149
1150     } else {
1151       push @{ $mail->{attachments} }, { path => $self->{tmpfile},
1152                                         id   => $self->{print_file_id},
1153                                         type => "application/pdf",
1154                                         name => $attachment_name };
1155     }
1156   }
1157
1158   push @attfiles,
1159     grep { $_ }
1160     map  { SL::File->get(id => $_) }
1161     @{ $self->{attach_file_ids} // [] };
1162
1163   foreach my $attfile ( @attfiles ) {
1164     push @{ $mail->{attachments} }, {
1165       path    => $attfile->get_file,
1166       id      => $attfile->id,
1167       type    => $attfile->mime_type,
1168       name    => $attfile->{override_file_name} // $attfile->file_name,
1169       content => $attfile->get_content ? ${ $attfile->get_content } : undef,
1170     };
1171   }
1172
1173   $mail->{message}  =~ s/\r//g;
1174   $mail->{message} .= $full_signature;
1175   $self->{emailerr} = $mail->send();
1176
1177   if ($self->{emailerr}) {
1178     $self->cleanup;
1179     $self->error($::locale->text('The email was not sent due to the following error: #1.', $self->{emailerr}));
1180   }
1181
1182   $self->{email_journal_id} = $mail->{journalentry};
1183   $self->{snumbers}  = "emailjournal" . "_" . $self->{email_journal_id};
1184   $self->{what_done} = $::form->{type};
1185   $self->{addition}  = "MAILED";
1186   $self->save_history;
1187
1188   #write back for message info and mail journal
1189   $self->{cc}  = $mail->{cc};
1190   $self->{bcc} = $mail->{bcc};
1191   $self->{email} = $mail->{to};
1192
1193   $main::lxdebug->leave_sub();
1194 }
1195
1196 sub output_file {
1197   $main::lxdebug->enter_sub();
1198
1199   my ($self,$mimeType,$command_formatter) = @_;
1200   my $numbytes = (-s $self->{tmpfile});
1201   open(IN, "<", $self->{tmpfile})
1202     or $self->error($self->cleanup . "$self->{tmpfile} : $!");
1203   binmode IN;
1204
1205   $self->{copies} = 1 unless $self->{media} eq 'printer';
1206
1207   chdir("$self->{cwd}");
1208   for my $i (1 .. $self->{copies}) {
1209     if ($self->{OUT}) {
1210       $self->{OUT} = $command_formatter->($self->{OUT_MODE}, $self->{OUT});
1211
1212       open  OUT, $self->{OUT_MODE}, $self->{OUT} or $self->error($self->cleanup . "$self->{OUT} : $!");
1213       print OUT $_ while <IN>;
1214       close OUT;
1215       seek  IN, 0, 0;
1216
1217     } else {
1218       my %headers = ('-type'       => $mimeType,
1219                      '-connection' => 'close',
1220                      '-charset'    => 'UTF-8');
1221
1222       $self->{attachment_filename} ||= $self->generate_attachment_filename;
1223
1224       if ($self->{attachment_filename}) {
1225         %headers = (
1226           %headers,
1227           '-attachment'     => $self->{attachment_filename},
1228           '-content-length' => $numbytes,
1229           '-charset'        => '',
1230         );
1231       }
1232
1233       print $::request->cgi->header(%headers);
1234
1235       $::locale->with_raw_io(\*STDOUT, sub { print while <IN> });
1236     }
1237   }
1238   close(IN);
1239   $main::lxdebug->leave_sub();
1240 }
1241
1242 sub get_formname_translation {
1243   $main::lxdebug->enter_sub();
1244   my ($self, $formname) = @_;
1245
1246   $formname ||= $self->{formname};
1247
1248   $self->{recipient_locale} ||=  Locale->lang_to_locale($self->{language});
1249   local $::locale = Locale->new($self->{recipient_locale});
1250
1251   my %formname_translations = (
1252     bin_list                => $main::locale->text('Bin List'),
1253     credit_note             => $main::locale->text('Credit Note'),
1254     invoice                 => $main::locale->text('Invoice'),
1255     pick_list               => $main::locale->text('Pick List'),
1256     proforma                => $main::locale->text('Proforma Invoice'),
1257     purchase_order          => $main::locale->text('Purchase Order'),
1258     request_quotation       => $main::locale->text('RFQ'),
1259     sales_order             => $main::locale->text('Confirmation'),
1260     sales_quotation         => $main::locale->text('Quotation'),
1261     storno_invoice          => $main::locale->text('Storno Invoice'),
1262     sales_delivery_order    => $main::locale->text('Delivery Order'),
1263     purchase_delivery_order => $main::locale->text('Delivery Order'),
1264     dunning                 => $main::locale->text('Dunning'),
1265     dunning1                => $main::locale->text('Payment Reminder'),
1266     dunning2                => $main::locale->text('Dunning'),
1267     dunning3                => $main::locale->text('Last Dunning'),
1268     dunning_invoice         => $main::locale->text('Dunning Invoice'),
1269     letter                  => $main::locale->text('Letter'),
1270     ic_supply               => $main::locale->text('Intra-Community supply'),
1271     statement               => $main::locale->text('Statement'),
1272   );
1273
1274   $main::lxdebug->leave_sub();
1275   return $formname_translations{$formname};
1276 }
1277
1278 sub get_number_prefix_for_type {
1279   $main::lxdebug->enter_sub();
1280   my ($self) = @_;
1281
1282   my $prefix =
1283       (first { $self->{type} eq $_ } qw(invoice credit_note)) ? 'inv'
1284     : ($self->{type} =~ /_quotation$/)                        ? 'quo'
1285     : ($self->{type} =~ /_delivery_order$/)                   ? 'do'
1286     : ($self->{type} =~ /letter/)                             ? 'letter'
1287     :                                                           'ord';
1288
1289   # better default like this?
1290   # : ($self->{type} =~ /(sales|purcharse)_order/           :  'ord';
1291   # :                                                           'prefix_undefined';
1292
1293   $main::lxdebug->leave_sub();
1294   return $prefix;
1295 }
1296
1297 sub get_extension_for_format {
1298   $main::lxdebug->enter_sub();
1299   my ($self)    = @_;
1300
1301   my $extension = $self->{format} =~ /pdf/i          ? ".pdf"
1302                 : $self->{format} =~ /postscript/i   ? ".ps"
1303                 : $self->{format} =~ /opendocument/i ? ".odt"
1304                 : $self->{format} =~ /excel/i        ? ".xls"
1305                 : $self->{format} =~ /html/i         ? ".html"
1306                 :                                      "";
1307
1308   $main::lxdebug->leave_sub();
1309   return $extension;
1310 }
1311
1312 sub generate_attachment_filename {
1313   $main::lxdebug->enter_sub();
1314   my ($self) = @_;
1315
1316   $self->{recipient_locale} ||=  Locale->lang_to_locale($self->{language});
1317   my $recipient_locale = Locale->new($self->{recipient_locale});
1318
1319   my $attachment_filename = $main::locale->unquote_special_chars('HTML', $self->get_formname_translation());
1320   my $prefix              = $self->get_number_prefix_for_type();
1321
1322   if ($self->{preview} && (first { $self->{type} eq $_ } qw(invoice credit_note))) {
1323     $attachment_filename .= ' (' . $recipient_locale->text('Preview') . ')' . $self->get_extension_for_format();
1324
1325   } elsif ($attachment_filename && $self->{"${prefix}number"}) {
1326     $attachment_filename .=  "_" . $self->{"${prefix}number"} . $self->get_extension_for_format();
1327
1328   } elsif ($attachment_filename) {
1329     $attachment_filename .=  $self->get_extension_for_format();
1330
1331   } else {
1332     $attachment_filename = "";
1333   }
1334
1335   $attachment_filename =  $main::locale->quote_special_chars('filenames', $attachment_filename);
1336   $attachment_filename =~ s|[\s/\\]+|_|g;
1337
1338   $main::lxdebug->leave_sub();
1339   return $attachment_filename;
1340 }
1341
1342 sub generate_email_subject {
1343   $main::lxdebug->enter_sub();
1344   my ($self) = @_;
1345
1346   my $subject = $main::locale->unquote_special_chars('HTML', $self->get_formname_translation());
1347   my $prefix  = $self->get_number_prefix_for_type();
1348
1349   if ($subject && $self->{"${prefix}number"}) {
1350     $subject .= " " . $self->{"${prefix}number"}
1351   }
1352
1353   $main::lxdebug->leave_sub();
1354   return $subject;
1355 }
1356
1357 sub generate_email_body {
1358   $main::lxdebug->enter_sub();
1359   my ($self, %params) = @_;
1360   # simple german and english will work grammatically (most european languages as well)
1361   # Dear Mr Alan Greenspan:
1362   # Sehr geehrte Frau Meyer,
1363   # A l’attention de Mme Villeroy,
1364   # Gentile Signora Ferrari,
1365   my $body = '';
1366
1367   if ($self->{cp_id} && !$params{record_email}) {
1368     my $givenname = SL::DB::Contact->load_cached($self->{cp_id})->cp_givenname; # for qw(gender givename name);
1369     my $name      = SL::DB::Contact->load_cached($self->{cp_id})->cp_name; # for qw(gender givename name);
1370     my $gender    = SL::DB::Contact->load_cached($self->{cp_id})->cp_gender; # for qw(gender givename name);
1371     my $mf = $gender eq 'f' ? 'female' : 'male';
1372     $body  = GenericTranslations->get(translation_type => "salutation_$mf", language_id => $self->{language_id});
1373     $body .= ' ' . $givenname . ' ' . $name if $body;
1374   } else {
1375     $body  = GenericTranslations->get(translation_type => "salutation_general", language_id => $self->{language_id});
1376   }
1377
1378   return undef unless $body;
1379
1380   $body   .= GenericTranslations->get(translation_type =>"salutation_punctuation_mark", language_id => $self->{language_id}) . "\n";
1381   $body   .= GenericTranslations->get(translation_type =>"preset_text_$self->{formname}", language_id => $self->{language_id});
1382
1383   $body = $main::locale->unquote_special_chars('HTML', $body);
1384
1385   $main::lxdebug->leave_sub();
1386   return $body;
1387 }
1388
1389 sub cleanup {
1390   $main::lxdebug->enter_sub();
1391
1392   my ($self, $application) = @_;
1393
1394   my $error_code = $?;
1395
1396   chdir("$self->{tmpdir}");
1397
1398   my @err = ();
1399   if ((-1 == $error_code) || (127 == (($error_code) >> 8))) {
1400     push @err, $::locale->text('The application "#1" was not found on the system.', $application || 'pdflatex') . ' ' . $::locale->text('Please contact your administrator.');
1401
1402   } elsif (-f "$self->{tmpfile}.err") {
1403     open(FH, "<:encoding(UTF-8)", "$self->{tmpfile}.err");
1404     @err = <FH>;
1405     close(FH);
1406   }
1407
1408   if ($self->{tmpfile} && !($::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files})) {
1409     $self->{tmpfile} =~ s|.*/||g;
1410     # strip extension
1411     $self->{tmpfile} =~ s/\.\w+$//g;
1412     my $tmpfile = $self->{tmpfile};
1413     unlink(<$tmpfile.*>);
1414   }
1415
1416   chdir("$self->{cwd}");
1417
1418   $main::lxdebug->leave_sub();
1419
1420   return "@err";
1421 }
1422
1423 sub datetonum {
1424   $main::lxdebug->enter_sub();
1425
1426   my ($self, $date, $myconfig) = @_;
1427   my ($yy, $mm, $dd);
1428
1429   if ($date && $date =~ /\D/) {
1430
1431     if ($myconfig->{dateformat} =~ /^yy/) {
1432       ($yy, $mm, $dd) = split /\D/, $date;
1433     }
1434     if ($myconfig->{dateformat} =~ /^mm/) {
1435       ($mm, $dd, $yy) = split /\D/, $date;
1436     }
1437     if ($myconfig->{dateformat} =~ /^dd/) {
1438       ($dd, $mm, $yy) = split /\D/, $date;
1439     }
1440
1441     $dd *= 1;
1442     $mm *= 1;
1443     $yy = ($yy < 70) ? $yy + 2000 : $yy;
1444     $yy = ($yy >= 70 && $yy <= 99) ? $yy + 1900 : $yy;
1445
1446     $dd = "0$dd" if ($dd < 10);
1447     $mm = "0$mm" if ($mm < 10);
1448
1449     $date = "$yy$mm$dd";
1450   }
1451
1452   $main::lxdebug->leave_sub();
1453
1454   return $date;
1455 }
1456
1457 # Database routines used throughout
1458 # DB Handling got moved to SL::DB, these are only shims for compatibility
1459
1460 sub dbconnect {
1461   SL::DB->client->dbh;
1462 }
1463
1464 sub get_standard_dbh {
1465   my $dbh = SL::DB->client->dbh;
1466
1467   if ($dbh && !$dbh->{Active}) {
1468     $main::lxdebug->message(LXDebug->INFO(), "get_standard_dbh: \$dbh is defined but not Active anymore");
1469     SL::DB->client->dbh(undef);
1470   }
1471
1472   SL::DB->client->dbh;
1473 }
1474
1475 sub disconnect_standard_dbh {
1476   SL::DB->client->dbh->rollback;
1477 }
1478
1479 # /database
1480
1481 sub date_closed {
1482   $main::lxdebug->enter_sub();
1483
1484   my ($self, $date, $myconfig) = @_;
1485   my $dbh = $self->get_standard_dbh;
1486
1487   my $query = "SELECT 1 FROM defaults WHERE ? < closedto";
1488   my $sth = prepare_execute_query($self, $dbh, $query, conv_date($date));
1489
1490   # Falls $date = '' - Fehlermeldung aus der Datenbank. Ich denke,
1491   # es ist sicher ein conv_date vorher IMMER auszuführen.
1492   # Testfälle ohne definiertes closedto:
1493   #   Leere Datumseingabe i.O.
1494   #     SELECT 1 FROM defaults WHERE '' < closedto
1495   #   normale Zahlungsbuchung über Rechnungsmaske i.O.
1496   #     SELECT 1 FROM defaults WHERE '10.05.2011' < closedto
1497   # Testfälle mit definiertem closedto (30.04.2011):
1498   #  Leere Datumseingabe i.O.
1499   #   SELECT 1 FROM defaults WHERE '' < closedto
1500   # normale Buchung im geschloßenem Zeitraum i.O.
1501   #   SELECT 1 FROM defaults WHERE '21.04.2011' < closedto
1502   #     Fehlermeldung: Es können keine Zahlungen für abgeschlossene Bücher gebucht werden!
1503   # normale Buchung in aktiver Buchungsperiode i.O.
1504   #   SELECT 1 FROM defaults WHERE '01.05.2011' < closedto
1505
1506   my ($closed) = $sth->fetchrow_array;
1507
1508   $main::lxdebug->leave_sub();
1509
1510   return $closed;
1511 }
1512
1513 # prevents bookings to the to far away future
1514 sub date_max_future {
1515   $main::lxdebug->enter_sub();
1516
1517   my ($self, $date, $myconfig) = @_;
1518   my $dbh = $self->get_standard_dbh;
1519
1520   my $query = "SELECT 1 FROM defaults WHERE ? - current_date > max_future_booking_interval";
1521   my $sth = prepare_execute_query($self, $dbh, $query, conv_date($date));
1522
1523   my ($max_future_booking_interval) = $sth->fetchrow_array;
1524
1525   $main::lxdebug->leave_sub();
1526
1527   return $max_future_booking_interval;
1528 }
1529
1530
1531 sub update_balance {
1532   $main::lxdebug->enter_sub();
1533
1534   my ($self, $dbh, $table, $field, $where, $value, @values) = @_;
1535
1536   # if we have a value, go do it
1537   if ($value != 0) {
1538
1539     # retrieve balance from table
1540     my $query = "SELECT $field FROM $table WHERE $where FOR UPDATE";
1541     my $sth = prepare_execute_query($self, $dbh, $query, @values);
1542     my ($balance) = $sth->fetchrow_array;
1543     $sth->finish;
1544
1545     $balance += $value;
1546
1547     # update balance
1548     $query = "UPDATE $table SET $field = $balance WHERE $where";
1549     do_query($self, $dbh, $query, @values);
1550   }
1551   $main::lxdebug->leave_sub();
1552 }
1553
1554 sub update_exchangerate {
1555   $main::lxdebug->enter_sub();
1556
1557   my ($self, $dbh, $curr, $transdate, $buy, $sell) = @_;
1558   my ($query);
1559   # some sanity check for currency
1560   if ($curr eq '') {
1561     $main::lxdebug->leave_sub();
1562     return;
1563   }
1564   $query = qq|SELECT name AS curr FROM currencies WHERE id=(SELECT currency_id FROM defaults)|;
1565
1566   my ($defaultcurrency) = selectrow_query($self, $dbh, $query);
1567
1568   if ($curr eq $defaultcurrency) {
1569     $main::lxdebug->leave_sub();
1570     return;
1571   }
1572
1573   $query = qq|SELECT e.currency_id FROM exchangerate e
1574                  WHERE e.currency_id = (SELECT cu.id FROM currencies cu WHERE cu.name=?) AND e.transdate = ?
1575                  FOR UPDATE|;
1576   my $sth = prepare_execute_query($self, $dbh, $query, $curr, $transdate);
1577
1578   if ($buy == 0) {
1579     $buy = "";
1580   }
1581   if ($sell == 0) {
1582     $sell = "";
1583   }
1584
1585   $buy = conv_i($buy, "NULL");
1586   $sell = conv_i($sell, "NULL");
1587
1588   my $set;
1589   if ($buy != 0 && $sell != 0) {
1590     $set = "buy = $buy, sell = $sell";
1591   } elsif ($buy != 0) {
1592     $set = "buy = $buy";
1593   } elsif ($sell != 0) {
1594     $set = "sell = $sell";
1595   }
1596
1597   if ($sth->fetchrow_array) {
1598     $query = qq|UPDATE exchangerate
1599                 SET $set
1600                 WHERE currency_id = (SELECT id FROM currencies WHERE name = ?)
1601                 AND transdate = ?|;
1602
1603   } else {
1604     $query = qq|INSERT INTO exchangerate (currency_id, buy, sell, transdate)
1605                 VALUES ((SELECT id FROM currencies WHERE name = ?), $buy, $sell, ?)|;
1606   }
1607   $sth->finish;
1608   do_query($self, $dbh, $query, $curr, $transdate);
1609
1610   $main::lxdebug->leave_sub();
1611 }
1612
1613 sub save_exchangerate {
1614   $main::lxdebug->enter_sub();
1615
1616   my ($self, $myconfig, $currency, $transdate, $rate, $fld) = @_;
1617
1618   SL::DB->client->with_transaction(sub {
1619     my $dbh = SL::DB->client->dbh;
1620
1621     my ($buy, $sell);
1622
1623     $buy  = $rate if $fld eq 'buy';
1624     $sell = $rate if $fld eq 'sell';
1625
1626
1627     $self->update_exchangerate($dbh, $currency, $transdate, $buy, $sell);
1628     1;
1629   }) or do { die SL::DB->client->error };
1630
1631   $main::lxdebug->leave_sub();
1632 }
1633
1634 sub get_exchangerate {
1635   $main::lxdebug->enter_sub();
1636
1637   my ($self, $dbh, $curr, $transdate, $fld) = @_;
1638   my ($query);
1639
1640   unless ($transdate && $curr) {
1641     $main::lxdebug->leave_sub();
1642     return 1;
1643   }
1644
1645   $query = qq|SELECT name AS curr FROM currencies WHERE id = (SELECT currency_id FROM defaults)|;
1646
1647   my ($defaultcurrency) = selectrow_query($self, $dbh, $query);
1648
1649   if ($curr eq $defaultcurrency) {
1650     $main::lxdebug->leave_sub();
1651     return 1;
1652   }
1653
1654   $query = qq|SELECT e.$fld FROM exchangerate e
1655                  WHERE e.currency_id = (SELECT id FROM currencies WHERE name = ?) AND e.transdate = ?|;
1656   my ($exchangerate) = selectrow_query($self, $dbh, $query, $curr, $transdate);
1657
1658
1659
1660   $main::lxdebug->leave_sub();
1661
1662   return $exchangerate;
1663 }
1664
1665 sub check_exchangerate {
1666   $main::lxdebug->enter_sub();
1667
1668   my ($self, $myconfig, $currency, $transdate, $fld) = @_;
1669
1670   if ($fld !~/^buy|sell$/) {
1671     $self->error('Fatal: check_exchangerate called with invalid buy/sell argument');
1672   }
1673
1674   unless ($transdate) {
1675     $main::lxdebug->leave_sub();
1676     return "";
1677   }
1678
1679   my ($defaultcurrency) = $self->get_default_currency($myconfig);
1680
1681   if ($currency eq $defaultcurrency) {
1682     $main::lxdebug->leave_sub();
1683     return 1;
1684   }
1685
1686   my $dbh   = $self->get_standard_dbh($myconfig);
1687   my $query = qq|SELECT e.$fld FROM exchangerate e
1688                  WHERE e.currency_id = (SELECT id FROM currencies WHERE name = ?) AND e.transdate = ?|;
1689
1690   my ($exchangerate) = selectrow_query($self, $dbh, $query, $currency, $transdate);
1691
1692   $main::lxdebug->leave_sub();
1693
1694   return $exchangerate;
1695 }
1696
1697 sub get_all_currencies {
1698   $main::lxdebug->enter_sub();
1699
1700   my $self     = shift;
1701   my $myconfig = shift || \%::myconfig;
1702   my $dbh      = $self->get_standard_dbh($myconfig);
1703
1704   my $query = qq|SELECT name FROM currencies|;
1705   my @currencies = map { $_->{name} } selectall_hashref_query($self, $dbh, $query);
1706
1707   $main::lxdebug->leave_sub();
1708
1709   return @currencies;
1710 }
1711
1712 sub get_default_currency {
1713   $main::lxdebug->enter_sub();
1714
1715   my ($self, $myconfig) = @_;
1716   my $dbh      = $self->get_standard_dbh($myconfig);
1717   my $query = qq|SELECT name AS curr FROM currencies WHERE id = (SELECT currency_id FROM defaults)|;
1718
1719   my ($defaultcurrency) = selectrow_query($self, $dbh, $query);
1720
1721   $main::lxdebug->leave_sub();
1722
1723   return $defaultcurrency;
1724 }
1725
1726 sub set_payment_options {
1727   my ($self, $myconfig, $transdate, $type) = @_;
1728
1729   my $terms = $self->{payment_id} ? SL::DB::PaymentTerm->new(id => $self->{payment_id})->load : undef;
1730   return if !$terms;
1731
1732   my $is_invoice                = $type =~ m{invoice}i;
1733
1734   $transdate                  ||= $self->{invdate} || $self->{transdate};
1735   my $due_date                  = $self->{duedate} || $self->{reqdate};
1736
1737   $self->{$_}                   = $terms->$_ for qw(terms_netto terms_skonto percent_skonto);
1738   $self->{payment_description}  = $terms->description;
1739   $self->{netto_date}           = $terms->calc_date(reference_date => $transdate, due_date => $due_date, terms => 'net')->to_kivitendo;
1740   $self->{skonto_date}          = $terms->calc_date(reference_date => $transdate, due_date => $due_date, terms => 'discount')->to_kivitendo;
1741
1742   my ($invtotal, $total);
1743   my (%amounts, %formatted_amounts);
1744
1745   if ($self->{type} =~ /_order$/) {
1746     $amounts{invtotal} = $self->{ordtotal};
1747     $amounts{total}    = $self->{ordtotal};
1748
1749   } elsif ($self->{type} =~ /_quotation$/) {
1750     $amounts{invtotal} = $self->{quototal};
1751     $amounts{total}    = $self->{quototal};
1752
1753   } else {
1754     $amounts{invtotal} = $self->{invtotal};
1755     $amounts{total}    = $self->{total};
1756   }
1757   map { $amounts{$_} = $self->parse_amount($myconfig, $amounts{$_}) } keys %amounts;
1758
1759   $amounts{skonto_in_percent}  = 100.0 * $self->{percent_skonto};
1760   $amounts{skonto_amount}      = $amounts{invtotal} * $self->{percent_skonto};
1761   $amounts{invtotal_wo_skonto} = $amounts{invtotal} * (1 - $self->{percent_skonto});
1762   $amounts{total_wo_skonto}    = $amounts{total}    * (1 - $self->{percent_skonto});
1763
1764   foreach (keys %amounts) {
1765     $amounts{$_}           = $self->round_amount($amounts{$_}, 2);
1766     $formatted_amounts{$_} = $self->format_amount($myconfig, $amounts{$_}, 2);
1767   }
1768
1769   if ($self->{"language_id"}) {
1770     my $language             = SL::DB::Language->new(id => $self->{language_id})->load;
1771
1772     $self->{payment_terms}   = $type =~ m{invoice}i ? $terms->translated_attribute('description_long_invoice', $language->id) : undef;
1773     $self->{payment_terms} ||= $terms->translated_attribute('description_long', $language->id);
1774
1775     if ($language->output_dateformat) {
1776       foreach my $key (qw(netto_date skonto_date)) {
1777         $self->{$key} = $::locale->reformat_date($myconfig, $self->{$key}, $language->output_dateformat, $language->output_longdates);
1778       }
1779     }
1780
1781     if ($language->output_numberformat && ($language->output_numberformat ne $myconfig->{numberformat})) {
1782       local $myconfig->{numberformat};
1783       $myconfig->{"numberformat"} = $language->output_numberformat;
1784       $formatted_amounts{$_} = $self->format_amount($myconfig, $amounts{$_}) for keys %amounts;
1785     }
1786   }
1787
1788   $self->{payment_terms} =  $self->{payment_terms} || ($is_invoice ? $terms->description_long_invoice : undef) || $terms->description_long;
1789
1790   $self->{payment_terms} =~ s/<%netto_date%>/$self->{netto_date}/g;
1791   $self->{payment_terms} =~ s/<%skonto_date%>/$self->{skonto_date}/g;
1792   $self->{payment_terms} =~ s/<%currency%>/$self->{currency}/g;
1793   $self->{payment_terms} =~ s/<%terms_netto%>/$self->{terms_netto}/g;
1794   $self->{payment_terms} =~ s/<%account_number%>/$self->{account_number}/g;
1795   $self->{payment_terms} =~ s/<%bank%>/$self->{bank}/g;
1796   $self->{payment_terms} =~ s/<%bank_code%>/$self->{bank_code}/g;
1797   $self->{payment_terms} =~ s/<\%bic\%>/$self->{bic}/g;
1798   $self->{payment_terms} =~ s/<\%iban\%>/$self->{iban}/g;
1799   $self->{payment_terms} =~ s/<\%mandate_date_of_signature\%>/$self->{mandate_date_of_signature}/g;
1800   $self->{payment_terms} =~ s/<\%mandator_id\%>/$self->{mandator_id}/g;
1801
1802   map { $self->{payment_terms} =~ s/<%${_}%>/$formatted_amounts{$_}/g; } keys %formatted_amounts;
1803
1804   $self->{skonto_in_percent} = $formatted_amounts{skonto_in_percent};
1805
1806 }
1807
1808 sub get_template_language {
1809   $main::lxdebug->enter_sub();
1810
1811   my ($self, $myconfig) = @_;
1812
1813   my $template_code = "";
1814
1815   if ($self->{language_id}) {
1816     my $dbh = $self->get_standard_dbh($myconfig);
1817     my $query = qq|SELECT template_code FROM language WHERE id = ?|;
1818     ($template_code) = selectrow_query($self, $dbh, $query, $self->{language_id});
1819   }
1820
1821   $main::lxdebug->leave_sub();
1822
1823   return $template_code;
1824 }
1825
1826 sub get_printer_code {
1827   $main::lxdebug->enter_sub();
1828
1829   my ($self, $myconfig) = @_;
1830
1831   my $template_code = "";
1832
1833   if ($self->{printer_id}) {
1834     my $dbh = $self->get_standard_dbh($myconfig);
1835     my $query = qq|SELECT template_code, printer_command FROM printers WHERE id = ?|;
1836     ($template_code, $self->{printer_command}) = selectrow_query($self, $dbh, $query, $self->{printer_id});
1837   }
1838
1839   $main::lxdebug->leave_sub();
1840
1841   return $template_code;
1842 }
1843
1844 sub get_shipto {
1845   $main::lxdebug->enter_sub();
1846
1847   my ($self, $myconfig) = @_;
1848
1849   my $template_code = "";
1850
1851   if ($self->{shipto_id}) {
1852     my $dbh = $self->get_standard_dbh($myconfig);
1853     my $query = qq|SELECT * FROM shipto WHERE shipto_id = ?|;
1854     my $ref = selectfirst_hashref_query($self, $dbh, $query, $self->{shipto_id});
1855     map({ $self->{$_} = $ref->{$_} } keys(%$ref));
1856
1857     my $cvars = CVar->get_custom_variables(
1858       dbh      => $dbh,
1859       module   => 'ShipTo',
1860       trans_id => $self->{shipto_id},
1861     );
1862     $self->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
1863   }
1864
1865   $main::lxdebug->leave_sub();
1866 }
1867
1868 sub add_shipto {
1869   my ($self, $dbh, $id, $module) = @_;
1870
1871   my $shipto;
1872   my @values;
1873
1874   foreach my $item (qw(name department_1 department_2 street zipcode city country gln
1875                        contact phone fax email)) {
1876     if ($self->{"shipto$item"}) {
1877       $shipto = 1 if ($self->{$item} ne $self->{"shipto$item"});
1878     }
1879     push(@values, $self->{"shipto${item}"});
1880   }
1881
1882   return if !$shipto;
1883
1884   my $shipto_id = $self->{shipto_id};
1885
1886   if ($self->{shipto_id}) {
1887     my $query = qq|UPDATE shipto set
1888                      shiptoname = ?,
1889                      shiptodepartment_1 = ?,
1890                      shiptodepartment_2 = ?,
1891                      shiptostreet = ?,
1892                      shiptozipcode = ?,
1893                      shiptocity = ?,
1894                      shiptocountry = ?,
1895                      shiptogln = ?,
1896                      shiptocontact = ?,
1897                      shiptocp_gender = ?,
1898                      shiptophone = ?,
1899                      shiptofax = ?,
1900                      shiptoemail = ?
1901                    WHERE shipto_id = ?|;
1902     do_query($self, $dbh, $query, @values, $self->{shipto_id});
1903   } else {
1904     my $query = qq|SELECT * FROM shipto
1905                    WHERE shiptoname = ? AND
1906                      shiptodepartment_1 = ? AND
1907                      shiptodepartment_2 = ? AND
1908                      shiptostreet = ? AND
1909                      shiptozipcode = ? AND
1910                      shiptocity = ? AND
1911                      shiptocountry = ? AND
1912                      shiptogln = ? AND
1913                      shiptocontact = ? AND
1914                      shiptocp_gender = ? AND
1915                      shiptophone = ? AND
1916                      shiptofax = ? AND
1917                      shiptoemail = ? AND
1918                      module = ? AND
1919                      trans_id = ?|;
1920     my $insert_check = selectfirst_hashref_query($self, $dbh, $query, @values, $module, $id);
1921     if(!$insert_check){
1922       my $insert_query =
1923         qq|INSERT INTO shipto (trans_id, shiptoname, shiptodepartment_1, shiptodepartment_2,
1924                                shiptostreet, shiptozipcode, shiptocity, shiptocountry, shiptogln,
1925                                shiptocontact, shiptocp_gender, shiptophone, shiptofax, shiptoemail, module)
1926            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|;
1927       do_query($self, $dbh, $insert_query, $id, @values, $module);
1928
1929       $insert_check = selectfirst_hashref_query($self, $dbh, $query, @values, $module, $id);
1930     }
1931
1932     $shipto_id = $insert_check->{shipto_id};
1933   }
1934
1935   return unless $shipto_id;
1936
1937   CVar->save_custom_variables(
1938     dbh         => $dbh,
1939     module      => 'ShipTo',
1940     trans_id    => $shipto_id,
1941     variables   => $self,
1942     name_prefix => 'shipto',
1943   );
1944 }
1945
1946 sub get_employee {
1947   $main::lxdebug->enter_sub();
1948
1949   my ($self, $dbh) = @_;
1950
1951   $dbh ||= $self->get_standard_dbh(\%main::myconfig);
1952
1953   my $query = qq|SELECT id, name FROM employee WHERE login = ?|;
1954   ($self->{"employee_id"}, $self->{"employee"}) = selectrow_query($self, $dbh, $query, $self->{login});
1955   $self->{"employee_id"} *= 1;
1956
1957   $main::lxdebug->leave_sub();
1958 }
1959
1960 sub get_employee_data {
1961   $main::lxdebug->enter_sub();
1962
1963   my $self     = shift;
1964   my %params   = @_;
1965   my $defaults = SL::DB::Default->get;
1966
1967   Common::check_params(\%params, qw(prefix));
1968   Common::check_params_x(\%params, qw(id));
1969
1970   if (!$params{id}) {
1971     $main::lxdebug->leave_sub();
1972     return;
1973   }
1974
1975   my $myconfig = \%main::myconfig;
1976   my $dbh      = $params{dbh} || $self->get_standard_dbh($myconfig);
1977
1978   my ($login, $deleted)  = selectrow_query($self, $dbh, qq|SELECT login,deleted FROM employee WHERE id = ?|, conv_i($params{id}));
1979
1980   if ($login) {
1981     # login already fetched and still the same client (mandant) | same for both cases (delete|!delete)
1982     $self->{$params{prefix} . '_login'}   = $login;
1983     $self->{$params{prefix} . "_${_}"}    = $defaults->$_ for qw(address businessnumber co_ustid company duns taxnumber);
1984
1985     if (!$deleted) {
1986       # get employee data from auth.user_config
1987       my $user = User->new(login => $login);
1988       $self->{$params{prefix} . "_${_}"} = $user->{$_} for qw(email fax name signature tel);
1989     } else {
1990       # get saved employee data from employee
1991       my $employee = SL::DB::Manager::Employee->find_by(id => conv_i($params{id}));
1992       $self->{$params{prefix} . "_${_}"} = $employee->{"deleted_$_"} for qw(email fax signature tel);
1993       $self->{$params{prefix} . "_name"} = $employee->name;
1994     }
1995  }
1996   $main::lxdebug->leave_sub();
1997 }
1998
1999 sub _get_contacts {
2000   $main::lxdebug->enter_sub();
2001
2002   my ($self, $dbh, $id, $key) = @_;
2003
2004   $key = "all_contacts" unless ($key);
2005
2006   if (!$id) {
2007     $self->{$key} = [];
2008     $main::lxdebug->leave_sub();
2009     return;
2010   }
2011
2012   my $query =
2013     qq|SELECT cp_id, cp_cv_id, cp_name, cp_givenname, cp_abteilung | .
2014     qq|FROM contacts | .
2015     qq|WHERE cp_cv_id = ? | .
2016     qq|ORDER BY lower(cp_name)|;
2017
2018   $self->{$key} = selectall_hashref_query($self, $dbh, $query, $id);
2019
2020   $main::lxdebug->leave_sub();
2021 }
2022
2023 sub _get_projects {
2024   $main::lxdebug->enter_sub();
2025
2026   my ($self, $dbh, $key) = @_;
2027
2028   my ($all, $old_id, $where, @values);
2029
2030   if (ref($key) eq "HASH") {
2031     my $params = $key;
2032
2033     $key = "ALL_PROJECTS";
2034
2035     foreach my $p (keys(%{$params})) {
2036       if ($p eq "all") {
2037         $all = $params->{$p};
2038       } elsif ($p eq "old_id") {
2039         $old_id = $params->{$p};
2040       } elsif ($p eq "key") {
2041         $key = $params->{$p};
2042       }
2043     }
2044   }
2045
2046   if (!$all) {
2047     $where = "WHERE active ";
2048     if ($old_id) {
2049       if (ref($old_id) eq "ARRAY") {
2050         my @ids = grep({ $_ } @{$old_id});
2051         if (@ids) {
2052           $where .= " OR id IN (" . join(",", map({ "?" } @ids)) . ") ";
2053           push(@values, @ids);
2054         }
2055       } else {
2056         $where .= " OR (id = ?) ";
2057         push(@values, $old_id);
2058       }
2059     }
2060   }
2061
2062   my $query =
2063     qq|SELECT id, projectnumber, description, active | .
2064     qq|FROM project | .
2065     $where .
2066     qq|ORDER BY lower(projectnumber)|;
2067
2068   $self->{$key} = selectall_hashref_query($self, $dbh, $query, @values);
2069
2070   $main::lxdebug->leave_sub();
2071 }
2072
2073 sub _get_shipto {
2074   $main::lxdebug->enter_sub();
2075
2076   my ($self, $dbh, $vc_id, $key) = @_;
2077
2078   $key = "all_shipto" unless ($key);
2079
2080   if ($vc_id) {
2081     # get shipping addresses
2082     my $query = qq|SELECT * FROM shipto WHERE trans_id = ?|;
2083
2084     $self->{$key} = selectall_hashref_query($self, $dbh, $query, $vc_id);
2085
2086   } else {
2087     $self->{$key} = [];
2088   }
2089
2090   $main::lxdebug->leave_sub();
2091 }
2092
2093 sub _get_printers {
2094   $main::lxdebug->enter_sub();
2095
2096   my ($self, $dbh, $key) = @_;
2097
2098   $key = "all_printers" unless ($key);
2099
2100   my $query = qq|SELECT id, printer_description, printer_command, template_code FROM printers|;
2101
2102   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2103
2104   $main::lxdebug->leave_sub();
2105 }
2106
2107 sub _get_charts {
2108   $main::lxdebug->enter_sub();
2109
2110   my ($self, $dbh, $params) = @_;
2111   my ($key);
2112
2113   $key = $params->{key};
2114   $key = "all_charts" unless ($key);
2115
2116   my $transdate = quote_db_date($params->{transdate});
2117
2118   my $query =
2119     qq|SELECT c.id, c.accno, c.description, c.link, c.charttype, tk.taxkey_id, tk.tax_id | .
2120     qq|FROM chart c | .
2121     qq|LEFT JOIN taxkeys tk ON | .
2122     qq|(tk.id = (SELECT id FROM taxkeys | .
2123     qq|          WHERE taxkeys.chart_id = c.id AND startdate <= $transdate | .
2124     qq|          ORDER BY startdate DESC LIMIT 1)) | .
2125     qq|ORDER BY c.accno|;
2126
2127   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2128
2129   $main::lxdebug->leave_sub();
2130 }
2131
2132 sub _get_taxcharts {
2133   $main::lxdebug->enter_sub();
2134
2135   my ($self, $dbh, $params) = @_;
2136
2137   my $key = "all_taxcharts";
2138   my @where;
2139
2140   if (ref $params eq 'HASH') {
2141     $key = $params->{key} if ($params->{key});
2142     if ($params->{module} eq 'AR') {
2143       push @where, 'chart_categories ~ \'[ACILQ]\'';
2144
2145     } elsif ($params->{module} eq 'AP') {
2146       push @where, 'chart_categories ~ \'[ACELQ]\'';
2147     }
2148
2149   } elsif ($params) {
2150     $key = $params;
2151   }
2152
2153   my $where = @where ? ' WHERE ' . join(' AND ', map { "($_)" } @where) : '';
2154
2155   my $query = qq|SELECT * FROM tax $where ORDER BY taxkey, rate|;
2156
2157   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2158
2159   $main::lxdebug->leave_sub();
2160 }
2161
2162 sub _get_taxzones {
2163   $main::lxdebug->enter_sub();
2164
2165   my ($self, $dbh, $key) = @_;
2166
2167   $key = "all_taxzones" unless ($key);
2168   my $tzfilter = "";
2169   $tzfilter = "WHERE obsolete is FALSE" if $key eq 'ALL_ACTIVE_TAXZONES';
2170
2171   my $query = qq|SELECT * FROM tax_zones $tzfilter ORDER BY sortkey|;
2172
2173   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2174
2175   $main::lxdebug->leave_sub();
2176 }
2177
2178 sub _get_employees {
2179   $main::lxdebug->enter_sub();
2180
2181   my ($self, $dbh, $params) = @_;
2182
2183   my $deleted = 0;
2184
2185   my $key;
2186   if (ref $params eq 'HASH') {
2187     $key     = $params->{key};
2188     $deleted = $params->{deleted};
2189
2190   } else {
2191     $key = $params;
2192   }
2193
2194   $key     ||= "all_employees";
2195   my $filter = $deleted ? '' : 'WHERE NOT COALESCE(deleted, FALSE)';
2196   $self->{$key} = selectall_hashref_query($self, $dbh, qq|SELECT * FROM employee $filter ORDER BY lower(name)|);
2197
2198   $main::lxdebug->leave_sub();
2199 }
2200
2201 sub _get_business_types {
2202   $main::lxdebug->enter_sub();
2203
2204   my ($self, $dbh, $key) = @_;
2205
2206   my $options       = ref $key eq 'HASH' ? $key : { key => $key };
2207   $options->{key} ||= "all_business_types";
2208   my $where         = '';
2209
2210   if (exists $options->{salesman}) {
2211     $where = 'WHERE ' . ($options->{salesman} ? '' : 'NOT ') . 'COALESCE(salesman)';
2212   }
2213
2214   $self->{ $options->{key} } = selectall_hashref_query($self, $dbh, qq|SELECT * FROM business $where ORDER BY lower(description)|);
2215
2216   $main::lxdebug->leave_sub();
2217 }
2218
2219 sub _get_languages {
2220   $main::lxdebug->enter_sub();
2221
2222   my ($self, $dbh, $key) = @_;
2223
2224   $key = "all_languages" unless ($key);
2225
2226   my $query = qq|SELECT * FROM language ORDER BY id|;
2227
2228   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2229
2230   $main::lxdebug->leave_sub();
2231 }
2232
2233 sub _get_dunning_configs {
2234   $main::lxdebug->enter_sub();
2235
2236   my ($self, $dbh, $key) = @_;
2237
2238   $key = "all_dunning_configs" unless ($key);
2239
2240   my $query = qq|SELECT * FROM dunning_config ORDER BY dunning_level|;
2241
2242   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2243
2244   $main::lxdebug->leave_sub();
2245 }
2246
2247 sub _get_currencies {
2248 $main::lxdebug->enter_sub();
2249
2250   my ($self, $dbh, $key) = @_;
2251
2252   $key = "all_currencies" unless ($key);
2253
2254   $self->{$key} = [$self->get_all_currencies()];
2255
2256   $main::lxdebug->leave_sub();
2257 }
2258
2259 sub _get_payments {
2260 $main::lxdebug->enter_sub();
2261
2262   my ($self, $dbh, $key) = @_;
2263
2264   $key = "all_payments" unless ($key);
2265
2266   my $query = qq|SELECT * FROM payment_terms ORDER BY sortkey|;
2267
2268   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2269
2270   $main::lxdebug->leave_sub();
2271 }
2272
2273 sub _get_customers {
2274   $main::lxdebug->enter_sub();
2275
2276   my ($self, $dbh, $key) = @_;
2277
2278   my $options        = ref $key eq 'HASH' ? $key : { key => $key };
2279   $options->{key}  ||= "all_customers";
2280   my $limit_clause   = $options->{limit} ? "LIMIT $options->{limit}" : '';
2281
2282   my @where;
2283   push @where, qq|business_id IN (SELECT id FROM business WHERE salesman)| if  $options->{business_is_salesman};
2284   push @where, qq|NOT obsolete|                                            if !$options->{with_obsolete};
2285   my $where_str = @where ? "WHERE " . join(" AND ", map { "($_)" } @where) : '';
2286
2287   my $query = qq|SELECT * FROM customer $where_str ORDER BY name $limit_clause|;
2288   $self->{ $options->{key} } = selectall_hashref_query($self, $dbh, $query);
2289
2290   $main::lxdebug->leave_sub();
2291 }
2292
2293 sub _get_vendors {
2294   $main::lxdebug->enter_sub();
2295
2296   my ($self, $dbh, $key) = @_;
2297
2298   $key = "all_vendors" unless ($key);
2299
2300   my $query = qq|SELECT * FROM vendor WHERE NOT obsolete ORDER BY name|;
2301
2302   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2303
2304   $main::lxdebug->leave_sub();
2305 }
2306
2307 sub _get_departments {
2308   $main::lxdebug->enter_sub();
2309
2310   my ($self, $dbh, $key) = @_;
2311
2312   $key = "all_departments" unless ($key);
2313
2314   my $query = qq|SELECT * FROM department ORDER BY description|;
2315
2316   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2317
2318   $main::lxdebug->leave_sub();
2319 }
2320
2321 sub _get_warehouses {
2322   $main::lxdebug->enter_sub();
2323
2324   my ($self, $dbh, $param) = @_;
2325
2326   my ($key, $bins_key);
2327
2328   if ('' eq ref $param) {
2329     $key = $param;
2330
2331   } else {
2332     $key      = $param->{key};
2333     $bins_key = $param->{bins};
2334   }
2335
2336   my $query = qq|SELECT w.* FROM warehouse w
2337                  WHERE (NOT w.invalid) AND
2338                    ((SELECT COUNT(b.*) FROM bin b WHERE b.warehouse_id = w.id) > 0)
2339                  ORDER BY w.sortkey|;
2340
2341   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2342
2343   if ($bins_key) {
2344     $query = qq|SELECT id, description FROM bin WHERE warehouse_id = ?
2345                 ORDER BY description|;
2346     my $sth = prepare_query($self, $dbh, $query);
2347
2348     foreach my $warehouse (@{ $self->{$key} }) {
2349       do_statement($self, $sth, $query, $warehouse->{id});
2350       $warehouse->{$bins_key} = [];
2351
2352       while (my $ref = $sth->fetchrow_hashref()) {
2353         push @{ $warehouse->{$bins_key} }, $ref;
2354       }
2355     }
2356     $sth->finish();
2357   }
2358
2359   $main::lxdebug->leave_sub();
2360 }
2361
2362 sub _get_simple {
2363   $main::lxdebug->enter_sub();
2364
2365   my ($self, $dbh, $table, $key, $sortkey) = @_;
2366
2367   my $query  = qq|SELECT * FROM $table|;
2368   $query    .= qq| ORDER BY $sortkey| if ($sortkey);
2369
2370   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2371
2372   $main::lxdebug->leave_sub();
2373 }
2374
2375 #sub _get_groups {
2376 #  $main::lxdebug->enter_sub();
2377 #
2378 #  my ($self, $dbh, $key) = @_;
2379 #
2380 #  $key ||= "all_groups";
2381 #
2382 #  my $groups = $main::auth->read_groups();
2383 #
2384 #  $self->{$key} = selectall_hashref_query($self, $dbh, $query);
2385 #
2386 #  $main::lxdebug->leave_sub();
2387 #}
2388
2389 sub get_lists {
2390   $main::lxdebug->enter_sub();
2391
2392   my $self = shift;
2393   my %params = @_;
2394
2395   my $dbh = $self->get_standard_dbh(\%main::myconfig);
2396   my ($sth, $query, $ref);
2397
2398   my ($vc, $vc_id);
2399   if ($params{contacts} || $params{shipto}) {
2400     $vc = 'customer' if $self->{"vc"} eq "customer";
2401     $vc = 'vendor'   if $self->{"vc"} eq "vendor";
2402     die "invalid use of get_lists, need 'vc'" unless $vc;
2403     $vc_id = $self->{"${vc}_id"};
2404   }
2405
2406   if ($params{"contacts"}) {
2407     $self->_get_contacts($dbh, $vc_id, $params{"contacts"});
2408   }
2409
2410   if ($params{"shipto"}) {
2411     $self->_get_shipto($dbh, $vc_id, $params{"shipto"});
2412   }
2413
2414   if ($params{"projects"} || $params{"all_projects"}) {
2415     $self->_get_projects($dbh, $params{"all_projects"} ?
2416                          $params{"all_projects"} : $params{"projects"},
2417                          $params{"all_projects"} ? 1 : 0);
2418   }
2419
2420   if ($params{"printers"}) {
2421     $self->_get_printers($dbh, $params{"printers"});
2422   }
2423
2424   if ($params{"languages"}) {
2425     $self->_get_languages($dbh, $params{"languages"});
2426   }
2427
2428   if ($params{"charts"}) {
2429     $self->_get_charts($dbh, $params{"charts"});
2430   }
2431
2432   if ($params{"taxcharts"}) {
2433     $self->_get_taxcharts($dbh, $params{"taxcharts"});
2434   }
2435
2436   if ($params{"taxzones"}) {
2437     $self->_get_taxzones($dbh, $params{"taxzones"});
2438   }
2439
2440   if ($params{"employees"}) {
2441     $self->_get_employees($dbh, $params{"employees"});
2442   }
2443
2444   if ($params{"salesmen"}) {
2445     $self->_get_employees($dbh, $params{"salesmen"});
2446   }
2447
2448   if ($params{"business_types"}) {
2449     $self->_get_business_types($dbh, $params{"business_types"});
2450   }
2451
2452   if ($params{"dunning_configs"}) {
2453     $self->_get_dunning_configs($dbh, $params{"dunning_configs"});
2454   }
2455
2456   if($params{"currencies"}) {
2457     $self->_get_currencies($dbh, $params{"currencies"});
2458   }
2459
2460   if($params{"customers"}) {
2461     $self->_get_customers($dbh, $params{"customers"});
2462   }
2463
2464   if($params{"vendors"}) {
2465     if (ref $params{"vendors"} eq 'HASH') {
2466       $self->_get_vendors($dbh, $params{"vendors"}{key}, $params{"vendors"}{limit});
2467     } else {
2468       $self->_get_vendors($dbh, $params{"vendors"});
2469     }
2470   }
2471
2472   if($params{"payments"}) {
2473     $self->_get_payments($dbh, $params{"payments"});
2474   }
2475
2476   if($params{"departments"}) {
2477     $self->_get_departments($dbh, $params{"departments"});
2478   }
2479
2480   if ($params{price_factors}) {
2481     $self->_get_simple($dbh, 'price_factors', $params{price_factors}, 'sortkey');
2482   }
2483
2484   if ($params{warehouses}) {
2485     $self->_get_warehouses($dbh, $params{warehouses});
2486   }
2487
2488 #  if ($params{groups}) {
2489 #    $self->_get_groups($dbh, $params{groups});
2490 #  }
2491
2492   if ($params{partsgroup}) {
2493     $self->get_partsgroup(\%main::myconfig, { all => 1, target => $params{partsgroup} });
2494   }
2495
2496   $main::lxdebug->leave_sub();
2497 }
2498
2499 # this sub gets the id and name from $table
2500 sub get_name {
2501   $main::lxdebug->enter_sub();
2502
2503   my ($self, $myconfig, $table) = @_;
2504
2505   # connect to database
2506   my $dbh = $self->get_standard_dbh($myconfig);
2507
2508   $table = $table eq "customer" ? "customer" : "vendor";
2509   my $arap = $self->{arap} eq "ar" ? "ar" : "ap";
2510
2511   my ($query, @values);
2512
2513   if (!$self->{openinvoices}) {
2514     my $where;
2515     if ($self->{customernumber} ne "") {
2516       $where = qq|(vc.customernumber ILIKE ?)|;
2517       push(@values, like($self->{customernumber}));
2518     } else {
2519       $where = qq|(vc.name ILIKE ?)|;
2520       push(@values, like($self->{$table}));
2521     }
2522
2523     $query =
2524       qq~SELECT vc.id, vc.name,
2525            vc.street || ' ' || vc.zipcode || ' ' || vc.city || ' ' || vc.country AS address
2526          FROM $table vc
2527          WHERE $where AND (NOT vc.obsolete)
2528          ORDER BY vc.name~;
2529   } else {
2530     $query =
2531       qq~SELECT DISTINCT vc.id, vc.name,
2532            vc.street || ' ' || vc.zipcode || ' ' || vc.city || ' ' || vc.country AS address
2533          FROM $arap a
2534          JOIN $table vc ON (a.${table}_id = vc.id)
2535          WHERE NOT (a.amount = a.paid) AND (vc.name ILIKE ?)
2536          ORDER BY vc.name~;
2537     push(@values, like($self->{$table}));
2538   }
2539
2540   $self->{name_list} = selectall_hashref_query($self, $dbh, $query, @values);
2541
2542   $main::lxdebug->leave_sub();
2543
2544   return scalar(@{ $self->{name_list} });
2545 }
2546
2547 sub new_lastmtime {
2548
2549   my ($self, $table, $provided_dbh) = @_;
2550
2551   my $dbh = $provided_dbh ? $provided_dbh : $self->get_standard_dbh;
2552   return                                       unless $self->{id};
2553   croak ("wrong call, no valid table defined") unless $table =~ /^(oe|ar|ap|delivery_orders|parts)$/;
2554
2555   my $query       = "SELECT mtime, itime FROM " . $table . " WHERE id = ?";
2556   my $ref         = selectfirst_hashref_query($self, $dbh, $query, $self->{id});
2557   $ref->{mtime} ||= $ref->{itime};
2558   $self->{lastmtime} = $ref->{mtime};
2559
2560 }
2561
2562 sub mtime_ischanged {
2563   my ($self, $table, $option) = @_;
2564
2565   return                                       unless $self->{id};
2566   croak ("wrong call, no valid table defined") unless $table =~ /^(oe|ar|ap|delivery_orders|parts)$/;
2567
2568   my $query       = "SELECT mtime, itime FROM " . $table . " WHERE id = ?";
2569   my $ref         = selectfirst_hashref_query($self, $self->get_standard_dbh, $query, $self->{id});
2570   $ref->{mtime} ||= $ref->{itime};
2571
2572   if ($self->{lastmtime} && $self->{lastmtime} ne $ref->{mtime} ) {
2573       $self->error(($option eq 'mail') ?
2574         t8("The document has been changed by another user. No mail was sent. Please reopen it in another window and copy the changes to the new window") :
2575         t8("The document has been changed by another user. Please reopen it in another window and copy the changes to the new window")
2576       );
2577     $::dispatcher->end_request;
2578   }
2579 }
2580
2581 # language_payment duplicates some of the functionality of all_vc (language,
2582 # printer, payment_terms), and at least in the case of sales invoices both
2583 # all_vc and language_payment are called when adding new invoices
2584 sub language_payment {
2585   $main::lxdebug->enter_sub();
2586
2587   my ($self, $myconfig) = @_;
2588
2589   my $dbh = $self->get_standard_dbh($myconfig);
2590   # get languages
2591   my $query = qq|SELECT id, description
2592                  FROM language
2593                  ORDER BY id|;
2594
2595   $self->{languages} = selectall_hashref_query($self, $dbh, $query);
2596
2597   # get printer
2598   $query = qq|SELECT printer_description, id
2599               FROM printers
2600               ORDER BY printer_description|;
2601
2602   $self->{printers} = selectall_hashref_query($self, $dbh, $query);
2603
2604   # get payment terms
2605   $query = qq|SELECT id, description
2606               FROM payment_terms
2607               WHERE ( obsolete IS FALSE OR id = ? )
2608               ORDER BY sortkey |;
2609   $self->{payment_terms} = selectall_hashref_query($self, $dbh, $query, $self->{payment_id} || undef);
2610
2611   # get buchungsgruppen
2612   $query = qq|SELECT id, description
2613               FROM buchungsgruppen|;
2614
2615   $self->{BUCHUNGSGRUPPEN} = selectall_hashref_query($self, $dbh, $query);
2616
2617   $main::lxdebug->leave_sub();
2618 }
2619
2620 # this is only used for reports
2621 sub all_departments {
2622   $main::lxdebug->enter_sub();
2623
2624   my ($self, $myconfig, $table) = @_;
2625
2626   my $dbh = $self->get_standard_dbh($myconfig);
2627
2628   my $query = qq|SELECT id, description
2629                  FROM department
2630                  ORDER BY description|;
2631   $self->{all_departments} = selectall_hashref_query($self, $dbh, $query);
2632
2633   delete($self->{all_departments}) unless (@{ $self->{all_departments} || [] });
2634
2635   $main::lxdebug->leave_sub();
2636 }
2637
2638 sub create_links {
2639   $main::lxdebug->enter_sub();
2640
2641   my ($self, $module, $myconfig, $table, $provided_dbh) = @_;
2642
2643   my ($fld, $arap);
2644   if ($table eq "customer") {
2645     $fld = "buy";
2646     $arap = "ar";
2647   } else {
2648     $table = "vendor";
2649     $fld = "sell";
2650     $arap = "ap";
2651   }
2652
2653   # get last customers or vendors
2654   my ($query, $sth, $ref);
2655
2656   my $dbh = $provided_dbh ? $provided_dbh : $self->get_standard_dbh($myconfig);
2657   my %xkeyref = ();
2658
2659   if (!$self->{id}) {
2660
2661     my $transdate = "current_date";
2662     if ($self->{transdate}) {
2663       $transdate = $dbh->quote($self->{transdate});
2664     }
2665
2666     # now get the account numbers
2667     $query = qq|
2668       SELECT c.accno, c.description, c.link, c.taxkey_id, c.id AS chart_id, tk2.tax_id
2669         FROM chart c
2670         -- find newest entries in taxkeys
2671         INNER JOIN (
2672           SELECT chart_id, MAX(startdate) AS startdate
2673           FROM taxkeys
2674           WHERE (startdate <= $transdate)
2675           GROUP BY chart_id
2676         ) tk ON (c.id = tk.chart_id)
2677         -- and load all of those entries
2678         INNER JOIN taxkeys tk2
2679            ON (tk.chart_id = tk2.chart_id AND tk.startdate = tk2.startdate)
2680        WHERE (c.link LIKE ?)
2681       ORDER BY c.accno|;
2682
2683     $sth = $dbh->prepare($query);
2684
2685     do_statement($self, $sth, $query, like($module));
2686
2687     $self->{accounts} = "";
2688     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
2689
2690       foreach my $key (split(/:/, $ref->{link})) {
2691         if ($key =~ /\Q$module\E/) {
2692
2693           # cross reference for keys
2694           $xkeyref{ $ref->{accno} } = $key;
2695
2696           push @{ $self->{"${module}_links"}{$key} },
2697             { accno       => $ref->{accno},
2698               chart_id    => $ref->{chart_id},
2699               description => $ref->{description},
2700               taxkey      => $ref->{taxkey_id},
2701               tax_id      => $ref->{tax_id} };
2702
2703           $self->{accounts} .= "$ref->{accno} " unless $key =~ /tax/;
2704         }
2705       }
2706     }
2707   }
2708
2709   # get taxkeys and description
2710   $query = qq|SELECT id, taxkey, taxdescription FROM tax|;
2711   $self->{TAXKEY} = selectall_hashref_query($self, $dbh, $query);
2712
2713   if (($module eq "AP") || ($module eq "AR")) {
2714     # get tax rates and description
2715     $query = qq|SELECT * FROM tax|;
2716     $self->{TAX} = selectall_hashref_query($self, $dbh, $query);
2717   }
2718
2719   my $extra_columns = '';
2720   $extra_columns   .= 'a.direct_debit, ' if ($module eq 'AR') || ($module eq 'AP');
2721
2722   if ($self->{id}) {
2723     $query =
2724       qq|SELECT
2725            a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid,
2726            a.duedate, a.ordnumber, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.notes,
2727            a.mtime, a.itime,
2728            a.intnotes, a.department_id, a.amount AS oldinvtotal,
2729            a.paid AS oldtotalpaid, a.employee_id, a.gldate, a.type,
2730            a.globalproject_id, ${extra_columns}
2731            c.name AS $table,
2732            d.description AS department,
2733            e.name AS employee
2734          FROM $arap a
2735          JOIN $table c ON (a.${table}_id = c.id)
2736          LEFT JOIN employee e ON (e.id = a.employee_id)
2737          LEFT JOIN department d ON (d.id = a.department_id)
2738          WHERE a.id = ?|;
2739     $ref = selectfirst_hashref_query($self, $dbh, $query, $self->{id});
2740
2741     foreach my $key (keys %$ref) {
2742       $self->{$key} = $ref->{$key};
2743     }
2744     $self->{mtime}   ||= $self->{itime};
2745     $self->{lastmtime} = $self->{mtime};
2746     my $transdate = "current_date";
2747     if ($self->{transdate}) {
2748       $transdate = $dbh->quote($self->{transdate});
2749     }
2750
2751     # now get the account numbers
2752     $query = qq|SELECT c.accno, c.description, c.link, c.taxkey_id, c.id AS chart_id, tk.tax_id
2753                 FROM chart c
2754                 LEFT JOIN taxkeys tk ON (tk.chart_id = c.id)
2755                 WHERE c.link LIKE ?
2756                   AND (tk.id = (SELECT id FROM taxkeys WHERE taxkeys.chart_id = c.id AND startdate <= $transdate ORDER BY startdate DESC LIMIT 1)
2757                     OR c.link LIKE '%_tax%' OR c.taxkey_id IS NULL)
2758                 ORDER BY c.accno|;
2759
2760     $sth = $dbh->prepare($query);
2761     do_statement($self, $sth, $query, like($module));
2762
2763     $self->{accounts} = "";
2764     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
2765
2766       foreach my $key (split(/:/, $ref->{link})) {
2767         if ($key =~ /\Q$module\E/) {
2768
2769           # cross reference for keys
2770           $xkeyref{ $ref->{accno} } = $key;
2771
2772           push @{ $self->{"${module}_links"}{$key} },
2773             { accno       => $ref->{accno},
2774               chart_id    => $ref->{chart_id},
2775               description => $ref->{description},
2776               taxkey      => $ref->{taxkey_id},
2777               tax_id      => $ref->{tax_id} };
2778
2779           $self->{accounts} .= "$ref->{accno} " unless $key =~ /tax/;
2780         }
2781       }
2782     }
2783
2784
2785     # get amounts from individual entries
2786     $query =
2787       qq|SELECT
2788            c.accno, c.description,
2789            a.acc_trans_id, a.source, a.amount, a.memo, a.transdate, a.gldate, a.cleared, a.project_id, a.taxkey, a.chart_id,
2790            p.projectnumber,
2791            t.rate, t.id
2792          FROM acc_trans a
2793          LEFT JOIN chart c ON (c.id = a.chart_id)
2794          LEFT JOIN project p ON (p.id = a.project_id)
2795          LEFT JOIN tax t ON (t.id= a.tax_id)
2796          WHERE a.trans_id = ?
2797          AND a.fx_transaction = '0'
2798          ORDER BY a.acc_trans_id, a.transdate|;
2799     $sth = $dbh->prepare($query);
2800     do_statement($self, $sth, $query, $self->{id});
2801
2802     # get exchangerate for currency
2803     $self->{exchangerate} =
2804       $self->get_exchangerate($dbh, $self->{currency}, $self->{transdate}, $fld);
2805     my $index = 0;
2806
2807     # store amounts in {acc_trans}{$key} for multiple accounts
2808     while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
2809       $ref->{exchangerate} =
2810         $self->get_exchangerate($dbh, $self->{currency}, $ref->{transdate}, $fld);
2811       if (!($xkeyref{ $ref->{accno} } =~ /tax/)) {
2812         $index++;
2813       }
2814       if (($xkeyref{ $ref->{accno} } =~ /paid/) && ($self->{type} eq "credit_note")) {
2815         $ref->{amount} *= -1;
2816       }
2817       $ref->{index} = $index;
2818
2819       push @{ $self->{acc_trans}{ $xkeyref{ $ref->{accno} } } }, $ref;
2820     }
2821
2822     $sth->finish;
2823     #check das:
2824     $query =
2825       qq|SELECT
2826            d.closedto, d.revtrans,
2827            (SELECT cu.name FROM currencies cu WHERE cu.id=d.currency_id) AS defaultcurrency,
2828            (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id) AS fxgain_accno,
2829            (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno,
2830            (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id = c.id) AS rndgain_accno,
2831            (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id = c.id) AS rndloss_accno
2832          FROM defaults d|;
2833     $ref = selectfirst_hashref_query($self, $dbh, $query);
2834     map { $self->{$_} = $ref->{$_} } keys %$ref;
2835
2836   } else {
2837
2838     # get date
2839     $query =
2840        qq|SELECT
2841             current_date AS transdate, d.closedto, d.revtrans,
2842             (SELECT cu.name FROM currencies cu WHERE cu.id=d.currency_id) AS defaultcurrency,
2843             (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id = c.id) AS fxgain_accno,
2844             (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id = c.id) AS fxloss_accno,
2845             (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id = c.id) AS rndgain_accno,
2846             (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id = c.id) AS rndloss_accno
2847           FROM defaults d|;
2848     $ref = selectfirst_hashref_query($self, $dbh, $query);
2849     map { $self->{$_} = $ref->{$_} } keys %$ref;
2850
2851     if ($self->{"$self->{vc}_id"}) {
2852
2853       # only setup currency
2854       ($self->{currency}) = $self->{defaultcurrency} if !$self->{currency};
2855
2856     } else {
2857
2858       $self->lastname_used($dbh, $myconfig, $table, $module);
2859
2860       # get exchangerate for currency
2861       $self->{exchangerate} =
2862         $self->get_exchangerate($dbh, $self->{currency}, $self->{transdate}, $fld);
2863
2864     }
2865
2866   }
2867
2868   $main::lxdebug->leave_sub();
2869 }
2870
2871 sub lastname_used {
2872   $main::lxdebug->enter_sub();
2873
2874   my ($self, $dbh, $myconfig, $table, $module) = @_;
2875
2876   my ($arap, $where);
2877
2878   $table         = $table eq "customer" ? "customer" : "vendor";
2879   my %column_map = ("a.${table}_id"           => "${table}_id",
2880                     "a.department_id"         => "department_id",
2881                     "d.description"           => "department",
2882                     "ct.name"                 => $table,
2883                     "cu.name"                 => "currency",
2884     );
2885
2886   if ($self->{type} =~ /delivery_order/) {
2887     $arap  = 'delivery_orders';
2888     delete $column_map{"cu.currency"};
2889
2890   } elsif ($self->{type} =~ /_order/) {
2891     $arap  = 'oe';
2892     $where = "quotation = '0'";
2893
2894   } elsif ($self->{type} =~ /_quotation/) {
2895     $arap  = 'oe';
2896     $where = "quotation = '1'";
2897
2898   } elsif ($table eq 'customer') {
2899     $arap  = 'ar';
2900
2901   } else {
2902     $arap  = 'ap';
2903
2904   }
2905
2906   $where           = "($where) AND" if ($where);
2907   my $query        = qq|SELECT MAX(id) FROM $arap
2908                         WHERE $where ${table}_id > 0|;
2909   my ($trans_id)   = selectrow_query($self, $dbh, $query);
2910   $trans_id       *= 1;
2911
2912   my $column_spec  = join(', ', map { "${_} AS $column_map{$_}" } keys %column_map);
2913   $query           = qq|SELECT $column_spec
2914                         FROM $arap a
2915                         LEFT JOIN $table     ct ON (a.${table}_id = ct.id)
2916                         LEFT JOIN department d  ON (a.department_id = d.id)
2917                         LEFT JOIN currencies cu ON (cu.id=ct.currency_id)
2918                         WHERE a.id = ?|;
2919   my $ref          = selectfirst_hashref_query($self, $dbh, $query, $trans_id);
2920
2921   map { $self->{$_} = $ref->{$_} } values %column_map;
2922
2923   $main::lxdebug->leave_sub();
2924 }
2925
2926 sub get_variable_content_types {
2927   my %html_variables  = (
2928       longdescription => 'html',
2929       partnotes       => 'html',
2930       notes           => 'html',
2931       orignotes       => 'html',
2932       notes1          => 'html',
2933       notes2          => 'html',
2934       notes3          => 'html',
2935       notes4          => 'html',
2936       header_text     => 'html',
2937       footer_text     => 'html',
2938   );
2939   return \%html_variables;
2940 }
2941
2942 sub current_date {
2943   $main::lxdebug->enter_sub();
2944
2945   my $self     = shift;
2946   my $myconfig = shift || \%::myconfig;
2947   my ($thisdate, $days) = @_;
2948
2949   my $dbh = $self->get_standard_dbh($myconfig);
2950   my $query;
2951
2952   $days *= 1;
2953   if ($thisdate) {
2954     my $dateformat = $myconfig->{dateformat};
2955     $dateformat .= "yy" if $myconfig->{dateformat} !~ /^y/;
2956     $thisdate = $dbh->quote($thisdate);
2957     $query = qq|SELECT to_date($thisdate, '$dateformat') + $days AS thisdate|;
2958   } else {
2959     $query = qq|SELECT current_date AS thisdate|;
2960   }
2961
2962   ($thisdate) = selectrow_query($self, $dbh, $query);
2963
2964   $main::lxdebug->leave_sub();
2965
2966   return $thisdate;
2967 }
2968
2969 sub redo_rows {
2970   $main::lxdebug->enter_sub();
2971
2972   my ($self, $flds, $new, $count, $numrows) = @_;
2973
2974   my @ndx = ();
2975
2976   map { push @ndx, { num => $new->[$_ - 1]->{runningnumber}, ndx => $_ } } 1 .. $count;
2977
2978   my $i = 0;
2979
2980   # fill rows
2981   foreach my $item (sort { $a->{num} <=> $b->{num} } @ndx) {
2982     $i++;
2983     my $j = $item->{ndx} - 1;
2984     map { $self->{"${_}_$i"} = $new->[$j]->{$_} } @{$flds};
2985   }
2986
2987   # delete empty rows
2988   for $i ($count + 1 .. $numrows) {
2989     map { delete $self->{"${_}_$i"} } @{$flds};
2990   }
2991
2992   $main::lxdebug->leave_sub();
2993 }
2994
2995 sub update_status {
2996   $main::lxdebug->enter_sub();
2997
2998   my ($self, $myconfig) = @_;
2999
3000   my ($i, $id);
3001
3002   SL::DB->client->with_transaction(sub {
3003     my $dbh = SL::DB->client->dbh;
3004
3005     my $query = qq|DELETE FROM status
3006                    WHERE (formname = ?) AND (trans_id = ?)|;
3007     my $sth = prepare_query($self, $dbh, $query);
3008
3009     if ($self->{formname} =~ /(check|receipt)/) {
3010       for $i (1 .. $self->{rowcount}) {
3011         do_statement($self, $sth, $query, $self->{formname}, $self->{"id_$i"} * 1);
3012       }
3013     } else {
3014       do_statement($self, $sth, $query, $self->{formname}, $self->{id});
3015     }
3016     $sth->finish();
3017
3018     my $printed = ($self->{printed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
3019     my $emailed = ($self->{emailed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
3020
3021     my %queued = split / /, $self->{queued};
3022     my @values;
3023
3024     if ($self->{formname} =~ /(check|receipt)/) {
3025
3026       # this is a check or receipt, add one entry for each lineitem
3027       my ($accno) = split /--/, $self->{account};
3028       $query = qq|INSERT INTO status (trans_id, printed, spoolfile, formname, chart_id)
3029                   VALUES (?, ?, ?, ?, (SELECT c.id FROM chart c WHERE c.accno = ?))|;
3030       @values = ($printed, $queued{$self->{formname}}, $self->{prinform}, $accno);
3031       $sth = prepare_query($self, $dbh, $query);
3032
3033       for $i (1 .. $self->{rowcount}) {
3034         if ($self->{"checked_$i"}) {
3035           do_statement($self, $sth, $query, $self->{"id_$i"}, @values);
3036         }
3037       }
3038       $sth->finish();
3039
3040     } else {
3041       $query = qq|INSERT INTO status (trans_id, printed, emailed, spoolfile, formname)
3042                   VALUES (?, ?, ?, ?, ?)|;
3043       do_query($self, $dbh, $query, $self->{id}, $printed, $emailed,
3044                $queued{$self->{formname}}, $self->{formname});
3045     }
3046     1;
3047   }) or do { die SL::DB->client->error };
3048
3049   $main::lxdebug->leave_sub();
3050 }
3051
3052 sub save_status {
3053   $main::lxdebug->enter_sub();
3054
3055   my ($self, $dbh) = @_;
3056
3057   my ($query, $printed, $emailed);
3058
3059   my $formnames  = $self->{printed};
3060   my $emailforms = $self->{emailed};
3061
3062   $query = qq|DELETE FROM status
3063                  WHERE (formname = ?) AND (trans_id = ?)|;
3064   do_query($self, $dbh, $query, $self->{formname}, $self->{id});
3065
3066   # this only applies to the forms
3067   # checks and receipts are posted when printed or queued
3068
3069   if ($self->{queued}) {
3070     my %queued = split / /, $self->{queued};
3071
3072     foreach my $formname (keys %queued) {
3073       $printed = ($self->{printed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
3074       $emailed = ($self->{emailed} =~ /\Q$self->{formname}\E/) ? "1" : "0";
3075
3076       $query = qq|INSERT INTO status (trans_id, printed, emailed, spoolfile, formname)
3077                   VALUES (?, ?, ?, ?, ?)|;
3078       do_query($self, $dbh, $query, $self->{id}, $printed, $emailed, $queued{$formname}, $formname);
3079
3080       $formnames  =~ s/\Q$self->{formname}\E//;
3081       $emailforms =~ s/\Q$self->{formname}\E//;
3082
3083     }
3084   }
3085
3086   # save printed, emailed info
3087   $formnames  =~ s/^ +//g;
3088   $emailforms =~ s/^ +//g;
3089
3090   my %status = ();
3091   map { $status{$_}{printed} = 1 } split / +/, $formnames;
3092   map { $status{$_}{emailed} = 1 } split / +/, $emailforms;
3093
3094   foreach my $formname (keys %status) {
3095     $printed = ($formnames  =~ /\Q$self->{formname}\E/) ? "1" : "0";
3096     $emailed = ($emailforms =~ /\Q$self->{formname}\E/) ? "1" : "0";
3097
3098     $query = qq|INSERT INTO status (trans_id, printed, emailed, formname)
3099                 VALUES (?, ?, ?, ?)|;
3100     do_query($self, $dbh, $query, $self->{id}, $printed, $emailed, $formname);
3101   }
3102
3103   $main::lxdebug->leave_sub();
3104 }
3105
3106 #--- 4 locale ---#
3107 # $main::locale->text('SAVED')
3108 # $main::locale->text('SCREENED')
3109 # $main::locale->text('DELETED')
3110 # $main::locale->text('ADDED')
3111 # $main::locale->text('PAYMENT POSTED')
3112 # $main::locale->text('POSTED')
3113 # $main::locale->text('POSTED AS NEW')
3114 # $main::locale->text('ELSE')
3115 # $main::locale->text('SAVED FOR DUNNING')
3116 # $main::locale->text('DUNNING STARTED')
3117 # $main::locale->text('PRINTED')
3118 # $main::locale->text('MAILED')
3119 # $main::locale->text('SCREENED')
3120 # $main::locale->text('CANCELED')
3121 # $main::locale->text('IMPORT')
3122 # $main::locale->text('UNIMPORT')
3123 # $main::locale->text('invoice')
3124 # $main::locale->text('proforma')
3125 # $main::locale->text('sales_order')
3126 # $main::locale->text('pick_list')
3127 # $main::locale->text('purchase_order')
3128 # $main::locale->text('bin_list')
3129 # $main::locale->text('sales_quotation')
3130 # $main::locale->text('request_quotation')
3131
3132 sub save_history {
3133   $main::lxdebug->enter_sub();
3134
3135   my $self = shift;
3136   my $dbh  = shift || SL::DB->client->dbh;
3137   SL::DB->client->with_transaction(sub {
3138
3139     if(!exists $self->{employee_id}) {
3140       &get_employee($self, $dbh);
3141     }
3142
3143     my $query =
3144      qq|INSERT INTO history_erp (trans_id, employee_id, addition, what_done, snumbers) | .
3145      qq|VALUES (?, (SELECT id FROM employee WHERE login = ?), ?, ?, ?)|;
3146     my @values = (conv_i($self->{id}), $self->{login},
3147                   $self->{addition}, $self->{what_done}, "$self->{snumbers}");
3148     do_query($self, $dbh, $query, @values);
3149     1;
3150   }) or do { die SL::DB->client->error };
3151
3152   $main::lxdebug->leave_sub();
3153 }
3154
3155 sub get_history {
3156   $main::lxdebug->enter_sub();
3157
3158   my ($self, $dbh, $trans_id, $restriction, $order) = @_;
3159   my ($orderBy, $desc) = split(/\-\-/, $order);
3160   $order = " ORDER BY " . ($order eq "" ? " h.itime " : ($desc == 1 ? $orderBy . " DESC " : $orderBy . " "));
3161   my @tempArray;
3162   my $i = 0;
3163   if ($trans_id ne "") {
3164     my $query =
3165       qq|SELECT h.employee_id, h.itime::timestamp(0) AS itime, h.addition, h.what_done, emp.name, h.snumbers, h.trans_id AS id | .
3166       qq|FROM history_erp h | .
3167       qq|LEFT JOIN employee emp ON (emp.id = h.employee_id) | .
3168       qq|WHERE (trans_id = | . $trans_id . qq|) $restriction | .
3169       $order;
3170
3171     my $sth = $dbh->prepare($query) || $self->dberror($query);
3172
3173     $sth->execute() || $self->dberror("$query");
3174
3175     while(my $hash_ref = $sth->fetchrow_hashref()) {
3176       $hash_ref->{addition} = $main::locale->text($hash_ref->{addition});
3177       $hash_ref->{what_done} = $main::locale->text($hash_ref->{what_done});
3178       my ( $what, $number ) = split /_/, $hash_ref->{snumbers};
3179       $hash_ref->{snumbers} = $number;
3180       $hash_ref->{haslink}  = 'controller.pl?action=EmailJournal/show&id='.$number if $what eq 'emailjournal';
3181       $hash_ref->{snumbers} = $main::locale->text("E-Mail").' '.$number if $what eq 'emailjournal';
3182       $tempArray[$i++] = $hash_ref;
3183     }
3184     $main::lxdebug->leave_sub() and return \@tempArray
3185       if ($i > 0 && $tempArray[0] ne "");
3186   }
3187   $main::lxdebug->leave_sub();
3188   return 0;
3189 }
3190
3191 sub get_partsgroup {
3192   $main::lxdebug->enter_sub();
3193
3194   my ($self, $myconfig, $p) = @_;
3195   my $target = $p->{target} || 'all_partsgroup';
3196
3197   my $dbh = $self->get_standard_dbh($myconfig);
3198
3199   my $query = qq|SELECT DISTINCT pg.id, pg.partsgroup
3200                  FROM partsgroup pg
3201                  JOIN parts p ON (p.partsgroup_id = pg.id) |;
3202   my @values;
3203
3204   if ($p->{searchitems} eq 'part') {
3205     $query .= qq|WHERE p.part_type = 'part'|;
3206   }
3207   if ($p->{searchitems} eq 'service') {
3208     $query .= qq|WHERE p.part_type = 'service'|;
3209   }
3210   if ($p->{searchitems} eq 'assembly') {
3211     $query .= qq|WHERE p.part_type = 'assembly'|;
3212   }
3213
3214   $query .= qq|ORDER BY partsgroup|;
3215
3216   if ($p->{all}) {
3217     $query = qq|SELECT id, partsgroup FROM partsgroup
3218                 ORDER BY partsgroup|;
3219   }
3220
3221   if ($p->{language_code}) {
3222     $query = qq|SELECT DISTINCT pg.id, pg.partsgroup,
3223                   t.description AS translation
3224                 FROM partsgroup pg
3225                 JOIN parts p ON (p.partsgroup_id = pg.id)
3226                 LEFT JOIN translation t ON ((t.trans_id = pg.id) AND (t.language_code = ?))
3227                 ORDER BY translation|;
3228     @values = ($p->{language_code});
3229   }
3230
3231   $self->{$target} = selectall_hashref_query($self, $dbh, $query, @values);
3232
3233   $main::lxdebug->leave_sub();
3234 }
3235
3236 sub get_pricegroup {
3237   $main::lxdebug->enter_sub();
3238
3239   my ($self, $myconfig, $p) = @_;
3240
3241   my $dbh = $self->get_standard_dbh($myconfig);
3242
3243   my $query = qq|SELECT p.id, p.pricegroup
3244                  FROM pricegroup p|;
3245
3246   $query .= qq| ORDER BY pricegroup|;
3247
3248   if ($p->{all}) {
3249     $query = qq|SELECT id, pricegroup FROM pricegroup
3250                 ORDER BY pricegroup|;
3251   }
3252
3253   $self->{all_pricegroup} = selectall_hashref_query($self, $dbh, $query);
3254
3255   $main::lxdebug->leave_sub();
3256 }
3257
3258 sub all_years {
3259 # usage $form->all_years($myconfig, [$dbh])
3260 # return list of all years where bookings found
3261 # (@all_years)
3262
3263   $main::lxdebug->enter_sub();
3264
3265   my ($self, $myconfig, $dbh) = @_;
3266
3267   $dbh ||= $self->get_standard_dbh($myconfig);
3268
3269   # get years
3270   my $query = qq|SELECT (SELECT MIN(transdate) FROM acc_trans),
3271                    (SELECT MAX(transdate) FROM acc_trans)|;
3272   my ($startdate, $enddate) = selectrow_query($self, $dbh, $query);
3273
3274   if ($myconfig->{dateformat} =~ /^yy/) {
3275     ($startdate) = split /\W/, $startdate;
3276     ($enddate) = split /\W/, $enddate;
3277   } else {
3278     (@_) = split /\W/, $startdate;
3279     $startdate = $_[2];
3280     (@_) = split /\W/, $enddate;
3281     $enddate = $_[2];
3282   }
3283
3284   my @all_years;
3285   $startdate = substr($startdate,0,4);
3286   $enddate = substr($enddate,0,4);
3287
3288   while ($enddate >= $startdate) {
3289     push @all_years, $enddate--;
3290   }
3291
3292   return @all_years;
3293
3294   $main::lxdebug->leave_sub();
3295 }
3296
3297 sub backup_vars {
3298   $main::lxdebug->enter_sub();
3299   my $self = shift;
3300   my @vars = @_;
3301
3302   map { $self->{_VAR_BACKUP}->{$_} = $self->{$_} if exists $self->{$_} } @vars;
3303
3304   $main::lxdebug->leave_sub();
3305 }
3306
3307 sub restore_vars {
3308   $main::lxdebug->enter_sub();
3309
3310   my $self = shift;
3311   my @vars = @_;
3312
3313   map { $self->{$_} = $self->{_VAR_BACKUP}->{$_} if exists $self->{_VAR_BACKUP}->{$_} } @vars;
3314
3315   $main::lxdebug->leave_sub();
3316 }
3317
3318 sub prepare_for_printing {
3319   my ($self) = @_;
3320
3321   my $defaults         = SL::DB::Default->get;
3322
3323   $self->{templates} ||= $defaults->templates;
3324   $self->{formname}  ||= $self->{type};
3325   $self->{media}     ||= 'email';
3326
3327   die "'media' other than 'email', 'file', 'printer' is not supported yet" unless $self->{media} =~ m/^(?:email|file|printer)$/;
3328
3329   # Several fields that used to reside in %::myconfig (stored in
3330   # auth.user_config) are now stored in defaults. Copy them over for
3331   # compatibility.
3332   $self->{$_} = $defaults->$_ for qw(company address taxnumber co_ustid duns sepa_creditor_id);
3333
3334   $self->{"myconfig_${_}"} = $::myconfig{$_} for grep { $_ ne 'dbpasswd' } keys %::myconfig;
3335
3336   if (!$self->{employee_id}) {
3337     $self->{"employee_${_}"} = $::myconfig{$_} for qw(email tel fax name signature);
3338     $self->{"employee_${_}"} = $defaults->$_   for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
3339   }
3340
3341   # Load shipping address from database. If shipto_id is set then it's
3342   # one from the customer's/vendor's master data. Otherwise look an a
3343   # customized address linking back to the current record.
3344   my $shipto_module = $self->{type} =~ /_delivery_order$/                                             ? 'DO'
3345                     : $self->{type} =~ /sales_order|sales_quotation|request_quotation|purchase_order/ ? 'OE'
3346                     :                                                                                   'AR';
3347   my $shipto        = $self->{shipto_id} ? SL::DB::Shipto->new(shipto_id => $self->{shipto_id})->load
3348                     :                      SL::DB::Manager::Shipto->get_first(where => [ module => $shipto_module, trans_id => $self->{id} ]);
3349   if ($shipto) {
3350     $self->{$_} = $shipto->$_ for grep { m{^shipto} } map { $_->name } @{ $shipto->meta->columns };
3351     $self->{"shiptocvar_" . $_->config->name} = $_->value_as_text for @{ $shipto->cvars_by_config };
3352   }
3353
3354   my $language = $self->{language} ? '_' . $self->{language} : '';
3355
3356   my ($language_tc, $output_numberformat, $output_dateformat, $output_longdates);
3357   if ($self->{language_id}) {
3358     ($language_tc, $output_numberformat, $output_dateformat, $output_longdates) = AM->get_language_details(\%::myconfig, $self, $self->{language_id});
3359   }
3360
3361   $output_dateformat   ||= $::myconfig{dateformat};
3362   $output_numberformat ||= $::myconfig{numberformat};
3363   $output_longdates    //= 1;
3364
3365   $self->{myconfig_output_dateformat}   = $output_dateformat   // $::myconfig{dateformat};
3366   $self->{myconfig_output_longdates}    = $output_longdates    // 1;
3367   $self->{myconfig_output_numberformat} = $output_numberformat // $::myconfig{numberformat};
3368
3369   # Retrieve accounts for tax calculation.
3370   IC->retrieve_accounts(\%::myconfig, $self, map { $_ => $self->{"id_$_"} } 1 .. $self->{rowcount});
3371
3372   if ($self->{type} =~ /_delivery_order$/) {
3373     DO->order_details(\%::myconfig, $self);
3374   } elsif ($self->{type} =~ /sales_order|sales_quotation|request_quotation|purchase_order/) {
3375     OE->order_details(\%::myconfig, $self);
3376   } else {
3377     IS->invoice_details(\%::myconfig, $self, $::locale);
3378   }
3379
3380   # Chose extension & set source file name
3381   my $extension = 'html';
3382   if ($self->{format} eq 'postscript') {
3383     $self->{postscript}   = 1;
3384     $extension            = 'tex';
3385   } elsif ($self->{"format"} =~ /pdf/) {
3386     $self->{pdf}          = 1;
3387     $extension            = $self->{'format'} =~ m/opendocument/i ? 'odt' : 'tex';
3388   } elsif ($self->{"format"} =~ /opendocument/) {
3389     $self->{opendocument} = 1;
3390     $extension            = 'odt';
3391   } elsif ($self->{"format"} =~ /excel/) {
3392     $self->{excel}        = 1;
3393     $extension            = 'xls';
3394   }
3395
3396   my $printer_code    = $self->{printer_code} ? '_' . $self->{printer_code} : '';
3397   my $email_extension = $self->{media} eq 'email' && -f ($defaults->templates . "/$self->{formname}_email${language}.${extension}") ? '_email' : '';
3398   $self->{IN}         = "$self->{formname}${email_extension}${language}${printer_code}.${extension}";
3399
3400   # Format dates.
3401   $self->format_dates($output_dateformat, $output_longdates,
3402                       qw(invdate orddate quodate pldate duedate reqdate transdate shippingdate deliverydate validitydate paymentdate datepaid
3403                          transdate_oe deliverydate_oe employee_startdate employee_enddate),
3404                       grep({ /^(?:datepaid|transdate_oe|reqdate|deliverydate|deliverydate_oe|transdate)_\d+$/ } keys(%{$self})));
3405
3406   $self->reformat_numbers($output_numberformat, 2,
3407                           qw(invtotal ordtotal quototal subtotal linetotal listprice sellprice netprice discount tax taxbase total paid),
3408                           grep({ /^(?:linetotal|listprice|sellprice|netprice|taxbase|discount|paid|subtotal|total|tax)_\d+$/ } keys(%{$self})));
3409
3410   $self->reformat_numbers($output_numberformat, undef, qw(qty price_factor), grep({ /^qty_\d+$/} keys(%{$self})));
3411
3412   my ($cvar_date_fields, $cvar_number_fields) = CVar->get_field_format_list('module' => 'CT', 'prefix' => 'vc_');
3413
3414   if (scalar @{ $cvar_date_fields }) {
3415     $self->format_dates($output_dateformat, $output_longdates, @{ $cvar_date_fields });
3416   }
3417
3418   while (my ($precision, $field_list) = each %{ $cvar_number_fields }) {
3419     $self->reformat_numbers($output_numberformat, $precision, @{ $field_list });
3420   }
3421
3422   # Translate units
3423   if (($self->{language} // '') ne '') {
3424     my $template_arrays = $self->{TEMPLATE_ARRAYS} || $self;
3425     for my $idx (0..scalar(@{ $template_arrays->{unit} }) - 1) {
3426       $template_arrays->{unit}->[$idx] = AM->translate_units($self, $self->{language}, $template_arrays->{unit}->[$idx], $template_arrays->{qty}->[$idx])
3427     }
3428   }
3429
3430   $self->{template_meta} = {
3431     formname  => $self->{formname},
3432     language  => SL::DB::Manager::Language->find_by_or_create(id => $self->{language_id} || undef),
3433     format    => $self->{format},
3434     media     => $self->{media},
3435     extension => $extension,
3436     printer   => SL::DB::Manager::Printer->find_by_or_create(id => $self->{printer_id} || undef),
3437     today     => DateTime->today,
3438   };
3439
3440   return $self;
3441 }
3442
3443 sub calculate_arap {
3444   my ($self,$buysell,$taxincluded,$exchangerate,$roundplaces) = @_;
3445
3446   # this function is used to calculate netamount, total_tax and amount for AP and
3447   # AR transactions (Kreditoren-/Debitorenbuchungen) by going over all lines
3448   # (1..$rowcount)
3449   # Thus it needs a fully prepared $form to work on.
3450   # calculate_arap assumes $form->{amount_$i} entries still need to be parsed
3451
3452   # The calculated total values are all rounded (default is to 2 places) and
3453   # returned as parameters rather than directly modifying form.  The aim is to
3454   # make the calculation of AP and AR behave identically.  There is a test-case
3455   # for this function in t/form/arap.t
3456
3457   # While calculating the totals $form->{amount_$i} and $form->{tax_$i} are
3458   # modified and formatted and receive the correct sign for writing straight to
3459   # acc_trans, depending on whether they are ar or ap.
3460
3461   # check parameters
3462   die "taxincluded needed in Form->calculate_arap" unless defined $taxincluded;
3463   die "exchangerate needed in Form->calculate_arap" unless defined $exchangerate;
3464   die 'illegal buysell parameter, has to be \"buy\" or \"sell\" in Form->calculate_arap\n' unless $buysell =~ /^(buy|sell)$/;
3465   $roundplaces = 2 unless $roundplaces;
3466
3467   my $sign = 1;  # adjust final results for writing amount to acc_trans
3468   $sign = -1 if $buysell eq 'buy';
3469
3470   my ($netamount,$total_tax,$amount);
3471
3472   my $tax;
3473
3474   # parse and round amounts, setting correct sign for writing to acc_trans
3475   for my $i (1 .. $self->{rowcount}) {
3476     $self->{"amount_$i"} = $self->round_amount($self->parse_amount(\%::myconfig, $self->{"amount_$i"}) * $exchangerate * $sign, $roundplaces);
3477
3478     $amount += $self->{"amount_$i"} * $sign;
3479   }
3480
3481   for my $i (1 .. $self->{rowcount}) {
3482     next unless $self->{"amount_$i"};
3483     ($self->{"tax_id_$i"}) = split /--/, $self->{"taxchart_$i"};
3484     my $tax_id = $self->{"tax_id_$i"};
3485
3486     my $selected_tax = SL::DB::Manager::Tax->find_by(id => "$tax_id");
3487
3488     if ( $selected_tax ) {
3489
3490       if ( $buysell eq 'sell' ) {
3491         $self->{AR_amounts}{"tax_$i"} = $selected_tax->chart->accno if defined $selected_tax->chart;
3492       } else {
3493         $self->{AP_amounts}{"tax_$i"} = $selected_tax->chart->accno if defined $selected_tax->chart;
3494       };
3495
3496       $self->{"taxkey_$i"} = $selected_tax->taxkey;
3497       $self->{"taxrate_$i"} = $selected_tax->rate;
3498     };
3499
3500     ($self->{"amount_$i"}, $self->{"tax_$i"}) = $self->calculate_tax($self->{"amount_$i"},$self->{"taxrate_$i"},$taxincluded,$roundplaces);
3501
3502     $netamount  += $self->{"amount_$i"};
3503     $total_tax  += $self->{"tax_$i"};
3504
3505   }
3506   $amount = $netamount + $total_tax;
3507
3508   # due to $sign amount_$i und tax_$i already have the right sign for acc_trans
3509   # but reverse sign of totals for writing amounts to ar
3510   if ( $buysell eq 'buy' ) {
3511     $netamount *= -1;
3512     $amount    *= -1;
3513     $total_tax *= -1;
3514   };
3515
3516   return($netamount,$total_tax,$amount);
3517 }
3518
3519 sub format_dates {
3520   my ($self, $dateformat, $longformat, @indices) = @_;
3521
3522   $dateformat ||= $::myconfig{dateformat};
3523
3524   foreach my $idx (@indices) {
3525     if ($self->{TEMPLATE_ARRAYS} && (ref($self->{TEMPLATE_ARRAYS}->{$idx}) eq "ARRAY")) {
3526       for (my $i = 0; $i < scalar(@{ $self->{TEMPLATE_ARRAYS}->{$idx} }); $i++) {
3527         $self->{TEMPLATE_ARRAYS}->{$idx}->[$i] = $::locale->reformat_date(\%::myconfig, $self->{TEMPLATE_ARRAYS}->{$idx}->[$i], $dateformat, $longformat);
3528       }
3529     }
3530
3531     next unless defined $self->{$idx};
3532
3533     if (!ref($self->{$idx})) {
3534       $self->{$idx} = $::locale->reformat_date(\%::myconfig, $self->{$idx}, $dateformat, $longformat);
3535
3536     } elsif (ref($self->{$idx}) eq "ARRAY") {
3537       for (my $i = 0; $i < scalar(@{ $self->{$idx} }); $i++) {
3538         $self->{$idx}->[$i] = $::locale->reformat_date(\%::myconfig, $self->{$idx}->[$i], $dateformat, $longformat);
3539       }
3540     }
3541   }
3542 }
3543
3544 sub reformat_numbers {
3545   my ($self, $numberformat, $places, @indices) = @_;
3546
3547   return if !$numberformat || ($numberformat eq $::myconfig{numberformat});
3548
3549   foreach my $idx (@indices) {
3550     if ($self->{TEMPLATE_ARRAYS} && (ref($self->{TEMPLATE_ARRAYS}->{$idx}) eq "ARRAY")) {
3551       for (my $i = 0; $i < scalar(@{ $self->{TEMPLATE_ARRAYS}->{$idx} }); $i++) {
3552         $self->{TEMPLATE_ARRAYS}->{$idx}->[$i] = $self->parse_amount(\%::myconfig, $self->{TEMPLATE_ARRAYS}->{$idx}->[$i]);
3553       }
3554     }
3555
3556     next unless defined $self->{$idx};
3557
3558     if (!ref($self->{$idx})) {
3559       $self->{$idx} = $self->parse_amount(\%::myconfig, $self->{$idx});
3560
3561     } elsif (ref($self->{$idx}) eq "ARRAY") {
3562       for (my $i = 0; $i < scalar(@{ $self->{$idx} }); $i++) {
3563         $self->{$idx}->[$i] = $self->parse_amount(\%::myconfig, $self->{$idx}->[$i]);
3564       }
3565     }
3566   }
3567
3568   my $saved_numberformat    = $::myconfig{numberformat};
3569   $::myconfig{numberformat} = $numberformat;
3570
3571   foreach my $idx (@indices) {
3572     if ($self->{TEMPLATE_ARRAYS} && (ref($self->{TEMPLATE_ARRAYS}->{$idx}) eq "ARRAY")) {
3573       for (my $i = 0; $i < scalar(@{ $self->{TEMPLATE_ARRAYS}->{$idx} }); $i++) {
3574         $self->{TEMPLATE_ARRAYS}->{$idx}->[$i] = $self->format_amount(\%::myconfig, $self->{TEMPLATE_ARRAYS}->{$idx}->[$i], $places);
3575       }
3576     }
3577
3578     next unless defined $self->{$idx};
3579
3580     if (!ref($self->{$idx})) {
3581       $self->{$idx} = $self->format_amount(\%::myconfig, $self->{$idx}, $places);
3582
3583     } elsif (ref($self->{$idx}) eq "ARRAY") {
3584       for (my $i = 0; $i < scalar(@{ $self->{$idx} }); $i++) {
3585         $self->{$idx}->[$i] = $self->format_amount(\%::myconfig, $self->{$idx}->[$i], $places);
3586       }
3587     }
3588   }
3589
3590   $::myconfig{numberformat} = $saved_numberformat;
3591 }
3592
3593 sub create_email_signature {
3594
3595   my $client_signature = $::instance_conf->get_signature;
3596   my $user_signature   = $::myconfig{signature};
3597
3598   my $signature = '';
3599   if ( $client_signature or $user_signature ) {
3600     $signature  = "\n\n-- \n";
3601     $signature .= $user_signature   . "\n" if $user_signature;
3602     $signature .= $client_signature . "\n" if $client_signature;
3603   };
3604   return $signature;
3605
3606 };
3607
3608 sub calculate_tax {
3609   # this function calculates the net amount and tax for the lines in ar, ap and
3610   # gl and is used for update as well as post. When used with update the return
3611   # value of amount isn't needed
3612
3613   # calculate_tax should always work with positive values, or rather as the user inputs them
3614   # calculate_tax uses db/perl numberformat, i.e. parsed numbers
3615   # convert to negative numbers (when necessary) only when writing to acc_trans
3616   # the amount from $form for ap/ar/gl is currently always rounded to 2 decimals before it reaches here
3617   # for post_transaction amount already contains exchangerate and correct sign and is rounded
3618   # calculate_tax doesn't (need to) know anything about exchangerate
3619
3620   my ($self,$amount,$taxrate,$taxincluded,$roundplaces) = @_;
3621
3622   $roundplaces //= 2;
3623   $taxincluded //= 0;
3624
3625   my $tax;
3626
3627   if ($taxincluded) {
3628     # calculate tax (unrounded), subtract from amount, round amount and round tax
3629     $tax       = $amount - ($amount / ($taxrate + 1)); # equivalent to: taxrate * amount / (taxrate + 1)
3630     $amount    = $self->round_amount($amount - $tax, $roundplaces);
3631     $tax       = $self->round_amount($tax, $roundplaces);
3632   } else {
3633     $tax       = $amount * $taxrate;
3634     $tax       = $self->round_amount($tax, $roundplaces);
3635   }
3636
3637   $tax = 0 unless $tax;
3638
3639   return ($amount,$tax);
3640 };
3641
3642 1;
3643
3644 __END__
3645
3646 =head1 NAME
3647
3648 SL::Form.pm - main data object.
3649
3650 =head1 SYNOPSIS
3651
3652 This is the main data object of kivitendo.
3653 Unfortunately it also acts as a god object for certain data retrieval procedures used in the entry points.
3654 Points of interest for a beginner are:
3655
3656  - $form->error            - renders a generic error in html. accepts an error message
3657  - $form->get_standard_dbh - returns a database connection for the
3658
3659 =head1 SPECIAL FUNCTIONS
3660
3661 =head2 C<redirect_header> $url
3662
3663 Generates a HTTP redirection header for the new C<$url>. Constructs an
3664 absolute URL including scheme, host name and port. If C<$url> is a
3665 relative URL then it is considered relative to kivitendo base URL.
3666
3667 This function C<die>s if headers have already been created with
3668 C<$::form-E<gt>header>.
3669
3670 Examples:
3671
3672   print $::form->redirect_header('oe.pl?action=edit&id=1234');
3673   print $::form->redirect_header('http://www.lx-office.org/');
3674
3675 =head2 C<header>
3676
3677 Generates a general purpose http/html header and includes most of the scripts
3678 and stylesheets needed. Stylesheets can be added with L<use_stylesheet>.
3679
3680 Only one header will be generated. If the method was already called in this
3681 request it will not output anything and return undef. Also if no
3682 HTTP_USER_AGENT is found, no header is generated.
3683
3684 Although header does not accept parameters itself, it will honor special
3685 hashkeys of its Form instance:
3686
3687 =over 4
3688
3689 =item refresh_time
3690
3691 =item refresh_url
3692
3693 If one of these is set, a http-equiv refresh is generated. Missing parameters
3694 default to 3 seconds and the refering url.
3695
3696 =item stylesheet
3697
3698 Either a scalar or an array ref. Will be inlined into the header. Add
3699 stylesheets with the L<use_stylesheet> function.
3700
3701 =item landscape
3702
3703 If true, a css snippet will be generated that sets the page in landscape mode.
3704
3705 =item favicon
3706
3707 Used to override the default favicon.
3708
3709 =item title
3710
3711 A html page title will be generated from this
3712
3713 =item mtime_ischanged
3714
3715 Tries to avoid concurrent write operations to records by checking the database mtime with a fetched one.
3716
3717 Can be used / called with any table, that has itime and mtime attributes.
3718 Valid C<table> names are: oe, ar, ap, delivery_orders, parts.
3719 Can be called wit C<option> mail to generate a different error message.
3720
3721 Returns undef if no save operation has been done yet ($self->{id} not present).
3722 Returns undef if no concurrent write process is detected otherwise a error message.
3723
3724 =back
3725
3726 =cut