format_amount - negative 0 vermeiden
[kivitendo-erp.git] / SL / Form.pm
index e115f97..35b21d5 100644 (file)
@@ -40,11 +40,14 @@ package Form;
 use Carp;
 use Data::Dumper;
 
+use Carp;
+use Config;
 use CGI;
 use Cwd;
 use Encode;
 use File::Copy;
 use IO::File;
+use Math::BigInt;
 use SL::Auth;
 use SL::Auth::DB;
 use SL::Auth::LDAP;
@@ -63,6 +66,7 @@ use SL::IC;
 use SL::IS;
 use SL::Layout::Dispatcher;
 use SL::Locale;
+use SL::Locale::String;
 use SL::Mailer;
 use SL::Menu;
 use SL::MoreCommon qw(uri_encode uri_decode);
@@ -76,6 +80,7 @@ use Template;
 use URI;
 use List::Util qw(first max min sum);
 use List::MoreUtils qw(all any apply);
+use SL::DB::Tax;
 
 use strict;
 
@@ -87,7 +92,8 @@ END {
 
 sub disconnect_standard_dbh {
   return unless $standard_dbh;
-  $standard_dbh->disconnect();
+
+  $standard_dbh->rollback();
   undef $standard_dbh;
 }
 
@@ -307,8 +313,7 @@ sub error {
     $self->show_generic_error($msg);
 
   } else {
-    print STDERR "Error: $msg\n";
-    ::end_of_request();
+    confess "Error: $msg\n";
   }
 
   $main::lxdebug->leave_sub();
@@ -464,16 +469,18 @@ sub header {
   # standard css for all
   # this should gradually move to the layouts that need it
   $layout->use_stylesheet("$_.css") for qw(
-    main menu list_accounts jquery.autocomplete
+    main menu common list_accounts jquery.autocomplete
     jquery.multiselect2side
     ui-lightness/jquery-ui
     jquery-ui.custom
+    tooltipster themes/tooltipster-light
   );
 
   $layout->use_javascript("$_.js") for (qw(
     jquery jquery-ui jquery.cookie jquery.checkall jquery.download
-    jquery/jquery.form client_js
-    common part_selection switchmenuframe autocomplete_part
+    jquery/jquery.form jquery/fixes client_js
+    jquery/jquery.tooltipster.min
+    common part_selection switchmenuframe
   ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}");
 
   $self->{favicon} ||= "favicon.ico";
@@ -533,7 +540,7 @@ sub footer {
   print $::request->{layout}->post_content;
 
   if (my @inline_scripts = $::request->{layout}->javascripts_inline) {
-    print "<script type='text/javascript'>@inline_scripts</script>\n";
+    print "<script type='text/javascript'>" . join("; ", @inline_scripts) . "</script>\n";
   }
 
   print <<EOL
@@ -578,6 +585,17 @@ sub set_standard_title {
   $::lxdebug->leave_sub;
 }
 
+sub prepare_global_vars {
+  my ($self) = @_;
+
+  $self->{AUTH}            = $::auth;
+  $self->{INSTANCE_CONF}   = $::instance_conf;
+  $self->{LOCALE}          = $::locale;
+  $self->{LXCONFIG}        = $::lx_office_conf;
+  $self->{LXDEBUG}         = $::lxdebug;
+  $self->{MYCONFIG}        = \%::myconfig;
+}
+
 sub _prepare_html_template {
   $main::lxdebug->enter_sub();
 
@@ -594,8 +612,11 @@ sub _prepare_html_template {
   if (-f "templates/webpages/${file}.html") {
     $file = "templates/webpages/${file}.html";
 
+  } elsif (ref $file eq 'SCALAR') {
+    # file is a scalarref, use inline mode
   } else {
     my $info = "Web page template '${file}' not found.\n";
+    $::form->header;
     print qq|<pre>$info</pre>|;
     ::end_of_request();
   }
@@ -673,6 +694,7 @@ sub init_template {
      'COMPILE_EXT'  => '.tcc',
      'COMPILE_DIR'  => $::lx_office_conf{paths}->{userspath} . '/templates-cache',
      'ERROR'        => 'templates/webpages/generic/exception.html',
+     'ENCODING'     => 'utf8',
   })) || die;
 }
 
@@ -694,7 +716,6 @@ sub show_generic_error {
   }
 
   if ($::request->is_ajax) {
-    $::lxdebug->message(0, "trying to render AJAX response...");
     SL::ClientJS->new
       ->error($error)
       ->render(SL::Controller::Base->new);
@@ -801,6 +822,7 @@ sub format_amount {
   my $force_places = defined $places && $places >= 0;
 
   $amount = $self->round_amount($amount, abs $places) if $force_places;
+  $neg    = 0 if $amount == 0; # don't show negative zero
   $amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa
 
   # before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl
@@ -941,28 +963,47 @@ sub parse_amount {
 
   # Make sure no code wich is not a math expression ends up in eval().
   return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x;
+
+  # Prevent numbers from being parsed as octals;
+  $amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx;
+
   return scalar(eval($amount)) * 1 ;
 }
 
 sub round_amount {
-  $main::lxdebug->enter_sub(2);
-
   my ($self, $amount, $places) = @_;
-  my $round_amount;
 
-  # Rounding like "Kaufmannsrunden" (see http://de.wikipedia.org/wiki/Rundung )
+  return 0 if !defined $amount;
 
-  # Round amounts to eight places before rounding to the requested
-  # number of places. This gets rid of errors due to internal floating
-  # point representation.
-  $amount       = $self->round_amount($amount, 8) if $places < 8;
-  $amount       = $amount * (10**($places));
-  $round_amount = int($amount + .5 * ($amount <=> 0)) / (10**($places));
+  # We use Perl's knowledge of string representation for
+  # rounding. First, convert the floating point number to a string
+  # with a high number of places. Then split the string on the decimal
+  # sign and use integer calculation for rounding the decimal places
+  # part. If an overflow occurs then apply that overflow to the part
+  # before the decimal sign as well using integer arithmetic again.
 
-  $main::lxdebug->leave_sub(2);
+  my $amount_str = sprintf '%.*f', $places + 10, abs($amount);
+
+  return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$};
+
+  my ($pre, $post)      = ($1, $2);
+  my $decimals          = '1' . substr($post, 0, $places);
+
+  my $propagation_limit = $Config{i32size} == 4 ? 7 : 18;
+  my $add_for_rounding  = substr($post, $places, 1) >= 5 ? 1 : 0;
 
-  return $round_amount;
+  if ($places > $propagation_limit) {
+    $decimals = Math::BigInt->new($decimals)->badd($add_for_rounding);
+    $pre      = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2';
 
+  } else {
+    $decimals += $add_for_rounding;
+    $pre      += 1 if substr($decimals, 0, 1) eq '2';
+  }
+
+  $amount  = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0);
+
+  return $amount;
 }
 
 sub parse_template {
@@ -1019,10 +1060,11 @@ sub parse_template {
                                       file_name => $self->{IN},
                                       form      => $self,
                                       myconfig  => $myconfig,
-                                      userspath => $userspath);
+                                      userspath => $userspath,
+                                      %{ $self->{TEMPLATE_DRIVER_OPTIONS} || {} });
 
   # Copy the notes from the invoice/sales order etc. back to the variable "notes" because that is where most templates expect it to be.
-  $self->{"notes"} = $self->{ $self->{"formname"} . "notes" };
+  $self->{"notes"} = $self->{ $self->{"formname"} . "notes" } if exists $self->{ $self->{"formname"} . "notes" };
 
   if (!$self->{employee_id}) {
     $self->{"employee_${_}"} = $myconfig->{$_} for qw(email tel fax name signature);
@@ -1080,8 +1122,9 @@ sub parse_template {
   }
 
   close OUT if $self->{OUT};
-
-  my $copy_to_webdav = $::instance_conf->get_webdav && $::instance_conf->get_webdav_documents && !$self->{preview} && $self->{tmpdir} && $self->{tmpfile} && $self->{type};
+  # check only one flag (webdav_documents)
+  # therefore copy to webdav, even if we do not have the webdav feature enabled (just archive)
+  my $copy_to_webdav =  $::instance_conf->get_webdav_documents && !$self->{preview} && $self->{tmpdir} && $self->{tmpfile} && $self->{type};
 
   if ($self->{media} eq 'file') {
     copy(join('/', $self->{cwd}, $userspath, $self->{tmpfile}), $out =~ m|^/| ? $out : join('/', $self->{cwd}, $out)) if $template->uses_temp_file;
@@ -1098,24 +1141,25 @@ sub parse_template {
 
   if ($self->{media} eq 'email') {
 
-    my $mail = new Mailer;
+    my $mail = Mailer->new;
 
     map { $mail->{$_} = $self->{$_} }
       qw(cc bcc subject message version format);
     $mail->{to} = $self->{EMAIL_RECIPIENT} ? $self->{EMAIL_RECIPIENT} : $self->{email};
     $mail->{from}   = qq|"$myconfig->{name}" <$myconfig->{email}>|;
     $mail->{fileid} = time() . '.' . $$ . '.';
-    $myconfig->{signature} =~ s/\r//g;
+    my $full_signature     =  $self->create_email_signature();
+    $full_signature        =~ s/\r//g;
 
     # if we send html or plain text inline
     if (($self->{format} eq 'html') && ($self->{sendmode} eq 'inline')) {
       $mail->{contenttype}    =  "text/html";
       $mail->{message}        =~ s/\r//g;
       $mail->{message}        =~ s/\n/<br>\n/g;
-      $myconfig->{signature}  =~ s/\n/<br>\n/g;
-      $mail->{message}       .=  "<br>\n-- <br>\n$myconfig->{signature}\n<br>";
+      $full_signature         =~ s/\n/<br>\n/g;
+      $mail->{message}       .=  $full_signature;
 
-      open(IN, "<", $self->{tmpfile})
+      open(IN, "<:encoding(UTF-8)", $self->{tmpfile})
         or $self->error($self->cleanup . "$self->{tmpfile} : $!");
       $mail->{message} .= $_ while <IN>;
       close(IN);
@@ -1129,9 +1173,7 @@ sub parse_template {
                                    "name"     => $attachment_name }];
       }
 
-      $mail->{message}  =~ s/\r//g;
-      $mail->{message} .=  "\n-- \n$myconfig->{signature}";
-
+      $mail->{message} .= $full_signature;
     }
 
     my $err = $mail->send();
@@ -1215,6 +1257,7 @@ sub get_formname_translation {
     sales_delivery_order    => $main::locale->text('Delivery Order'),
     purchase_delivery_order => $main::locale->text('Delivery Order'),
     dunning                 => $main::locale->text('Dunning'),
+    letter                  => $main::locale->text('Letter')
   );
 
   $main::lxdebug->leave_sub();
@@ -1229,8 +1272,13 @@ sub get_number_prefix_for_type {
       (first { $self->{type} eq $_ } qw(invoice credit_note)) ? 'inv'
     : ($self->{type} =~ /_quotation$/)                        ? 'quo'
     : ($self->{type} =~ /_delivery_order$/)                   ? 'do'
+    : ($self->{type} =~ /letter/)                             ? 'letter'
     :                                                           'ord';
 
+  # better default like this?
+  # : ($self->{type} =~ /(sales|purcharse)_order/           :  'ord';
+  # :                                                           'prefix_undefined';
+
   $main::lxdebug->leave_sub();
   return $prefix;
 }
@@ -1266,6 +1314,9 @@ sub generate_attachment_filename {
   } elsif ($attachment_filename && $self->{"${prefix}number"}) {
     $attachment_filename .=  "_" . $self->{"${prefix}number"} . $self->get_extension_for_format();
 
+  } elsif ($attachment_filename) {
+    $attachment_filename .=  $self->get_extension_for_format();
+
   } else {
     $attachment_filename = "";
   }
@@ -1306,7 +1357,7 @@ sub cleanup {
     push @err, $::locale->text('The application "#1" was not found on the system.', $application || 'pdflatex') . ' ' . $::locale->text('Please contact your administrator.');
 
   } elsif (-f "$self->{tmpfile}.err") {
-    open(FH, "$self->{tmpfile}.err");
+    open(FH, "<:encoding(UTF-8)", "$self->{tmpfile}.err");
     @err = <FH>;
     close(FH);
   }
@@ -1670,36 +1721,19 @@ sub get_default_currency {
 }
 
 sub set_payment_options {
-  $main::lxdebug->enter_sub();
-
   my ($self, $myconfig, $transdate) = @_;
 
-  return $main::lxdebug->leave_sub() unless ($self->{payment_id});
-
-  my $dbh = $self->get_standard_dbh($myconfig);
-
-  my $query =
-    qq|SELECT p.terms_netto, p.terms_skonto, p.percent_skonto, p.description_long , p.description | .
-    qq|FROM payment_terms p | .
-    qq|WHERE p.id = ?|;
-
-  ($self->{terms_netto}, $self->{terms_skonto}, $self->{percent_skonto},
-   $self->{payment_terms}, $self->{payment_description}) =
-     selectrow_query($self, $dbh, $query, $self->{payment_id});
+  my $terms = $self->{payment_id} ? SL::DB::PaymentTerm->new(id => $self->{payment_id})->load : undef;
+  return if !$terms;
 
-  if ($transdate eq "") {
-    if ($self->{invdate}) {
-      $transdate = $self->{invdate};
-    } else {
-      $transdate = $self->{transdate};
-    }
-  }
+  $transdate                  ||= $self->{invdate} || $self->{transdate};
+  my $due_date                  = $self->{duedate} || $self->{reqdate};
 
-  $query =
-    qq|SELECT ?::date + ?::integer AS netto_date, ?::date + ?::integer AS skonto_date | .
-    qq|FROM payment_terms|;
-  ($self->{netto_date}, $self->{skonto_date}) =
-    selectrow_query($self, $dbh, $query, $transdate, $self->{terms_netto}, $transdate, $self->{terms_skonto});
+  $self->{$_}                   = $terms->$_ for qw(terms_netto terms_skonto percent_skonto);
+  $self->{payment_terms}        = $terms->description_long;
+  $self->{payment_description}  = $terms->description;
+  $self->{netto_date}           = $terms->calc_date(reference_date => $transdate, due_date => $due_date, terms => 'net')->to_kivitendo;
+  $self->{skonto_date}          = $terms->calc_date(reference_date => $transdate, due_date => $due_date, terms => 'discount')->to_kivitendo;
 
   my ($invtotal, $total);
   my (%amounts, %formatted_amounts);
@@ -1729,7 +1763,8 @@ sub set_payment_options {
   }
 
   if ($self->{"language_id"}) {
-    $query =
+    my $dbh   = $self->get_standard_dbh($myconfig);
+    my $query =
       qq|SELECT t.translation, l.output_numberformat, l.output_dateformat, l.output_longdates | .
       qq|FROM generic_translations t | .
       qq|LEFT JOIN language l ON t.language_id = l.id | .
@@ -1768,13 +1803,15 @@ sub set_payment_options {
   $self->{payment_terms} =~ s/<%account_number%>/$self->{account_number}/g;
   $self->{payment_terms} =~ s/<%bank%>/$self->{bank}/g;
   $self->{payment_terms} =~ s/<%bank_code%>/$self->{bank_code}/g;
+  $self->{payment_terms} =~ s/<\%bic\%>/$self->{bic}/g;
+  $self->{payment_terms} =~ s/<\%iban\%>/$self->{iban}/g;
+  $self->{payment_terms} =~ s/<\%mandate_date_of_signature\%>/$self->{mandate_date_of_signature}/g;
+  $self->{payment_terms} =~ s/<\%mandator_id\%>/$self->{mandator_id}/g;
 
   map { $self->{payment_terms} =~ s/<%${_}%>/$formatted_amounts{$_}/g; } keys %formatted_amounts;
 
   $self->{skonto_in_percent} = $formatted_amounts{skonto_in_percent};
 
-  $main::lxdebug->leave_sub();
-
 }
 
 sub get_template_language {
@@ -1926,35 +1963,25 @@ sub get_employee_data {
   my $myconfig = \%main::myconfig;
   my $dbh      = $params{dbh} || $self->get_standard_dbh($myconfig);
 
-  my ($login)  = selectrow_query($self, $dbh, qq|SELECT login FROM employee WHERE id = ?|, conv_i($params{id}));
+  my ($login, $deleted)  = selectrow_query($self, $dbh, qq|SELECT login,deleted FROM employee WHERE id = ?|, conv_i($params{id}));
 
   if ($login) {
-    my $user = User->new(login => $login);
-    $self->{$params{prefix} . "_${_}"}    = $user->{$_}   for qw(email fax name signature tel);
-    $self->{$params{prefix} . "_${_}"}    = $defaults->$_ for qw(address businessnumber co_ustid company duns taxnumber);
-
+    # login already fetched and still the same client (mandant) | same for both cases (delete|!delete)
     $self->{$params{prefix} . '_login'}   = $login;
-    $self->{$params{prefix} . '_name'}  ||= $login;
-  }
-
-  $main::lxdebug->leave_sub();
-}
-
-sub get_duedate {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $reference_date) = @_;
-
-  my $terms   = $self->{payment_id}  ? SL::DB::PaymentTerm->new(id => $self->{payment_id}) ->load
-              : $self->{customer_id} ? SL::DB::Customer   ->new(id => $self->{customer_id})->load->payment
-              : $self->{vendor_id}   ? SL::DB::Vendor     ->new(id => $self->{vendor_id})  ->load->payment
-              :                        croak("Missing field in \$::form: payment_id, customer_id or vendor_id");
-
-  my $duedate = $terms ? $terms->calc_date(reference_date => $reference_date)->to_kivitendo : undef;
+    $self->{$params{prefix} . "_${_}"}    = $defaults->$_ for qw(address businessnumber co_ustid company duns taxnumber);
 
+    if (!$deleted) {
+      # get employee data from auth.user_config
+      my $user = User->new(login => $login);
+      $self->{$params{prefix} . "_${_}"} = $user->{$_} for qw(email fax name signature tel);
+    } else {
+      # get saved employee data from employee
+      my $employee = SL::DB::Manager::Employee->find_by(id => conv_i($params{id}));
+      $self->{$params{prefix} . "_${_}"} = $employee->{"deleted_$_"} for qw(email fax signature tel);
+      $self->{$params{prefix} . "_name"} = $employee->name;
+    }
+ }
   $main::lxdebug->leave_sub();
-
-  return $duedate;
 }
 
 sub _get_contacts {
@@ -2126,8 +2153,10 @@ sub _get_taxzones {
   my ($self, $dbh, $key) = @_;
 
   $key = "all_taxzones" unless ($key);
+  my $tzfilter = "";
+  $tzfilter = "WHERE obsolete is FALSE" if $key eq 'ALL_ACTIVE_TAXZONES';
 
-  my $query = qq|SELECT * FROM tax_zones ORDER BY id|;
+  my $query = qq|SELECT * FROM tax_zones $tzfilter ORDER BY sortkey|;
 
   $self->{$key} = selectall_hashref_query($self, $dbh, $query);
 
@@ -2354,8 +2383,13 @@ sub get_lists {
   my $dbh = $self->get_standard_dbh(\%main::myconfig);
   my ($sth, $query, $ref);
 
-  my $vc = $self->{"vc"} eq "customer" ? "customer" : "vendor";
-  my $vc_id = $self->{"${vc}_id"};
+  my ($vc, $vc_id);
+  if ($params{contacts} || $params{shipto}) {
+    $vc = 'customer' if $self->{"vc"} eq "customer";
+    $vc = 'vendor'   if $self->{"vc"} eq "vendor";
+    die "invalid use of get_lists, need 'vc'" unless $vc;
+    $vc_id = $self->{"${vc}_id"};
+  }
 
   if ($params{"contacts"}) {
     $self->_get_contacts($dbh, $vc_id, $params{"contacts"});
@@ -2396,7 +2430,7 @@ sub get_lists {
   }
 
   if ($params{"salesmen"}) {
-    $self->_get_employees($dbh, "all_salesmen", $params{"salesmen"});
+    $self->_get_employees($dbh, $params{"salesmen"});
   }
 
   if ($params{"business_types"}) {
@@ -2571,6 +2605,25 @@ sub all_vc {
   $main::lxdebug->leave_sub();
 }
 
+sub mtime_ischanged {
+  my ($self, $table, $option) = @_;
+
+  return                                       unless $self->{id};
+  croak ("wrong call, no valid table defined") unless $table =~ /^(oe|ar|ap|delivery_orders|parts)$/;
+
+  my $query       = "SELECT mtime, itime FROM " . $table . " WHERE id = ?";
+  my $ref         = selectfirst_hashref_query($self, $self->get_standard_dbh, $query, $self->{id});
+  $ref->{mtime} ||= $ref->{itime};
+
+  if ($self->{lastmtime} && $self->{lastmtime} ne $ref->{mtime} ) {
+      $self->error(($option eq 'mail') ?
+        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") :
+        t8("The document has been changed by another user. Please reopen it in another window and copy the changes to the new window")
+      );
+    ::end_of_request();
+  }
+}
+
 sub language_payment {
   $main::lxdebug->enter_sub();
 
@@ -2722,6 +2775,7 @@ sub create_links {
       qq|SELECT
            a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid,
            a.duedate, a.ordnumber, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.notes,
+           a.mtime, a.itime,
            a.intnotes, a.department_id, a.amount AS oldinvtotal,
            a.paid AS oldtotalpaid, a.employee_id, a.gldate, a.type,
            a.globalproject_id, ${extra_columns}
@@ -2738,7 +2792,8 @@ sub create_links {
     foreach my $key (keys %$ref) {
       $self->{$key} = $ref->{$key};
     }
-
+    $self->{mtime}   ||= $self->{itime};
+    $self->{lastmtime} = $self->{mtime};
     my $transdate = "current_date";
     if ($self->{transdate}) {
       $transdate = $dbh->quote($self->{transdate});
@@ -2872,7 +2927,6 @@ sub lastname_used {
                     "d.description"           => "department",
                     "ct.name"                 => $table,
                     "cu.name"                 => "currency",
-                    "current_date + ct.terms" => "duedate",
     );
 
   if ($self->{type} =~ /delivery_order/) {
@@ -3173,81 +3227,6 @@ sub get_history {
   return 0;
 }
 
-sub update_defaults {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $fld, $provided_dbh) = @_;
-
-  my $dbh;
-  if ($provided_dbh) {
-    $dbh = $provided_dbh;
-  } else {
-    $dbh = $self->dbconnect_noauto($myconfig);
-  }
-  my $query = qq|SELECT $fld FROM defaults FOR UPDATE|;
-  my $sth   = $dbh->prepare($query);
-
-  $sth->execute || $self->dberror($query);
-  my ($var) = $sth->fetchrow_array;
-  $sth->finish;
-
-  $var   = 0 if !defined($var) || ($var eq '');
-  $var   = SL::PrefixedNumber->new(number => $var)->get_next;
-  $query = qq|UPDATE defaults SET $fld = ?|;
-  do_query($self, $dbh, $query, $var);
-
-  if (!$provided_dbh) {
-    $dbh->commit;
-    $dbh->disconnect;
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return $var;
-}
-
-sub update_business {
-  $main::lxdebug->enter_sub();
-
-  my ($self, $myconfig, $business_id, $provided_dbh) = @_;
-
-  my $dbh;
-  if ($provided_dbh) {
-    $dbh = $provided_dbh;
-  } else {
-    $dbh = $self->dbconnect_noauto($myconfig);
-  }
-  my $query =
-    qq|SELECT customernumberinit FROM business
-       WHERE id = ? FOR UPDATE|;
-  my ($var) = selectrow_query($self, $dbh, $query, $business_id);
-
-  return undef unless $var;
-
-  if ($var =~ m/\d+$/) {
-    my $new_var  = (substr $var, $-[0]) * 1 + 1;
-    my $len_diff = length($var) - $-[0] - length($new_var);
-    $var         = substr($var, 0, $-[0]) . ($len_diff > 0 ? '0' x $len_diff : '') . $new_var;
-
-  } else {
-    $var = $var . '1';
-  }
-
-  $query = qq|UPDATE business
-              SET customernumberinit = ?
-              WHERE id = ?|;
-  do_query($self, $dbh, $query, $var, $business_id);
-
-  if (!$provided_dbh) {
-    $dbh->commit;
-    $dbh->disconnect;
-  }
-
-  $main::lxdebug->leave_sub();
-
-  return $var;
-}
-
 sub get_partsgroup {
   $main::lxdebug->enter_sub();
 
@@ -3394,11 +3373,17 @@ sub prepare_for_printing {
   # compatibility.
   $self->{$_} = $defaults->$_ for qw(company address taxnumber co_ustid duns sepa_creditor_id);
 
-  # set shipto from billto unless set
-  my $has_shipto = any { $self->{"shipto$_"} } qw(name street zipcode city country contact);
-  if (!$has_shipto && ($self->{type} =~ m/^(?:purchase_order|request_quotation)$/)) {
-    $self->{shiptoname}   = $defaults->company;
-    $self->{shiptostreet} = $defaults->address;
+  $self->{"myconfig_${_}"} = $::myconfig{$_} for grep { $_ ne 'dbpasswd' } keys %::myconfig;
+
+  if (!$self->{employee_id}) {
+    $self->{"employee_${_}"} = $::myconfig{$_} for qw(email tel fax name signature);
+    $self->{"employee_${_}"} = $defaults->$_   for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
+  }
+
+  # Load shipping address from database if shipto_id is set.
+  if ($self->{shipto_id}) {
+    my $shipto  = SL::DB::Shipto->new(shipto_id => $self->{shipto_id})->load;
+    $self->{$_} = $shipto->$_ for grep { m{^shipto} } map { $_->name } @{ $shipto->meta->columns };
   }
 
   my $language = $self->{language} ? '_' . $self->{language} : '';
@@ -3406,12 +3391,16 @@ sub prepare_for_printing {
   my ($language_tc, $output_numberformat, $output_dateformat, $output_longdates);
   if ($self->{language_id}) {
     ($language_tc, $output_numberformat, $output_dateformat, $output_longdates) = AM->get_language_details(\%::myconfig, $self, $self->{language_id});
-  } else {
-    $output_dateformat   = $::myconfig{dateformat};
-    $output_numberformat = $::myconfig{numberformat};
-    $output_longdates    = 1;
   }
 
+  $output_dateformat   ||= $::myconfig{dateformat};
+  $output_numberformat ||= $::myconfig{numberformat};
+  $output_longdates    //= 1;
+
+  $self->{myconfig_output_dateformat}   = $output_dateformat   // $::myconfig{dateformat};
+  $self->{myconfig_output_longdates}    = $output_longdates    // 1;
+  $self->{myconfig_output_numberformat} = $output_numberformat // $::myconfig{numberformat};
+
   # Retrieve accounts for tax calculation.
   IC->retrieve_accounts(\%::myconfig, $self, map { $_ => $self->{"id_$_"} } 1 .. $self->{rowcount});
 
@@ -3465,9 +3454,95 @@ sub prepare_for_printing {
     $self->reformat_numbers($output_numberformat, $precision, @{ $field_list });
   }
 
+  $self->{template_meta} = {
+    formname  => $self->{formname},
+    language  => SL::DB::Manager::Language->find_by_or_create(id => $self->{language_id} || undef),
+    format    => $self->{format},
+    media     => $self->{media},
+    extension => $extension,
+    printer   => SL::DB::Manager::Printer->find_by_or_create(id => $self->{printer_id} || undef),
+    today     => DateTime->today,
+  };
+
   return $self;
 }
 
+sub calculate_arap {
+  my ($self,$buysell,$taxincluded,$exchangerate,$roundplaces) = @_;
+
+  # this function is used to calculate netamount, total_tax and amount for AP and
+  # AR transactions (Kreditoren-/Debitorenbuchungen) by going over all lines
+  # (1..$rowcount)
+  # Thus it needs a fully prepared $form to work on.
+  # calculate_arap assumes $form->{amount_$i} entries still need to be parsed
+
+  # The calculated total values are all rounded (default is to 2 places) and
+  # returned as parameters rather than directly modifying form.  The aim is to
+  # make the calculation of AP and AR behave identically.  There is a test-case
+  # for this function in t/form/arap.t
+
+  # While calculating the totals $form->{amount_$i} and $form->{tax_$i} are
+  # modified and formatted and receive the correct sign for writing straight to
+  # acc_trans, depending on whether they are ar or ap.
+
+  # check parameters
+  die "taxincluded needed in Form->calculate_arap" unless defined $taxincluded;
+  die "exchangerate needed in Form->calculate_arap" unless defined $exchangerate;
+  die 'illegal buysell parameter, has to be \"buy\" or \"sell\" in Form->calculate_arap\n' unless $buysell =~ /^(buy|sell)$/;
+  $roundplaces = 2 unless $roundplaces;
+
+  my $sign = 1;  # adjust final results for writing amount to acc_trans
+  $sign = -1 if $buysell eq 'buy';
+
+  my ($netamount,$total_tax,$amount);
+
+  my $tax;
+
+  # parse and round amounts, setting correct sign for writing to acc_trans
+  for my $i (1 .. $self->{rowcount}) {
+    $self->{"amount_$i"} = $self->round_amount($self->parse_amount(\%::myconfig, $self->{"amount_$i"}) * $exchangerate * $sign, $roundplaces);
+
+    $amount += $self->{"amount_$i"} * $sign;
+  }
+
+  for my $i (1 .. $self->{rowcount}) {
+    next unless $self->{"amount_$i"};
+    ($self->{"tax_id_$i"}) = split /--/, $self->{"taxchart_$i"};
+    my $tax_id = $self->{"tax_id_$i"};
+
+    my $selected_tax = SL::DB::Manager::Tax->find_by(id => "$tax_id");
+
+    if ( $selected_tax ) {
+
+      if ( $buysell eq 'sell' ) {
+        $self->{AR_amounts}{"tax_$i"} = $selected_tax->chart->accno if defined $selected_tax->chart;
+      } else {
+        $self->{AP_amounts}{"tax_$i"} = $selected_tax->chart->accno if defined $selected_tax->chart;
+      };
+
+      $self->{"taxkey_$i"} = $selected_tax->taxkey;
+      $self->{"taxrate_$i"} = $selected_tax->rate;
+    };
+
+    ($self->{"amount_$i"}, $self->{"tax_$i"}) = $self->calculate_tax($self->{"amount_$i"},$self->{"taxrate_$i"},$taxincluded,$roundplaces);
+
+    $netamount  += $self->{"amount_$i"};
+    $total_tax  += $self->{"tax_$i"};
+
+  }
+  $amount = $netamount + $total_tax;
+
+  # due to $sign amount_$i und tax_$i already have the right sign for acc_trans
+  # but reverse sign of totals for writing amounts to ar
+  if ( $buysell eq 'buy' ) {
+    $netamount *= -1;
+    $amount    *= -1;
+    $total_tax *= -1;
+  };
+
+  return($netamount,$total_tax,$amount);
+}
+
 sub format_dates {
   my ($self, $dateformat, $longformat, @indices) = @_;
 
@@ -3542,6 +3617,21 @@ sub reformat_numbers {
   $::myconfig{numberformat} = $saved_numberformat;
 }
 
+sub create_email_signature {
+
+  my $client_signature = $::instance_conf->get_signature;
+  my $user_signature   = $::myconfig{signature};
+
+  my $signature = '';
+  if ( $client_signature or $user_signature ) {
+    $signature  = "\n\n-- \n";
+    $signature .= $user_signature   . "\n" if $user_signature;
+    $signature .= $client_signature . "\n" if $client_signature;
+  };
+  return $signature;
+
+};
+
 sub layout {
   my ($self) = @_;
   $::lxdebug->enter_sub;
@@ -3565,6 +3655,39 @@ sub layout {
   return $layout;
 }
 
+sub calculate_tax {
+  # this function calculates the net amount and tax for the lines in ar, ap and
+  # gl and is used for update as well as post. When used with update the return
+  # value of amount isn't needed
+
+  # calculate_tax should always work with positive values, or rather as the user inputs them
+  # calculate_tax uses db/perl numberformat, i.e. parsed numbers
+  # convert to negative numbers (when necessary) only when writing to acc_trans
+  # the amount from $form for ap/ar/gl is currently always rounded to 2 decimals before it reaches here
+  # for post_transaction amount already contains exchangerate and correct sign and is rounded
+  # calculate_tax doesn't (need to) know anything about exchangerate
+
+  my ($self,$amount,$taxrate,$taxincluded,$roundplaces) = @_;
+
+  $roundplaces = 2 unless defined $roundplaces;
+
+  my $tax;
+
+  if ($taxincluded *= 1) {
+    # calculate tax (unrounded), subtract from amount, round amount and round tax
+    $tax       = $amount - ($amount / ($taxrate + 1)); # equivalent to: taxrate * amount / (taxrate + 1)
+    $amount    = $self->round_amount($amount - $tax, $roundplaces);
+    $tax       = $self->round_amount($tax, $roundplaces);
+  } else {
+    $tax       = $amount * $taxrate;
+    $tax       = $self->round_amount($tax, $roundplaces);
+  }
+
+  $tax = 0 unless $tax;
+
+  return ($amount,$tax);
+};
+
 1;
 
 __END__
@@ -3584,18 +3707,6 @@ Points of interest for a beginner are:
 
 =head1 SPECIAL FUNCTIONS
 
-=head2 C<update_business> PARAMS
-
-PARAMS (not named):
- \%config,     - config hashref
- $business_id, - business id
- $dbh          - optional database handle
-
-handles business (thats customer/vendor types) sequences.
-
-special behaviour for empty strings in customerinitnumber field:
-will in this case not increase the value, and return undef.
-
 =head2 C<redirect_header> $url
 
 Generates a HTTP redirection header for the new C<$url>. Constructs an
@@ -3648,6 +3759,17 @@ Used to override the default favicon.
 
 A html page title will be generated from this
 
+=item mtime_ischanged
+
+Tries to avoid concurrent write operations to records by checking the database mtime with a fetched one.
+
+Can be used / called with any table, that has itime and mtime attributes.
+Valid C<table> names are: oe, ar, ap, delivery_orders, parts.
+Can be called wit C<option> mail to generate a different error message.
+
+Returns undef if no save operation has been done yet ($self->{id} not present).
+Returns undef if no concurrent write process is detected otherwise a error message.
+
 =back
 
 =cut