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