Merge branch 'f-cvar-htmlfield'
authorMoritz Bunkus <m.bunkus@linet.de>
Fri, 17 Dec 2021 14:13:09 +0000 (15:13 +0100)
committerMoritz Bunkus <m.bunkus@linet.de>
Fri, 17 Dec 2021 14:13:09 +0000 (15:13 +0100)
34 files changed:
SL/Controller/CsvImport.pm
SL/Controller/CsvImport/AdditionalBillingAddress.pm [new file with mode: 0644]
SL/Controller/File.pm
SL/Controller/Order.pm
SL/Controller/Part.pm
SL/Controller/PayPostingImport.pm
SL/DB/AdditionalBillingAddress.pm
SL/DB/Employee.pm
SL/DB/Invoice.pm
SL/File/Backend/Webdav.pm
SL/Form.pm
SL/Presenter/Tag.pm
SL/RecordLinks.pm
SL/Template.pm
bin/mozilla/io.pl
bin/mozilla/is.pl
js/kivi.Order.js
js/kivi.Validator.js
js/locale/de.js
js/locale/en.js
locale/de/all
locale/en/all
menus/user/00-erp.yaml
t/run.sh
templates/print/marei/kiviletter.sty
templates/webpages/ca/list.html
templates/webpages/common/_send_email_dialog.html
templates/webpages/csv_import/form.html
templates/webpages/ct/search.html
templates/webpages/datev/export.html
templates/webpages/datev/export_bewegungsdaten.html
templates/webpages/datev/net_gross_difference.html
templates/webpages/oe/edit_periodic_invoices_config.html
templates/webpages/part/_basic_data.html

index 06f4bc2..d1ff719 100644 (file)
@@ -13,6 +13,7 @@ use SL::Helper::Flash;
 use SL::Locale::String;
 use SL::SessionFile;
 use SL::SessionFile::Random;
+use SL::Controller::CsvImport::AdditionalBillingAddress;
 use SL::Controller::CsvImport::Contact;
 use SL::Controller::CsvImport::CustomerVendor;
 use SL::Controller::CsvImport::Part;
@@ -307,7 +308,7 @@ sub check_auth {
 sub check_type {
   my ($self) = @_;
 
-  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders delivery_orders bank_transactions ar_transactions);
+  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors billing_addresses addresses contacts projects orders delivery_orders bank_transactions ar_transactions);
   $self->type($::form->{profile}->{type});
 }
 
@@ -348,6 +349,7 @@ sub render_inputs {
   }
 
   my $title = $self->type eq 'customers_vendors' ? $::locale->text('CSV import: customers and vendors')
+            : $self->type eq 'billing_addresses' ? $::locale->text('CSV import: additional billing addresses')
             : $self->type eq 'addresses'         ? $::locale->text('CSV import: shipping addresses')
             : $self->type eq 'contacts'          ? $::locale->text('CSV import: contacts')
             : $self->type eq 'parts'             ? $::locale->text('CSV import: parts and services')
@@ -720,6 +722,7 @@ sub init_worker {
 
   return $self->{type} eq 'customers_vendors' ? SL::Controller::CsvImport::CustomerVendor->new(@args)
        : $self->{type} eq 'contacts'          ? SL::Controller::CsvImport::Contact->new(@args)
+       : $self->{type} eq 'billing_addresses' ? SL::Controller::CsvImport::AdditionalBillingAddress->new(@args)
        : $self->{type} eq 'addresses'         ? SL::Controller::CsvImport::Shipto->new(@args)
        : $self->{type} eq 'parts'             ? SL::Controller::CsvImport::Part->new(@args)
        : $self->{type} eq 'inventories'       ? SL::Controller::CsvImport::Inventory->new(@args)
diff --git a/SL/Controller/CsvImport/AdditionalBillingAddress.pm b/SL/Controller/CsvImport/AdditionalBillingAddress.pm
new file mode 100644 (file)
index 0000000..6d2d8ea
--- /dev/null
@@ -0,0 +1,90 @@
+package SL::Controller::CsvImport::AdditionalBillingAddress;
+
+use strict;
+
+use SL::Helper::Csv;
+
+use parent qw(SL::Controller::CsvImport::Base);
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(table) ],
+);
+
+sub set_profile_defaults {
+};
+
+sub init_class {
+  my ($self) = @_;
+  $self->class('SL::DB::AdditionalBillingAddress');
+}
+
+sub _hash_object {
+  my ($o) = @_;
+  return join('--', map({ s/[\s,\.\-]//g; $_ } ($o->name, $o->street)));
+}
+
+sub check_objects {
+  my ($self) = @_;
+
+  $self->controller->track_progress(phase => 'building data', progress => 0);
+
+  my %existing_by_id_name_street = map { (_hash_object($_) => $_) } @{ $self->existing_objects };
+  my $methods                    = $self->controller->headers->{methods};
+
+  my $i = 0;
+  my $num_data = scalar @{ $self->controller->data };
+  foreach my $entry (@{ $self->controller->data }) {
+    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
+
+    $self->check_vc($entry, 'customer_id');
+
+    next if @{ $entry->{errors} };
+
+    my $object   = $entry->{object};
+    my $idx      = _hash_object($object);
+    my $existing = $existing_by_id_name_street{$idx};
+
+    if (!$existing) {
+      $existing_by_id_name_street{$idx} = $object;
+    } else {
+      $entry->{object_to_save} = $existing;
+
+      $existing->$_( $object->$_ ) for @{ $methods };
+
+      push @{ $entry->{information} }, $::locale->text('Updating existing entry in database');
+    }
+
+  } continue {
+    $i++;
+  }
+
+  $self->add_info_columns({ header => $::locale->text('Customer/Vendor'), method => 'vc_name' });
+}
+
+sub setup_displayable_columns {
+  my ($self) = @_;
+
+  $self->SUPER::setup_displayable_columns;
+
+  $self->add_displayable_columns(
+    { name => 'default_address', description => $::locale->text('Default address flag') },
+    { name => 'name',            description => $::locale->text('Name')                 },
+    { name => 'department_1',    description => $::locale->text('Department 1')         },
+    { name => 'department_2',    description => $::locale->text('Department 2')         },
+    { name => 'street',          description => $::locale->text('Street')               },
+    { name => 'zipcode',         description => $::locale->text('Zipcode')              },
+    { name => 'city',            description => $::locale->text('City')                 },
+    { name => 'country',         description => $::locale->text('Country')              },
+    { name => 'contact',         description => $::locale->text('Contact')              },
+    { name => 'email',           description => $::locale->text('E-mail')               },
+    { name => 'fax',             description => $::locale->text('Fax')                  },
+    { name => 'gln',             description => $::locale->text('GLN')                  },
+    { name => 'phone',           description => $::locale->text('Phone')                },
+    { name => 'customer_id',     description => $::locale->text('Customer')             },
+    { name => 'customer',        description => $::locale->text('Customer (name)')      },
+    { name => 'customernumber',  description => $::locale->text('Customer Number')      },
+  );
+}
+
+1;
index 9005d79..7cd9406 100644 (file)
@@ -679,7 +679,12 @@ sub _convert_pdf_to_png {
 
   my $size    = $params{size} // 64;
   my $sfile   = SL::SessionFile::Random->new();
-  my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . $filename . ' ' . $sfile->file_name;
+  unless (-f $filename) {
+    $::lxdebug->message(LXDebug::WARN(), "_convert_pdf_to_png failed, no file found: $filename");
+    return;
+  }
+  # quotemeta for storno case "storno\ zu\ 1020" *nix only
+  my $command = 'pdftoppm -singlefile -scale-to ' . $size . ' -png' . ' ' . quotemeta($filename) . ' ' . $sfile->file_name;
 
   if (system($command) == -1) {
     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: system call failed: " . $ERRNO);
@@ -687,6 +692,7 @@ sub _convert_pdf_to_png {
   }
   if ($CHILD_ERROR) {
     $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: pdftoppm failed with error code: " . ($CHILD_ERROR >> 8));
+    $::lxdebug->message(LXDebug::WARN(), "SL::File::_convert_pdf_to_png: File: $filename");
     return;
   }
 
index b030d83..759de32 100644 (file)
@@ -14,6 +14,7 @@ use SL::MIME;
 use SL::Util qw(trim);
 use SL::YAML;
 use SL::DB::AdditionalBillingAddress;
+use SL::DB::AuthUser;
 use SL::DB::History;
 use SL::DB::Order;
 use SL::DB::Default;
@@ -279,8 +280,8 @@ sub action_print {
   my $groupitems  = $::form->{print_options}->{groupitems};
   my $printer_id  = $::form->{print_options}->{printer_id};
 
-  # only pdf and opendocument by now
-  if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
+  # only PDF, OpenDocument & HTML for now
+  if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
   }
 
@@ -296,25 +297,25 @@ sub action_print {
   $form->{format}           = $format;
   $form->{formname}         = $formname;
   $form->{language}         = '_' . $self->order->language->template_code if $self->order->language;
-  my $pdf_filename          = $form->generate_attachment_filename();
+  my $doc_filename          = $form->generate_attachment_filename();
 
-  my $pdf;
-  my @errors = $self->generate_pdf(\$pdf, { format     => $format,
+  my $doc;
+  my @errors = $self->generate_doc(\$doc, { format     => $format,
                                             formname   => $formname,
                                             language   => $self->order->language,
                                             printer_id => $printer_id,
                                             groupitems => $groupitems });
   if (scalar @errors) {
-    return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
+    return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render;
   }
 
   if ($media eq 'screen') {
     # screen/download
-    $self->js->flash('info', t8('The PDF has been created'));
+    $self->js->flash('info', t8('The document has been created.'));
     $self->send_file(
-      \$pdf,
-      type         => SL::MIME->mime_type_from_ext($pdf_filename),
-      name         => $pdf_filename,
+      \$doc,
+      type         => SL::MIME->mime_type_from_ext($doc_filename),
+      name         => $doc_filename,
       js_no_render => 1,
     );
 
@@ -323,13 +324,13 @@ sub action_print {
     my $printer_id = $::form->{print_options}->{printer_id};
     SL::DB::Printer->new(id => $printer_id)->load->print_document(
       copies  => $copies,
-      content => $pdf,
+      content => $doc,
     );
 
-    $self->js->flash('info', t8('The PDF has been printed'));
+    $self->js->flash('info', t8('The document has been printed.'));
   }
 
-  my @warnings = $self->store_pdf_to_webdav_and_filemanagement($pdf, $pdf_filename);
+  my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $doc_filename);
   if (scalar @warnings) {
     $self->js->flash('warning', $_) for @warnings;
   }
@@ -366,7 +367,7 @@ sub action_preview_pdf {
   my $pdf_filename          = $form->generate_attachment_filename();
 
   my $pdf;
-  my @errors = $self->generate_pdf(\$pdf, { format     => $format,
+  my @errors = $self->generate_doc(\$pdf, { format     => $format,
                                             formname   => $formname,
                                             language   => $self->order->language,
                                           });
@@ -425,13 +426,18 @@ sub action_save_and_show_email_dialog {
   $email_form->{js_send_function}    = 'kivi.Order.send_email()';
 
   my %files = $self->get_files_for_email_dialog();
-  $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
+
+  my @employees_with_email = grep {
+    my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
+    $user && !!trim($user->get_config_value('email'));
+  } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
+
   my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
-                                  email_form  => $email_form,
-                                  show_bcc    => $::auth->assert('email_bcc', 'may fail'),
-                                  FILES       => \%files,
-                                  is_customer => $self->cv eq 'customer',
-                                  ALL_EMPLOYEES => $self->{all_employees},
+                                  email_form    => $email_form,
+                                  show_bcc      => $::auth->assert('email_bcc', 'may fail'),
+                                  FILES         => \%files,
+                                  is_customer   => $self->cv eq 'customer',
+                                  ALL_EMPLOYEES => \@employees_with_email,
   );
 
   $self->js
@@ -469,24 +475,24 @@ sub action_send_email {
   $::form->{media}  = 'email';
 
   if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
-    my $pdf;
-    my @errors = $self->generate_pdf(\$pdf, {media      => $::form->{media},
+    my $doc;
+    my @errors = $self->generate_doc(\$doc, {media      => $::form->{media},
                                             format     => $::form->{print_options}->{format},
                                             formname   => $::form->{print_options}->{formname},
                                             language   => $self->order->language,
                                             printer_id => $::form->{print_options}->{printer_id},
                                             groupitems => $::form->{print_options}->{groupitems}});
     if (scalar @errors) {
-      return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
+      return $self->js->flash('error', t8('Generating the document failed: #1', $errors[0]))->render($self);
     }
 
-    my @warnings = $self->store_pdf_to_webdav_and_filemanagement($pdf, $::form->{attachment_filename});
+    my @warnings = $self->store_doc_to_webdav_and_filemanagement($doc, $::form->{attachment_filename});
     if (scalar @warnings) {
       flash_later('warning', $_) for @warnings;
     }
 
     my $sfile = SL::SessionFile::Random->new(mode => "w");
-    $sfile->fh->print($pdf);
+    $sfile->fh->print($doc);
     $sfile->fh->close;
 
     $::form->{tmpfile} = $sfile->file_name;
@@ -494,7 +500,7 @@ sub action_send_email {
   }
 
   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
-  $::form->send_email(\%::myconfig, 'pdf');
+  $::form->send_email(\%::myconfig, $::form->{print_options}->{format});
 
   # internal notes
   my $intnotes = $self->order->intnotes;
@@ -1879,7 +1885,7 @@ sub pre_render {
                 no_queue           => 1,
                 no_postscript      => 1,
                 no_opendocument    => 0,
-                no_html            => 1},
+                no_html            => 0},
   );
 
   foreach my $item (@{$self->order->orderitems}) {
@@ -1914,8 +1920,8 @@ sub pre_render {
 
   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
 
-  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
-                                                         edit_periodic_invoices_config calculate_qty kivi.Validator follow_up show_history);
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
+                                                         edit_periodic_invoices_config calculate_qty follow_up show_history);
   $self->setup_edit_action_bar;
 }
 
@@ -2065,8 +2071,8 @@ sub setup_edit_action_bar {
   }
 }
 
-sub generate_pdf {
-  my ($self, $pdf_ref, $params) = @_;
+sub generate_doc {
+  my ($self, $doc_ref, $params) = @_;
 
   my $order  = $self->order;
   my @errors = ();
@@ -2088,6 +2094,9 @@ sub generate_pdf {
   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
     $template_ext  = 'odt';
     $template_type = 'OpenDocument';
+  } elsif ($print_form->{format} =~ m{html}i) {
+    $template_ext  = 'html';
+    $template_type = 'HTML';
   }
 
   # search for the template
@@ -2109,7 +2118,7 @@ sub generate_pdf {
     eval {
       $print_form->prepare_for_printing;
 
-      $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
+      $$doc_ref = SL::Helper::CreatePDF->create_pdf(
         format        => $print_form->{format},
         template_type => $template_type,
         template      => $template_file,
@@ -2305,7 +2314,7 @@ sub save_history {
   )->save;
 }
 
-sub store_pdf_to_webdav_and_filemanagement {
+sub store_doc_to_webdav_and_filemanagement {
   my ($self, $content, $filename) = @_;
 
   my $order = $self->order;
@@ -2325,21 +2334,21 @@ sub store_pdf_to_webdav_and_filemanagement {
       $webdav_file->store(data => \$content);
       1;
     } or do {
-      push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
+      push @errors, t8('Storing the document to the WebDAV folder failed: #1', $@);
     };
   }
   if ($order->id && $::instance_conf->get_doc_storage) {
     eval {
       SL::File->save(object_id     => $order->id,
                      object_type   => $order->type,
-                     mime_type     => 'application/pdf',
+                     mime_type     => SL::MIME->mime_type_from_ext($filename),
                      source        => 'created',
                      file_type     => 'document',
                      file_name     => $filename,
                      file_contents => $content);
       1;
     } or do {
-      push @errors, t8('Storing PDF in storage backend failed: #1', $@);
+      push @errors, t8('Storing the document in the storage backend failed: #1', $@);
     };
   }
 
index 757f1ab..d2638fb 100644 (file)
@@ -709,7 +709,7 @@ sub add {
 
 sub _set_javascript {
   my ($self) = @_;
-  $::request->layout->use_javascript("${_}.js")  for qw(kivi.Part kivi.File kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery kivi.ShopPart);
+  $::request->layout->use_javascript("${_}.js")  for qw(kivi.Part kivi.File kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery kivi.ShopPart kivi.Validator);
   $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $self->part->id ]})});") if $self->part->id;
 }
 
@@ -1335,6 +1335,7 @@ sub _setup_form_action_bar {
           t8('Save'),
           call      => [ 'kivi.Part.save' ],
           disabled  => !$may_edit ? t8('You do not have the permissions to access this function.') : undef,
+          checks    => ['kivi.validate_form'],
         ],
         action => [
           t8('Use as new'),
index 6d9fd2a..a2d103f 100644 (file)
@@ -81,7 +81,9 @@ sub parse_and_import {
 
       # optional KOST1 - KOST2 ?
       $department_name = $row->[36];
-      $department    = SL::DB::Manager::Department->get_first(description => { like =>  $department_name . '%' });
+      if ($department_name) {
+        $department    = SL::DB::Manager::Department->get_first(description => { like =>  $department_name . '%' });
+      }
 
       my $amount = $::form->parse_amount({ numberformat => '1000,00' }, $row->[0]);
 
index 940dce8..c6ffad4 100644 (file)
@@ -7,6 +7,24 @@ use SL::DB::Manager::AdditionalBillingAddress;
 
 __PACKAGE__->meta->initialize;
 
+__PACKAGE__->after_save('_after_save_ensure_only_one_marked_as_default_per_customer');
+
+sub _after_save_ensure_only_one_marked_as_default_per_customer {
+  my ($self) = @_;
+
+  if ($self->id && $self->customer_id && $self->default_address) {
+    SL::DB::Manager::AdditionalBillingAddress->update_all(
+      set   => { default_address => 0 },
+      where => [
+        customer_id => $self->customer_id,
+        '!id'       => $self->id,
+      ],
+    );
+  }
+
+  return 1;
+}
+
 sub displayable_id {
   my $self = shift;
   my $text = join('; ', grep { $_ } (map({ $self->$_ } qw(name street)),
index 8da20c0..0c321b6 100644 (file)
@@ -27,4 +27,14 @@ sub safe_name {
   return $self->name || $self->login;
 }
 
+sub auth_user {
+  my ($self) = @_;
+
+  die 'not an accessor' if scalar(@_) > 1;
+
+  require SL::DB::AuthUser;
+
+  return SL::DB::Manager::AuthUser->find_by(login => $self->login);
+}
+
 1;
index c5c29dd..54f5483 100644 (file)
@@ -539,7 +539,8 @@ sub invoice_type {
   my ($self) = @_;
 
   return 'ar_transaction'     if !$self->invoice;
-  return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
+  # stornoed credit_notes are still credit notes and not invoices
+  return 'credit_note'        if $self->type eq 'credit_note' && $self->amount < 0;
   return 'invoice_storno'     if $self->type ne 'credit_note' && $self->amount < 0 &&  $self->storno;
   return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 &&  $self->storno;
   return 'invoice';
index 40e1398..c457080 100644 (file)
@@ -73,7 +73,7 @@ sub get_mtime {
   die "no dbfile" unless $params{dbfile};
   $main::lxdebug->message(LXDebug->DEBUG2(), "version=" .$params{version});
   my ($path, undef, undef) = $self->webdav_path($params{dbfile});
-  die "no file found in backend" if !-f $path;
+  die "No file found in Backend: " . $path unless -f $path;
   my @st = stat($path);
   my $dt = DateTime->from_epoch(epoch => $st[9])->clone();
   $main::lxdebug->message(LXDebug->DEBUG2(), "dt=" .$dt);
@@ -84,7 +84,7 @@ sub get_filepath {
   my ($self, %params) = @_;
   die "no dbfile" unless $params{dbfile};
   my ($path, undef, undef) = $self->webdav_path($params{dbfile});
-  die "no file" if !-f $path;
+  die "No file found in Backend: " . $path unless -f $path;
   return $path;
 }
 
@@ -105,8 +105,7 @@ sub sync_from_backend {
 }
 
 sub enabled {
-  return 0 unless $::instance_conf->get_doc_webdav;
-  return 1;
+  return $::instance_conf->get_doc_webdav;
 }
 
 #
@@ -187,18 +186,17 @@ sub webdav_path {
   }
   $main::lxdebug->message(LXDebug->DEBUG2(), "file_name=" . $dbfile->file_name ." number=".$number);
 
-  my @fileparts = split(/_/, $dbfile->file_name);
-  my $number_ext = pop @fileparts;
-  my ($maynumber, $ext) = split(/\./, $number_ext, 2);
-  push @fileparts, $maynumber if $maynumber ne $number;
-
-  my $basename = join('_', @fileparts);
-
   my $path = File::Spec->catdir($self->get_rootdir, "webdav", $::auth->client->{id}, $type, $number);
   if (!-d $path) {
     File::Path::make_path($path, { chmod => 0770 });
   }
-  my $fname = $basename . '_' . $number . '_' . $dbfile->itime->strftime('%Y%m%d_%H%M%S');
+  # simply add the timestring before the last .
+  # fails for .tar.gz but the number extraction algorithm failed for all
+  # '123 Storno zu 456' cases and doubled the name like:
+  # Rechnung_123_Storno_zu_456_202113104 Storno zu 456_20211123_113023
+  # TODO extension should be part of the File Model (filetype)
+  my ($filename, $ext) = split(/\.([^\.]+)$/, $dbfile->file_name);
+  my $fname = $filename . '_' . $dbfile->itime->strftime('%Y%m%d_%H%M%S');
   $fname .= '.' . $ext if $ext;
 
   $main::lxdebug->message(LXDebug->DEBUG2(), "webdav path=" . $path . " filename=" . $fname);
index 52d222b..29d9d6c 100644 (file)
@@ -2986,7 +2986,7 @@ sub get_history {
       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 | .
       qq|FROM history_erp h | .
       qq|LEFT JOIN employee emp ON (emp.id = h.employee_id) | .
-      qq|WHERE (trans_id = | . $trans_id . qq|) $restriction | .
+      qq|WHERE (trans_id = | . $dbh->quote($trans_id) . qq|) $restriction | .
       $order;
 
     my $sth = $dbh->prepare($query) || $self->dberror($query);
index 032b179..25c7d0c 100644 (file)
@@ -247,6 +247,9 @@ sub select_tag {
 sub checkbox_tag {
   my ($name, %attributes) = @_;
 
+  my %label_attributes = map { (substr($_, 6) => $attributes{$_}) } grep { m{^label_} } keys %attributes;
+  delete @attributes{grep { m{^label_} } keys %attributes};
+
   _set_id_attribute(\%attributes, $name);
 
   $attributes{value}   = 1 unless defined $attributes{value};
@@ -263,7 +266,7 @@ sub checkbox_tag {
   my $code  = '';
   $code    .= hidden_tag($name, 0, %attributes, id => $attributes{id} . '_hidden') if $for_submit;
   $code    .= html_tag('input', undef,  %attributes, name => $name, type => 'checkbox');
-  $code    .= html_tag('label', $label, for => $attributes{id}) if $label;
+  $code    .= html_tag('label', $label, for => $attributes{id}, %label_attributes) if $label;
   $code    .= javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
 
   return $code;
@@ -272,6 +275,9 @@ sub checkbox_tag {
 sub radio_button_tag {
   my ($name, %attributes) = @_;
 
+  my %label_attributes = map { (substr($_, 6) => $attributes{$_}) } grep { m{^label_} } keys %attributes;
+  delete @attributes{grep { m{^label_} } keys %attributes};
+
   $attributes{value}   = 1 unless exists $attributes{value};
 
   _set_id_attribute(\%attributes, $name, 1);
@@ -286,7 +292,7 @@ sub radio_button_tag {
   }
 
   my $code  = html_tag('input', undef,  %attributes, name => $name, type => 'radio');
-  $code    .= html_tag('label', $label, for => $attributes{id}) if $label;
+  $code    .= html_tag('label', $label, for => $attributes{id}, %label_attributes) if $label;
 
   return $code;
 }
@@ -574,7 +580,10 @@ C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
 
 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
 created with said C<label>. No attribute named C<label> is created in
-that case.
+that case. Furthermore, all attributes whose names start with
+C<label_> become attributes on the label tag without the C<label_>
+prefix. For example, C<label_style='#ff0000'> will be turned into
+C<style='#ff0000'> on the label tag, causing the text to become red.
 
 If C<%attributes> contains a key C<checkall> then the value is taken as a
 JQuery selector and clicking this checkbox will also toggle all checkboxes
@@ -588,7 +597,10 @@ C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
 
 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
 created with said C<label>. No attribute named C<label> is created in
-that case.
+that case. Furthermore, all attributes whose names start with
+C<label_> become attributes on the label tag without the C<label_>
+prefix. For example, C<label_style='#ff0000'> will be turned into
+C<style='#ff0000'> on the label tag, causing the text to become red.
 
 =item C<select_tag $name, \@collection, %attributes>
 
index 18940c3..da5d54a 100644 (file)
@@ -203,7 +203,7 @@ sub delete {
     do_query($form, $dbh, $query, @where_values);
 
     1;
-  }) or die { SL::DD->client->error };
+  }) or die { SL::DB->client->error };
 
   $main::lxdebug->leave_sub();
 }
index 8682c7e..b20a7b1 100644 (file)
@@ -47,7 +47,7 @@ sub available_templates {
        -d ($::lx_office_conf{paths}->{templates} . "/$_")
     && !/^\.\.?$/
     && !m/\.(?:html|tex|sty|odt)$/
-    && !m/^(?:webpages$|print$|mail$|\.)/
+    && !m/^(?:webpages$|mobile_webpages$|pdf$|print$|mail$|\.)/
   } keys %dir_h;
 
   tie %dir_h, 'IO::Dir', "$::lx_office_conf{paths}->{templates}/print";
index 89e40d0..eedde3f 100644 (file)
@@ -54,7 +54,9 @@ use SL::IO;
 use SL::File;
 use SL::PriceSource;
 use SL::Presenter::Part;
+use SL::Util qw(trim);
 
+use SL::DB::AuthUser;
 use SL::DB::Contact;
 use SL::DB::Currency;
 use SL::DB::Customer;
@@ -2113,7 +2115,11 @@ sub show_sales_purchase_email_dialog {
     $body_params{fallback_translation_type} = "preset_text_invoice";
   }
 
-  $::form->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
+  my @employees_with_email = grep {
+    my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
+    $user && !!trim($user->get_config_value('email'));
+  } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
+
   my $email_form = {
     to                  => $email,
     cc                  => $email_cc,
@@ -2125,12 +2131,12 @@ sub show_sales_purchase_email_dialog {
 
   my %files = _get_files_for_email_dialog();
   my $html  = $::form->parse_html_template("common/_send_email_dialog", {
-    email_form  => $email_form,
-    show_bcc    => $::auth->assert('email_bcc', 'may fail'),
-    FILES       => \%files,
-    is_customer => $::form->{vc} eq 'customer',
+    email_form      => $email_form,
+    show_bcc        => $::auth->assert('email_bcc', 'may fail'),
+    FILES           => \%files,
+    is_customer     => $::form->{vc} eq 'customer',
     is_invoice_mail => ($record_email && $::form->{type} eq 'invoice'),
-    ALL_EMPLOYEES   => $::form->{all_employees},
+    ALL_EMPLOYEES   => \@employees_with_email,
   });
 
   print $::form->ajax_response_header, $html;
@@ -2195,3 +2201,35 @@ sub _maybe_attach_zugferd_data {
     $::form->error($e->message);
   }
 }
+
+sub download_factur_x_xml {
+  my ($form) = @_;
+
+  my $record = _make_record();
+
+  die if !$record
+      || !$record->can('customer')
+      || !$record->customer
+      || !$record->can('create_pdf_a_print_options')
+      || !$record->can('create_zugferd_data')
+      || !$record->customer->create_zugferd_invoices_for_this_customer;
+
+  my $xml_content = eval { $record->create_zugferd_data };
+
+  if (my $e = SL::X::ZUGFeRDValidation->caught) {
+    $::form->error($e->message);
+  }
+
+  my $attachment_filename = $::form->generate_attachment_filename;
+  $attachment_filename    =~ s{\.[^.]+$}{.xml};
+  my %headers             = (
+    '-type'           => 'application/xml',
+    '-connection'     => 'close',
+    '-attachment'     => $attachment_filename,
+    '-content-length' => length($xml_content),
+  );
+
+  print $::request->cgi->header(%headers);
+
+  $::locale->with_raw_io(\*STDOUT, sub { print $xml_content });
+}
index 76d4b3e..12deb26 100644 (file)
@@ -276,12 +276,14 @@ sub prepare_invoice {
 }
 
 sub setup_is_action_bar {
+  my ($tmpl_var)              = @_;
   my $form                    = $::form;
   my $change_never            = $::instance_conf->get_is_changeable == 0;
   my $change_on_same_day_only = $::instance_conf->get_is_changeable == 2 && ($form->current_date(\%::myconfig) ne $form->{gldate});
   my $payments_balanced       = ($::form->{oldtotalpaid} == 0);
   my $has_storno              = ($::form->{storno} && !$::form->{storno_id});
   my $may_edit_create         = $::auth->assert('invoice_edit', 1);
+  my $factur_x_enabled        = $tmpl_var->{invoice_obj} && $tmpl_var->{invoice_obj}->customer->create_zugferd_invoices_for_this_customer;
   my ($is_linked_bank_transaction, $warn_unlinked_delivery_order);
     if ($::form->{id}
         && SL::DB::Default->get->payments_changeable != 0
@@ -423,6 +425,14 @@ sub setup_is_action_bar {
                     : $form->{postal_invoice} ? t8('This customer wants a postal invoices.')
                     :                     undef,
         ],
+        action => [ t8('Factur-X/ZUGFeRD'),
+          submit   => [ '#form', { action => "download_factur_x_xml" } ],
+          checks   => [ 'kivi.validate_form' ],
+          disabled => !$may_edit_create  ? t8('You must not print this invoice.')
+                    : !$form->{id}       ? t8('This invoice has not been posted yet.')
+                    : !$factur_x_enabled ? t8('Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.')
+                    :                      undef,
+        ],
       ], # end of combobox "Export"
 
       combobox => [
@@ -562,7 +572,7 @@ sub form_header {
   $TMPL_VAR{payment_terms_obj} = get_payment_terms_for_invoice();
   $form->{duedate}             = $TMPL_VAR{payment_terms_obj}->calc_date(reference_date => $form->{invdate}, due_date => $form->{duedate})->to_kivitendo if $TMPL_VAR{payment_terms_obj};
 
-  setup_is_action_bar();
+  setup_is_action_bar(\%TMPL_VAR);
 
   $form->header();
 
index 9d2c9e8..7294072 100644 (file)
@@ -96,6 +96,9 @@ namespace('kivi.Order', function(ns) {
 
     $('#print_options_form table').first().remove().appendTo('#email_form_print_options');
 
+    $('select#format').change(kivi.Order.adjust_email_attachment_name_for_template_format);
+    kivi.Order.adjust_email_attachment_name_for_template_format();
+
     var to_focus = $('#email_form_to').val() === '' ? 'to' : 'subject';
     $('#email_form_' + to_focus).focus();
   };
@@ -155,6 +158,20 @@ namespace('kivi.Order', function(ns) {
     email_dialog.dialog("close");
   };
 
+  ns.adjust_email_attachment_name_for_template_format = function() {
+    var $filename_elt = $('#email_form_attachment_filename');
+    var $format_elt   = $('select#format');
+
+    if (!$filename_elt || !$format_elt)
+      return;
+
+    var format   = $format_elt.val().toLowerCase();
+    var new_ext  = format == 'html' ? 'html' : format == 'opendocument' ? 'odt' : 'pdf';
+    var filename = $filename_elt.val();
+
+    $filename_elt.val(filename.replace(/[^.]+$/, new_ext));
+  };
+
   ns.set_number_in_title = function(elt) {
     $('#nr_in_title').html($(elt).val());
   };
@@ -234,6 +251,8 @@ namespace('kivi.Order', function(ns) {
   };
 
   ns.recalc_amounts_and_taxes = function() {
+    if (!kivi.validate_form('#order_form')) return;
+
     var data = $('#order_form').serializeArray();
     data.push({ name: 'action', value: 'Order/recalc_amounts_and_taxes' });
 
@@ -821,7 +840,7 @@ namespace('kivi.Order', function(ns) {
     if (number_info !== '') { info += ' (' + number_info + ')' }
     if (name_info   !== '') { info += ' (' + name_info + ')' }
 
-    if (!$('#follow_up_rowcount').lenght) {
+    if (!$('#follow_up_rowcount').length) {
       $('<input type="hidden" name="follow_up_rowcount"        id="follow_up_rowcount">').appendTo('#order_form');
       $('<input type="hidden" name="follow_up_trans_id_1"      id="follow_up_trans_id_1">').appendTo('#order_form');
       $('<input type="hidden" name="follow_up_trans_type_1"    id="follow_up_trans_type_1">').appendTo('#order_form');
index b564ffb..4d03451 100644 (file)
@@ -142,6 +142,26 @@ namespace("kivi.Validator", function(ns) {
         ns.annotate($e_annotate);
         return true;
       }
+    },
+    trimmed_whitespaces: function($e, $e_annotate) {
+      $e_annotate = $e_annotate || $e;
+
+      var string = $e.val();
+
+      if ($e.hasClass('tooltipstered'))
+        $e.tooltipster('destroy');
+
+      if (string.match(/^\s|\s$/)) {
+        $e.val(string.trim());
+
+        $e.tooltipster({
+          content: kivi.t8("Leading and trailing whitespaces have been removed."),
+          contentAsHTML: true,
+          theme: 'tooltipster-light',
+        });
+        $e.tooltipster('show');
+      }
+      return true;
     }
   };
 
index 3e79c97..937c20e 100644 (file)
@@ -89,6 +89,7 @@ namespace("kivi").setupLocale({
 "July":"Juli",
 "Jun":"Jun",
 "June":"Juni",
+"Leading and trailing whitespaces have been removed.":"Leerzeichen wurden vorne und hinten entfernt",
 "Loading...":"Wird geladen...",
 "Map":"Karte",
 "Mar":"März",
index 9c87389..342e9ba 100644 (file)
@@ -89,6 +89,7 @@ namespace("kivi").setupLocale({
 "July":"",
 "Jun":"",
 "June":"",
+"Leading and trailing whitespaces have been removed.":"",
 "Loading...":"",
 "Map":"",
 "Mar":"",
index 9b84bc6..dad9acf 100755 (executable)
@@ -538,6 +538,7 @@ $self->{texts} = {
   'CSV Export successful!'      => 'CSV-Export erfolgreich!',
   'CSV export'                  => 'CSV-Export',
   'CSV export -- options'       => 'CSV-Export -- Optionen',
+  'CSV import: additional billing addresses' => 'CSV-Import: zusätzliche Rechnungsadressen',
   'CSV import: ar transactions' => 'CSV Import: Debitorenbuchungen',
   'CSV import: bank transactions' => 'CSV Import: Bankbewegungen',
   'CSV import: contacts'        => 'CSV-Import: Ansprechpersonen',
@@ -821,6 +822,7 @@ $self->{texts} = {
   'Created for'                 => 'Erstellt für',
   'Created on'                  => 'Erstellt am',
   'Creating Documents'          => 'Erzeuge Dokumente',
+  'Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.' => 'Das Erzeugen von Factur-X/ZUGFeRD-Rechnungen ist für diesen Kunden nicht aktiviert.',
   'Creating invoices'           => 'Erzeuge Rechnungen',
   'Creating the PDF failed:'    => 'PDF-Erzeugung fehlgeschlagen:',
   'Creation Date'               => 'Erstelldatum',
@@ -995,6 +997,7 @@ $self->{texts} = {
   'Default Transfer with services' => 'Ein- /Auslagern von Dienstleistungen über Standard-Lagerplatz',
   'Default Warehouse'           => 'Standard-Lager',
   'Default Warehouse with ignoring onhand' => 'Standard-Lager für Auslagern ohne Prüfung auf Bestand',
+  'Default address flag'        => 'Standard-Adresse-Schalter',
   'Default article for converting into quotations and orders' => 'Standardartikel für Konvertierung von Pflichtenheften in Angebote und Aufträge',
   'Default booking group'       => 'Standardbuchungsgruppe',
   'Default client'              => 'Standardmandant',
@@ -1499,6 +1502,7 @@ $self->{texts} = {
   'Extended status'             => 'Erweiterter Status',
   'Extension Of Time'           => 'Dauerfristverlängerung',
   'Factor'                      => 'Faktor',
+  'Factur-X/ZUGFeRD'            => 'Factur-X/ZUGFeRD',
   'Factur-X/ZUGFeRD import'     => 'Factur-X-/ZUGFeRD-Import',
   'Factur-X/ZUGFeRD invoice'    => 'Factur-X-/ZUGFeRD-Rechnung',
   'Factur-X/ZUGFeRD notes for each invoice' => 'Factur-X-/ZUGFeRD-Notizen für jede Rechnung',
@@ -1618,6 +1622,7 @@ $self->{texts} = {
   'General ledger transactions can only be changed on the day they are posted.' => 'Dialogbuchungen können nur am Buchungstag geändert werden.',
   'General settings'            => 'Allgemeine Einstellungen',
   'Generate and print sales delivery orders' => 'Erzeuge und drucke Lieferscheine',
+  'Generating the document failed: #1' => 'Das Dokument konnte nicht erzeugt werden: #1',
   'Germany'                     => 'Deutschland',
   'Get one order'               => 'Hole eine Bestellung',
   'Get one order by shopordernumber' => 'Hole eine Bestellung über Shopbestellnummer',
@@ -1932,6 +1937,7 @@ $self->{texts} = {
   'Lastcost'                    => 'Einkaufspreis',
   'Lastcost (with X being a number)' => 'Einkaufspreis (X ist eine fortlaufende Zahl)',
   'Lastname'                    => 'Nachname',
+  'Leading and trailing whitespaces have been removed.' => 'Leerzeichen wurden vorne und hinten entfernt',
   'Left'                        => 'Links',
   'Letter'                      => 'Brief',
   'Letter Draft'                => 'Briefentwurf',
@@ -3257,6 +3263,8 @@ $self->{texts} = {
   'Storage Type for shopimages' => 'Speichertyp für Shopbilder',
   'Storing PDF in storage backend failed: #1' => 'Speichern der PDF-Datei im Datei-Speicher fehlgeschlagen: #1',
   'Storing PDF to webdav folder failed: #1' => 'Speichern der PDF im WebDAV Ordner fehlgeschlagen: #1',
+  'Storing the document in the storage backend failed: #1' => 'Das Ablegen des Dokuments im Dokumentenspeicher schlug fehl: #1',
+  'Storing the document to the WebDAV folder failed: #1' => 'Das Ablegen des Dokuments im WebDAV-Ordner schlug fehl: #1',
   'Storing the emails in the journal is currently disabled in the client configuration.' => 'Das Speichern von versendeten E-Mails ist derzeit in der Mandantenkonfigurierung abgeschaltet.',
   'Storno'                      => 'Storno',
   'Storno (one letter abbreviation)' => 'S',
@@ -3506,6 +3514,8 @@ $self->{texts} = {
   'The display of (mainly) picker results can be configured. To insert the value of one option use <%Name%>.' => 'Die Anzeigenamen von (hauptsächlich) Auswahl-Ergebnissen (Picker) können konfiguriert werden. Um einen Wert einer Option in die Anzeige aufzunehmen, verwenden Sie <%Name%>.',
   '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' => 'Die Daten wurden bereits von einem anderen Benutzer verändert. Deshalb ist das Dokument ungültig und es wurde keine E-Mail verschickt. Bitte öffnen Sie das Dokument erneut in einem extra Fenster und übertragen Sie die Daten',
   'The document has been changed by another user. Please reopen it in another window and copy the changes to the new window' => 'Die Daten wurden bereits von einem anderen Benutzer verändert. Deshalb ist das Dokument ungültig. Bitte öffnen Sie das Dokument erneut in einem extra Fenster und übertragen Sie die Daten',
+  'The document has been created.' => 'Das Dokument wurde erzeugt.',
+  'The document has been printed.' => 'Das Dokument wurde gedruckt.',
   'The documents have been sent to the printer \'#1\'.' => 'Die Dokumente sind zum Drucker \'#1\' geschickt',
   'The dunnings have been printed.' => 'Die Mahnung(en) wurden gedruckt.',
   'The email has been sent.'    => 'Die E-Mail wurde verschickt.',
index 4581ae8..59c78fd 100644 (file)
@@ -538,6 +538,7 @@ $self->{texts} = {
   'CSV Export successful!'      => '',
   'CSV export'                  => '',
   'CSV export -- options'       => '',
+  'CSV import: additional billing addresses' => '',
   'CSV import: ar transactions' => '',
   'CSV import: bank transactions' => '',
   'CSV import: contacts'        => '',
@@ -821,6 +822,7 @@ $self->{texts} = {
   'Created for'                 => '',
   'Created on'                  => '',
   'Creating Documents'          => '',
+  'Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.' => '',
   'Creating invoices'           => '',
   'Creating the PDF failed:'    => '',
   'Creation Date'               => '',
@@ -995,6 +997,7 @@ $self->{texts} = {
   'Default Transfer with services' => '',
   'Default Warehouse'           => '',
   'Default Warehouse with ignoring onhand' => '',
+  'Default address flag'        => '',
   'Default article for converting into quotations and orders' => '',
   'Default booking group'       => '',
   'Default client'              => '',
@@ -1499,6 +1502,7 @@ $self->{texts} = {
   'Extended status'             => '',
   'Extension Of Time'           => '',
   'Factor'                      => '',
+  'Factur-X/ZUGFeRD'            => '',
   'Factur-X/ZUGFeRD import'     => '',
   'Factur-X/ZUGFeRD invoice'    => '',
   'Factur-X/ZUGFeRD notes for each invoice' => '',
@@ -1932,6 +1936,7 @@ $self->{texts} = {
   'Lastcost'                    => '',
   'Lastcost (with X being a number)' => '',
   'Lastname'                    => '',
+  'Leading and trailing whitespaces have been removed.' => '',
   'Left'                        => '',
   'Letter'                      => '',
   'Letter Draft'                => '',
index b8d9630..52d7b07 100644 (file)
   params:
     action: CsvImport/new
     profile.type: contacts
+- parent: system_import_csv
+  id: system_import_csv_additional_billing_address
+  name: Additional Billing Addresses
+  order: 250
+  params:
+    action: CsvImport/new
+    profile.type: billing_addresses
 - parent: system_import_csv
   id: system_import_csv_shipto
   name: Shipto
index adaca91..4b8b13d 100755 (executable)
--- a/t/run.sh
+++ b/t/run.sh
@@ -2,4 +2,6 @@
 
 dir="$(dirname "$0")"
 
-perl "-I${dir}/../modules/override" "-I${dir}/.." "-I${dir}/../modules/fallback" "$@"
+for TEST in "$@"; do
+  perl "-I${dir}/../modules/override" "-I${dir}/.." "-I${dir}/../modules/fallback" "$TEST"
+done
index 553ed49..fbe0b1a 100644 (file)
   \let\ExtraDescription\__kivi_addExtraDescription:n
   \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
   \seq_map_inline:Nn \l_kivi_PricingTable_seq {
+    \if_mode_horizontal: \par \fi
     \bool_if:NT \g__kivi_Tabular_rowcolor_bool {
       \int_gincr:N \g__kivi_PricingTable_rowcolor_int
       \int_if_odd:nTF {\g__kivi_PricingTable_rowcolor_int}
index 964790c..c0392c2 100644 (file)
@@ -4,7 +4,7 @@
 [% USE LxERP %]
 <h1>[% title | html %]</h1>
 
-<form method=post action="[% script %]">
+<form method=post action="[% script | html %]">
 
 [% L.hidden_tag('accno', accno) %]
 [% L.hidden_tag('description', description) %]
index 79b984c..e626c35 100644 (file)
@@ -43,7 +43,7 @@
  [%- IF ALL_EMPLOYEES.size %]
   <tr class="hidden" data-toggle-recipients="1">
    <th align="right" nowrap>[% LxERP.t8("CC to Employee") %]</th>
-   <td>[% L.select_tag('email_form.cc_employee',  ALL_EMPLOYEES, value_key='login' title_key='safe_name', with_empty=1, style=style) %]</td>
+   <td>[% L.select_tag('email_form.cc_employee', ALL_EMPLOYEES, value_key='login' title_key='safe_name', with_empty=1, style=style) %]</td>
   </tr>
  [%- END %]
 
index e867f7a..568324e 100644 (file)
     </p>
    [%- END %]
 [%- END %][%# IF SELF.type == … %]
+  </div>
 
-   <p>
-    [%- L.submit_tag('action_download_sample', LxERP.t8('Download sample file')) %]
-   </p>
-
+  <div>
+    <p>
+      [%- L.submit_tag('action_download_sample', LxERP.t8('Download sample file')) %]
+    </p>
   </div>
 
   <hr>
index f948b5f..d4d8d7e 100644 (file)
         <label for="l_id">[% 'ID' | $T8 %]</label>
        </td>
        <td>
-        <input name="l_[% db %]number" id="l_[% db %]number" type="checkbox" class="checkbox" value="Y" checked>
-        <label for="l_[% db %]number">[% IF IS_CUSTOMER %][% 'Customer Number' | $T8 %][% ELSE %][% 'Vendor Number' | $T8 %][% END %]</label>
+        <input name="l_[% db | html %]number" id="l_[% db | html %]number" type="checkbox" class="checkbox" value="Y" checked>
+        <label for="l_[% db | html %]number">[% IF IS_CUSTOMER %][% 'Customer Number' | $T8 %][% ELSE %][% 'Vendor Number' | $T8 %][% END %]</label>
        </td>
        <td>
         <input name="l_name" id="l_name" type="checkbox" class="checkbox" value="Y" checked>
index d7bd915..2e1fc62 100644 (file)
@@ -1,8 +1,8 @@
 [%- USE T8 %]
 [%- USE L %]
-<h1>[% 'DATEX - Export Assistent' | $T8 %]</h1>
+<h1>[% 'DATEV - Export Assistent' | $T8 %]</h1>
 
-<form method='post' action='[% script %]' id='form'>
+<form method='post' action='[% script | html %]' id='form'>
 
 <table width=100%>
   <tr>
       <table>
         <tr>
           <td align=left nowrap>[% 'Beraternummer' | $T8 %]</td>
-          <td><input name=beraternr size=10 maxlength=7 value="[% beraternr %]"></td>
+          <td>[% L.input_tag("beraternr", beraternr, size=10, maxlength=7) %]</td>
 
           <td align=left nowrap>[% 'DFV-Kennzeichen' | $T8 %]</td>
-          <td><input name=dfvkz size=5 maxlength=2 value="[% dfvkz %]"></td>
+          <td>[% L.input_tag("dfvkz", dfvkz, size=5, maxlength=2) %]</td>
         </tr>
         <tr>
           <td align=left nowrap>[% 'Beratername' | $T8 %]</td>
-          <td><input name=beratername size=10 maxlength=9 value="[% beratername %]"></td>
+          <td>[% L.input_tag("beratername", berater, size=10, maxlength=9) %]</td>
 
           <td align=left nowrap>[% 'Password' | $T8 %]</td>
-          <td><input name=passwort size=5 maxlength=4 value="[% passwort %]"></td>
+          <td>[% L.input_tag("passwort", passwort, size=5, maxlength=4) %]</td>
         </tr>
         <tr>
           <td align=left nowrap>[% 'Mandantennummer' | $T8 %]</td>
-          <td><input name=mandantennr size=10 maxlength=5 value="[% mandantennr %]"></td>
+          <td>[% L.input_tag("mandantennr", mandantennr, size=10, maxlength=5) %]</td>
 
           <td align=left nowrap>[% 'Medium Number' | $T8 %]</td>
-          <td><input name=datentraegernr size=5 maxlength=3 value="[% datentraegernr %]"></td>
+          <td>[% L.input_tag("datentraegernr", datentraegernr, size=5, maxlength=3) %]</td>
         </tr>
         <tr>
           <td></td>
           <td></td>
 
           <td align=left nowrap>[% 'Abrechnungsnummer' | $T8 %]</td>
-          <td><input name=abrechnungsnr size=5 maxlength=3 value="[% abrechnungsnr %]"></td>
+          <td>[% L.input_tag("abrechnungsnr", abrechnungsnr, size=5, maxlength=3) %]</td>
         </tr>
       </table>
     </td>
index 89bc582..80f6c3a 100644 (file)
           [% IF show_pk_option %]
            <td align=left>[% 'Export with CV Charts' | $T8 %]</td>
            <td align=left></td>
-           <td>[% L.checkbox_tag('use_pk', value => 1, checked => 0) %]</td>
+           <td>[% L.checkbox_tag('use_pk', value = 1, checked = 0) %]</td>
           [% ELSE %]
            <td align=left><font color="gray">[% 'Export with CV Charts' | $T8 %]</font></td>
            <td align=left></td>
-           <td>[% L.checkbox_tag('use_pk', value => 1, checked => 0, disabled => 1) %] </td>
+           <td>[% L.checkbox_tag('use_pk', value = 1, checked = 0, disabled = 1) %] </td>
            <td colspan="2"><font color="gray">[% 'Hint: Not all VC Numbers are personal accounts compliant' | $T8 %]</font></td>
           [% END %]
          </tr>
   </tr>
 </table>
 
-<input type=hidden name=beraternr value="[% beraternr %]">
-<input type=hidden name=dfvkz value="[% dfvkz %]">
-<input type=hidden name=beratername value="[% beratername %]">
-<input type=hidden name=passwort value="[% passwort %]">
-<input type=hidden name=mandantennr value="[% mandantennr %]">
-<input type=hidden name=datentraegernr value="[% datentraegernr %]">
-<input type=hidden name=exportformat value="[% exportformat %]">
-<input type=hidden name=abrechnungsnr value="[% abrechnungsnr %]">
+<input type=hidden name=beraternr value="[% beraternr | html %]">
+<input type=hidden name=dfvkz value="[% dfvkz | html %]">
+<input type=hidden name=beratername value="[% beratername | html %]">
+<input type=hidden name=passwort value="[% passwort | html %]">
+<input type=hidden name=mandantennr value="[% mandantennr | html %]">
+<input type=hidden name=datentraegernr value="[% datentraegernr | html %]">
+<input type=hidden name=exportformat value="[% exportformat | html %]">
+<input type=hidden name=abrechnungsnr value="[% abrechnungsnr | html %]">
 
-<input type=hidden name=exporttype value="[% exporttype %]">
+<input type=hidden name=exporttype value="[% exporttype | html %]">
 
 </form>
index 238e58b..f2e19e1 100644 (file)
@@ -21,7 +21,7 @@
 </p>
 
 <p>
- In dem gerade durchgeführten Export gab es [% net_gross_differences.size %]
+ In dem gerade durchgeführten Export gab es [% net_gross_differences.size | html %]
  solcher Fälle. Die Summe aller Abweichungen beläuft sich auf
  [% LxERP.format_amount(sum_net_gross_differences, 2) %].
 </p>
index ebfafeb..f9802bf 100644 (file)
@@ -8,6 +8,18 @@
 [%- END -%]
 
  <form name="Form" action="oe.pl" method="post">
+  [%- IF popup_dialog -%]
+    [% L.button_tag(popup_js_assign_function, LxERP.t8('Assign')) %]
+    [% L.button_tag(popup_js_close_function, LxERP.t8('Cancel')) %]
+
+  [%- ELSE -%]
+    [% L.hidden_tag('action', 'save_periodic_invoices_config') %]
+
+    <p>
+     [% L.submit_tag('', LxERP.t8('Assign')) %]
+     [% L.submit_tag('', LxERP.t8('Cancel'), onclick => "self.close(); return false;") %]
+    </p>
+  [%- END -%]
 
   <p>
    <table border="0">
   <p>(2): [% LxERP.t8("If missing then the start date will be used.") %]</p>
   <p>(3): [% LxERP.t8("Multiple addresses can be entered separated by commas.") %]</p>
   <p>(4): [% LxERP.t8("If left empty the default sender from the kivitendo configuration will be used (key 'email_from' in section 'periodic_invoices'; current value: #1).", HTML.escape(LXCONFIG.periodic_invoices.email_from)) %]</p>
-
-  [%- IF popup_dialog -%]
-    [% L.button_tag(popup_js_assign_function, LxERP.t8('Assign')) %]
-    [% L.button_tag(popup_js_close_function, LxERP.t8('Cancel')) %]
-
-  [%- ELSE -%]
-    [% L.hidden_tag('action', 'save_periodic_invoices_config') %]
-
-    <p>
-     [% L.submit_tag('', LxERP.t8('Assign')) %]
-     [% L.submit_tag('', LxERP.t8('Cancel'), onclick => "self.close(); return false;") %]
-    </p>
-  [%- END -%]
  </form>
 
  <script type="text/javascript">
index ba18da2..a9fa531 100644 (file)
@@ -19,7 +19,7 @@
             <table id="ic3">
              <tr>
               <th align="right">[% 'Part Number' | $T8 %]</th>
-              <td>[% L.input_tag("part.partnumber", SELF.part.partnumber, size=40, class="initial_focus") %]</td>
+              <td>[% L.input_tag("part.partnumber", SELF.part.partnumber, size=40, class="initial_focus", "data-validate"="trimmed_whitespaces") %]</td>
              </tr>
              <tr>
               <th align="right">[% 'Part Classification' | $T8 %]</th>
@@ -33,7 +33,7 @@
              </tr>
              <tr>
                <th align="right">[% 'EAN-Code' | $T8 %]</th>
-               <td>[% L.input_tag("part.ean", SELF.part.ean, size=40) %]</td>
+               <td>[% L.input_tag("part.ean", SELF.part.ean, size=40, "data-validate"="trimmed_whitespaces") %]</td>
              </tr>
              <tr>
               [%- IF SELF.all_partsgroups.size %]
       <table id="ic6">
        <tr>
         <th align="right" nowrap>[% 'Image' | $T8 %]</th>
-        <td>[% L.input_tag("part.image", SELF.part.image, size=40) %]</td>
+        <td>[% L.input_tag("part.image", SELF.part.image, size=40, "data-validate"="trimmed_whitespaces") %]</td>
         <th align="right" nowrap>[% 'Microfiche' | $T8 %]</th>
-        <td>[% L.input_tag("part.microfiche", SELF.part.microfiche, size=20) %]</td>
+        <td>[% L.input_tag("part.microfiche", SELF.part.microfiche, size=20, "data-validate"="trimmed_whitespaces") %]</td>
        </tr>
        <tr>
         <th align="right" nowrap>[% 'Drawing' | $T8 %]</th>
-        <td>[% L.input_tag("part.drawing", SELF.part.drawing, size=40) %]</td>
+        <td>[% L.input_tag("part.drawing", SELF.part.drawing, size=40, "data-validate"="trimmed_whitespaces") %]</td>
        </tr>
       </table>
      </td>