Auftrags-Controller: Webdav
[kivitendo-erp.git] / SL / Controller / Order.pm
index 7728f67..9310f91 100644 (file)
@@ -4,8 +4,12 @@ use strict;
 use parent qw(SL::Controller::Base);
 
 use SL::Helper::Flash;
-use SL::ClientJS;
 use SL::Presenter;
+use SL::Locale::String;
+use SL::SessionFile::Random;
+use SL::PriceSource;
+use SL::Form;
+use SL::Webdav;
 
 use SL::DB::Order;
 use SL::DB::Customer;
@@ -15,15 +19,19 @@ use SL::DB::Employee;
 use SL::DB::Project;
 use SL::DB::Default;
 use SL::DB::Unit;
+use SL::DB::Price;
 
 use SL::Helper::DateTime;
+use SL::Helper::CreatePDF qw(:all);
 
 use List::Util qw(max first);
-use List::MoreUtils qw(none pairwise);
+use List::MoreUtils qw(none pairwise first_index);
+use English qw(-no_match_vars);
+use File::Spec;
 
 use Rose::Object::MakeMethods::Generic
 (
- 'scalar --get_set_init' => [ qw(order valid_types type cv js p) ],
+ 'scalar --get_set_init' => [ qw(order valid_types type cv p) ],
 );
 
 
@@ -31,8 +39,10 @@ use Rose::Object::MakeMethods::Generic
 __PACKAGE__->run_before('_check_auth');
 
 __PACKAGE__->run_before('_recalc',
-                        only => [ qw(edit update save) ]);
+                        only => [ qw(edit update save save_and_delivery_order create_pdf send_email) ]);
 
+__PACKAGE__->run_before('_get_unalterable_data',
+                        only => [ qw(save save_and_delivery_order create_pdf send_email) ]);
 
 #
 # actions
@@ -42,6 +52,7 @@ sub action_add {
   my ($self) = @_;
 
   $self->order->transdate(DateTime->now_local());
+  $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
 
   $self->_pre_render();
   $self->render(
@@ -86,7 +97,7 @@ sub action_save {
 
   if (scalar @{ $errors }) {
     $self->js->flash('error', $_) foreach @{ $errors };
-    return $self->js->render($self);
+    return $self->js->render();
   }
 
   flash_later('info', $::locale->text('The order has been saved'));
@@ -99,34 +110,199 @@ sub action_save {
   $self->redirect_to(@redirect_params);
 }
 
-sub action_customer_vendor_changed {
+sub action_create_pdf {
   my ($self) = @_;
 
-  if ($self->cv eq 'customer') {
-    $self->order->customer(SL::DB::Manager::Customer->find_by_or_create(id => $::form->{cv_id}));
+  my $pdf;
+  my @errors = _create_pdf($self->order, \$pdf);
+  if (scalar @errors) {
+    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
+  }
+
+  my $sfile = SL::SessionFile::Random->new(mode => "w");
+  $sfile->fh->print($pdf);
+  $sfile->fh->close;
+
+  my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
+  $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
+
+  my $form = Form->new;
+  $form->{ordnumber} = $self->order->ordnumber;
+  $form->{formname}  = $self->type;
+  $form->{type}      = $self->type;
+  $form->{language}  = 'de';
+  $form->{format}    = 'pdf';
+
+  my $pdf_filename = $form->generate_attachment_filename();
+
+  # copy file to webdav folder
+  if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
+    my $webdav = SL::Webdav->new(
+      type     => $self->type,
+      number   => $self->order->ordnumber,
+    );
+    my $webdav_file = SL::Webdav::File->new(
+      webdav   => $webdav,
+      filename => $pdf_filename,
+    );
+    eval {
+      $webdav_file->store(data => \$pdf);
+      1;
+    } or do {
+      $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
+    }
+  }
 
-  } elsif ($self->cv eq 'vendor') {
-    $self->order->vendor(SL::DB::Manager::Vendor->find_by_or_create(id => $::form->{cv_id}));
+  $self->js
+    ->run('download_pdf', $pdf_filename, $key)
+    ->flash('info', t8('The PDF has been created'))->render($self);
+}
+
+sub action_download_pdf {
+  my ($self) = @_;
+
+  my $key = $::form->{key};
+  my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
+  return $self->send_file(
+    $tmp_filename,
+    type => 'application/pdf',
+    name => $::form->{pdf_filename},
+  );
+}
+
+sub action_show_email_dialog {
+  my ($self) = @_;
+
+  my $cv_method = $self->cv;
+
+  if (!$self->order->$cv_method) {
+    return $self->js->flash('error', t8('Cannot send E-mail without ' . $self->cv))
+                    ->render($self);
+  }
+
+  $self->{email}->{to}   = $self->order->contact->cp_email if $self->order->contact;
+  $self->{email}->{to} ||= $self->order->$cv_method->email;
+  $self->{email}->{cc}   = $self->order->$cv_method->cc;
+  $self->{email}->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
+  # Todo: get addresses from shipto, if any
+
+  my $form = Form->new;
+  $form->{ordnumber} = $self->order->ordnumber;
+  $form->{formname}  = $self->type;
+  $form->{type}      = $self->type;
+  $form->{language} = 'de';
+  $form->{format}   = 'pdf';
+
+  $self->{email}->{subject}             = $form->generate_email_subject();
+  $self->{email}->{attachment_filename} = $form->generate_attachment_filename();
+  $self->{email}->{message}             = $form->create_email_signature();
+
+  my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
+  $self->js
+      ->run('show_email_dialog', $dialog_html)
+      ->reinit_widgets
+      ->render($self);
+}
+
+# Todo: handling error messages: flash is not displayed in dialog, but in the main form
+sub action_send_email {
+  my ($self) = @_;
+
+  my $mail      = Mailer->new;
+  $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
+  $mail->{$_}   = $::form->{email}->{$_} for qw(to cc bcc subject message);
+
+  my $pdf;
+  my @errors = _create_pdf($self->order, \$pdf, {media => 'email'});
+  if (scalar @errors) {
+    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
+  }
+
+  $mail->{attachments} = [{ "content" => $pdf,
+                            "name"    => $::form->{email}->{attachment_filename} }];
+
+  if (my $err = $mail->send) {
+    return $self->js->flash('error', t8('Sending E-mail: ') . $err)
+                    ->render($self);
+  }
+
+  # internal notes
+  my $intnotes = $self->order->intnotes;
+  $intnotes   .= "\n\n" if $self->order->intnotes;
+  $intnotes   .= t8('[email]')                                                                                        . "\n";
+  $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
+  $intnotes   .= t8('To (email)') . ": " . $mail->{to}                                                                . "\n";
+  $intnotes   .= t8('Cc')         . ": " . $mail->{cc}                                                                . "\n"    if $mail->{cc};
+  $intnotes   .= t8('Bcc')        . ": " . $mail->{bcc}                                                               . "\n"    if $mail->{bcc};
+  $intnotes   .= t8('Subject')    . ": " . $mail->{subject}                                                           . "\n\n";
+  $intnotes   .= t8('Message')    . ": " . $mail->{message};
+
+  $self->js
+      ->val('#order_intnotes', $intnotes)
+      ->run('close_email_dialog')
+      ->render($self);
+}
+
+sub action_save_and_delivery_order {
+  my ($self) = @_;
+
+  my $errors = $self->_save();
+
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
   }
 
-  if ($self->order->{$self->cv}->contacts && scalar @{ $self->order->{$self->cv}->contacts } > 0) {
+  my $delivery_order = $self->order->convert_to_delivery_order($self->order);
+
+  flash_later('info', $::locale->text('The order has been saved'));
+  my @redirect_params = (
+    controller => 'do.pl',
+    action     => 'edit',
+    type       => $delivery_order->type,
+    id         => $delivery_order->id,
+    vc         => $delivery_order->is_sales ? 'customer' : 'vendor',
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
+sub action_customer_vendor_changed {
+  my ($self) = @_;
+
+  my $cv_method = $self->cv;
+
+  if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
     $self->js->show('#cp_row');
   } else {
     $self->js->hide('#cp_row');
   }
 
-  if ($self->order->{$self->cv}->shipto && scalar @{ $self->order->{$self->cv}->shipto } > 0) {
+  if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
     $self->js->show('#shipto_row');
   } else {
     $self->js->hide('#shipto_row');
   }
 
+  $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
+
+  if ($self->order->is_sales) {
+    $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
+                              ? $self->order->$cv_method->taxincluded_checked
+                              : $::myconfig{taxincluded_checked});
+  }
+
+  $self->_recalc();
+
   $self->js
-    ->replaceWith('#order_cp_id',     $self->build_contact_select)
-    ->replaceWith('#order_shipto_id', $self->build_shipto_select)
-    ->val('#order_taxzone_id', $self->order->{$self->cv}->taxzone_id)
-    ->focus('#order_' . $self->cv . '_id')
-    ->render($self);
+    ->replaceWith('#order_cp_id',       $self->build_contact_select)
+    ->replaceWith('#order_shipto_id',   $self->build_shipto_select)
+    ->val(        '#order_taxzone_id',  $self->order->taxzone_id)
+    ->val(        '#order_taxincluded', $self->order->taxincluded)
+    ->focus(      '#order_' . $self->cv . '_id');
+
+  $self->_js_redisplay_amounts_and_taxes;
+  $self->js->render();
 }
 
 sub action_add_item {
@@ -136,28 +312,7 @@ sub action_add_item {
 
   return unless $form_attr->{parts_id};
 
-  my $item = SL::DB::OrderItem->new;
-  $item->assign_attributes(%$form_attr);
-
-  my $part        = SL::DB::Part->new(id => $form_attr->{parts_id})->load;
-  my $cv_method   = $self->cv;
-  my $cv_discount = $self->order->$cv_method? $self->order->$cv_method->discount : 0.0;
-
-  my %new_attr;
-  $new_attr{part}        = $part;
-  $new_attr{description} = $part->description if ! $item->description;
-  $new_attr{qty}         = 1.0                if ! $item->qty;
-  $new_attr{unit}        = $part->unit;
-  $new_attr{sellprice}   = $part->sellprice   if ! $item->sellprice;
-  $new_attr{discount}    = $cv_discount       if ! $item->discount;
-
-  # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
-  # they cannot be retrieved via custom_variables until the order/orderitem is
-  # saved. Adding empty custom_variables to new orderitem here solves this problem.
-  $new_attr{custom_variables} = [];
-
-  $item->assign_attributes(%new_attr);
-
+  my $item = _make_item($self->order, $form_attr);
   $self->order->add_items($item);
 
   $self->_recalc();
@@ -176,10 +331,57 @@ sub action_add_item {
     ->run('row_table_scroll_down')
     ->run('row_set_keyboard_events_by_id', $item_id)
     ->on('.recalc', 'change', 'recalc_amounts_and_taxes')
+    ->on('.reformat_number', 'change', 'reformat_number')
     ->focus('#add_item_parts_id_name');
 
   $self->_js_redisplay_amounts_and_taxes;
-  $self->js->render($self);
+  $self->js->render();
+}
+
+sub action_show_multi_items_dialog {
+  my ($self) = @_;
+
+  $self->{multi_items}->{parts} = SL::DB::Manager::Part->get_all_sorted(where => [ obsolete => 0 ]);
+  my $dialog_html = $self->render('order/tabs/_multi_items_dialog', { output => 0 });
+
+  $self->js
+    ->run('show_multi_items_dialog', $dialog_html, t8('Add multiple parts'))
+    ->reinit_widgets
+    ->render();
+}
+
+sub action_add_multi_items {
+  my ($self) = @_;
+
+  my @form_attr = grep { $_->{qty} } @{ $::form->{add_multi_items} };
+  return unless scalar @form_attr;
+
+  my @items;
+  foreach my $attr (@form_attr) {
+    push @items, _make_item($self->order, $attr);
+  }
+  $self->order->add_items(@items);
+
+  $self->_recalc();
+
+  foreach my $item (@items) {
+    my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
+    my $row_as_html = $self->p->render('order/tabs/_row', ITEM => $item, ID => $item_id);
+
+    $self->js
+        ->append('#row_table_id', $row_as_html)
+        ->run('row_set_keyboard_events_by_id', $item_id);
+  }
+
+  $self->js
+    ->run('close_multi_items_dialog')
+    ->run('row_table_scroll_down')
+    ->on('.recalc', 'change', 'recalc_amounts_and_taxes')
+    ->on('.reformat_number', 'change', 'reformat_number')
+    ->focus('#add_item_parts_id_name');
+
+  $self->_js_redisplay_amounts_and_taxes;
+  $self->js->render();
 }
 
 sub action_recalc_amounts_and_taxes {
@@ -189,7 +391,16 @@ sub action_recalc_amounts_and_taxes {
 
   $self->_js_redisplay_linetotals;
   $self->_js_redisplay_amounts_and_taxes;
-  $self->js->render($self);
+  $self->js->render();
+}
+
+sub action_price_popup {
+  my ($self) = @_;
+
+  my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
+  my $item = $self->order->items->[$idx];
+
+  $self->render_price_dialog($item);
 }
 
 sub _js_redisplay_linetotals {
@@ -203,6 +414,18 @@ sub _js_redisplay_linetotals {
 sub _js_redisplay_amounts_and_taxes {
   my ($self) = @_;
 
+  if (scalar @{ $self->{taxes} }) {
+    $self->js->show('#taxincluded_row_id');
+  } else {
+    $self->js->hide('#taxincluded_row_id');
+  }
+
+  if ($self->order->taxincluded) {
+    $self->js->hide('#subtotal_row_id');
+  } else {
+    $self->js->show('#subtotal_row_id');
+  }
+
   $self->js
     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
@@ -238,10 +461,6 @@ sub init_cv {
   return $cv;
 }
 
-sub init_js {
-  SL::ClientJS->new;
-}
-
 sub init_p {
   SL::Presenter->get;
 }
@@ -289,13 +508,34 @@ sub build_tax_rows {
   my ($self) = @_;
 
   my $rows_as_html;
-  foreach my $tax (@{ $self->{taxes} }) {
-    $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax);
+  foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
+    $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
   }
   return $rows_as_html;
 }
 
 
+sub render_price_dialog {
+  my ($self, $record_item) = @_;
+
+  my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
+
+  $self->js
+    ->run(
+      'kivi.io.price_chooser_dialog',
+      t8('Available Prices'),
+      $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
+    )
+    ->reinit_widgets;
+
+#   if (@errors) {
+#     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
+#     $self->js->show('#dialog_flash_error');
+#   }
+
+  $self->js->render;
+}
+
 sub _make_order {
   my ($self) = @_;
 
@@ -311,6 +551,57 @@ sub _make_order {
   return $order;
 }
 
+sub _make_item {
+  my ($record, $attr) = @_;
+
+  my $item = SL::DB::OrderItem->new;
+  $item->assign_attributes(%$attr);
+
+  my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
+  my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
+
+  $item->unit($part->unit) if !$item->unit;
+
+  my $price_src;
+  if ($item->sellprice) {
+    $price_src = $price_source->price_from_source("");
+    $price_src->price($item->sellprice);
+  } else {
+    $price_src = $price_source->best_price
+           ? $price_source->best_price
+           : $price_source->price_from_source("");
+    $price_src->price(0) if !$price_source->best_price;
+  }
+
+  my $discount_src;
+  if ($item->discount) {
+    $discount_src = $price_source->discount_from_source("");
+    $discount_src->discount($item->discount);
+  } else {
+    $discount_src = $price_source->best_discount
+                  ? $price_source->best_discount
+                  : $price_source->discount_from_source("");
+    $discount_src->discount(0) if !$price_source->best_discount;
+  }
+
+  my %new_attr;
+  $new_attr{part}                   = $part;
+  $new_attr{description}            = $part->description if ! $item->description;
+  $new_attr{qty}                    = 1.0                if ! $item->qty;
+  $new_attr{sellprice}              = $price_src->price;
+  $new_attr{discount}               = $discount_src->discount;
+  $new_attr{active_price_source}    = $price_src;
+  $new_attr{active_discount_source} = $discount_src;
+
+  # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
+  # they cannot be retrieved via custom_variables until the order/orderitem is
+  # saved. Adding empty custom_variables to new orderitem here solves this problem.
+  $new_attr{custom_variables} = [];
+
+  $item->assign_attributes(%new_attr);
+
+  return $item;
+}
 
 sub _recalc {
   my ($self) = @_;
@@ -319,23 +610,45 @@ sub _recalc {
   $self->order->currency_id($::instance_conf->get_currency_id());
 
   my %pat = $self->order->calculate_prices_and_taxes();
-
+  $self->{taxes} = [];
   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
-    push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
-                                tax    => $tax });
+
+    my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
+    push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
+                                netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
+                                tax       => $tax });
   }
 
   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
 }
 
-sub _save {
+
+sub _get_unalterable_data {
   my ($self) = @_;
 
-  # autovivify all cvars that are not in the form (cvars_by_config can do it)
   foreach my $item (@{ $self->order->items }) {
-    $item->cvars_by_config;
+    if ($item->id) {
+      # load data from orderitems (db)
+      my $db_item = SL::DB::OrderItem->new(id => $item->id)->load;
+      $item->$_($db_item->$_) for qw(longdescription);
+    } else {
+      # set data from part (or other sources)
+      $item->longdescription($item->part->notes);
+    }
+
+    # autovivify all cvars that are not in the form (cvars_by_config can do it).
+    # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
+    foreach my $var (@{ $item->cvars_by_config }) {
+      $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
+    }
+    $item->parse_custom_variable_values;
   }
+}
+
+
+sub _save {
+  my ($self) = @_;
 
   my $errors = [];
   my $db = $self->order->db;
@@ -353,9 +666,13 @@ sub _pre_render {
   my ($self) = @_;
 
   $self->{all_taxzones}        = SL::DB::Manager::TaxZone->get_all_sorted();
+  $self->{all_departments}     = SL::DB::Manager::Department->get_all_sorted();
   $self->{all_employees}       = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
                                                                                        deleted => 0 ] ],
                                                                     sort_by => 'name');
+  $self->{all_salesmen}        = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
+                                                                                       deleted => 0 ] ],
+                                                                    sort_by => 'name');
   $self->{all_projects}        = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
                                                                                       active => 1 ] ],
                                                                    sort_by => 'projectnumber');
@@ -364,9 +681,63 @@ sub _pre_render {
 
   $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
 
+  foreach  my $item (@{$self->order->items}) {
+    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
+    $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
+    $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
+
+  }
+
+  if ($self->order->ordnumber && $::instance_conf->get_webdav) {
+    my $webdav = SL::Webdav->new(
+      type     => $self->type,
+      number   => $self->order->ordnumber,
+    );
+    my $webdav_path = $webdav->webdav_path;
+    my @all_objects = $webdav->get_all_objects;
+    @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
+                                                    type => t8('File'),
+                                                    link => File::Spec->catdir($webdav_path, $_->filename),
+                                                } } @all_objects;
+  }
+
   $::request->{layout}->use_javascript("${_}.js")  for qw(ckeditor/ckeditor ckeditor/adapters/jquery);
 }
 
+sub _create_pdf {
+  my ($order, $pdf_ref, $params) = @_;
+
+  my $print_form = Form->new('');
+  $print_form->{type}     = $order->type;
+  $print_form->{formname} = $order->type;
+  $print_form->{format}   = $params->{format} || 'pdf',
+  $print_form->{media}    = $params->{media}  || 'file';
+
+  $order->flatten_to_form($print_form, format_amounts => 1);
+  # flatten_to_form sets payment_terms from customer/vendor - we do not want that here
+  delete $print_form->{payment_terms} if !$print_form->{payment_id};
+
+  my @errors = ();
+  $print_form->throw_on_error(sub {
+    eval {
+      $print_form->prepare_for_printing;
+
+      $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
+        template  => SL::Helper::CreatePDF->find_template(name => $print_form->{formname}),
+        variables => $print_form,
+        variable_content_types => {
+          longdescription => 'html',
+          partnotes       => 'html',
+          notes           => 'html',
+        },
+      );
+      1;
+    } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
+  });
+
+  return @errors;
+}
+
 sub _sales_order_type {
   'sales_order';
 }
@@ -376,3 +747,22 @@ sub _purchase_order_type {
 }
 
 1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Order - controller for orders
+
+=head1 TODO
+
+Testing, PriceSources, pricefactor, units, currency, delivered, delivery order created, ...
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
+