Merge branch 'mass_convert_delivery_orders_to_invoice'
authorJan Büren <jan@kivitendo-premium.de>
Fri, 4 Sep 2015 11:19:33 +0000 (13:19 +0200)
committerJan Büren <jan@kivitendo-premium.de>
Fri, 4 Sep 2015 11:19:33 +0000 (13:19 +0200)
SL/BackgroundJob/MassRecordCreationAndPrinting.pm [new file with mode: 0644]
SL/Controller/MassInvoiceCreatePrint.pm [new file with mode: 0644]
SL/DB/DeliveryOrder.pm
js/kivi.MassInvoiceCreatePrint.js [new file with mode: 0644]
menus/user/00-erp.yaml
t/db_helper/convert_invoice.t [new file with mode: 0644]
templates/webpages/mass_invoice_create_print_from_do/_create_print_all_status.html [new file with mode: 0644]
templates/webpages/mass_invoice_create_print_from_do/_create_print_all_step_1.html [new file with mode: 0644]
templates/webpages/mass_invoice_create_print_from_do/_filter.html [new file with mode: 0644]
templates/webpages/mass_invoice_create_print_from_do/list_invoices.html [new file with mode: 0644]
templates/webpages/mass_invoice_create_print_from_do/list_sales_delivery_orders.html [new file with mode: 0644]

diff --git a/SL/BackgroundJob/MassRecordCreationAndPrinting.pm b/SL/BackgroundJob/MassRecordCreationAndPrinting.pm
new file mode 100644 (file)
index 0000000..458d47a
--- /dev/null
@@ -0,0 +1,270 @@
+package SL::BackgroundJob::MassRecordCreationAndPrinting;
+
+use strict;
+use warnings;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use SL::DB::DeliveryOrder;
+use SL::DB::Order;  # origin order to delivery_order
+use SL::DB::Invoice;
+use SL::DB::Printer;
+use SL::SessionFile;
+use SL::Template;
+
+use constant WAITING_FOR_EXECUTION       => 0;
+use constant CONVERTING_DELIVERY_ORDERS  => 1;
+use constant PRINTING_INVOICES           => 2;
+use constant DONE                        => 3;
+# Data format:
+# my $data             = {
+#   record_ids          => [ 123, 124, 127, ],
+#   printer_id         => 4711,
+#   num_created        => 0,
+#   num_printed        => 0,
+#   invoice_ids        => [ 234, 235, ],
+#   conversion_errors  => [ { id => 124, number => 'A981723', message => "Stuff went boom" }, ],
+#   print_errors       => [ { id => 234, number => 'L87123123', message => "Printer is out of coffee" }, ],
+#   pdf_file_name      => 'qweqwe.pdf',
+# };
+
+sub create_invoices {
+  my ($self)  = @_;
+
+  my $job_obj = $self->{job_obj};
+  my $db      = $job_obj->db;
+
+  $job_obj->set_data(status => CONVERTING_DELIVERY_ORDERS())->save;
+
+  foreach my $delivery_order_id (@{ $job_obj->data_as_hash->{record_ids} }) {
+    my $number = $delivery_order_id;
+    my $data   = $job_obj->data_as_hash;
+
+    eval {
+      my $invoice;
+      my $sales_delivery_order = SL::DB::DeliveryOrder->new(id => $delivery_order_id)->load;
+      $number                  = $sales_delivery_order->donumber;
+
+      if (!$db->do_transaction(sub {
+        $invoice = $sales_delivery_order->convert_to_invoice(item_filter => \&delivery_order_item_filter, queue_sort => 1) || die $db->error;
+        # $delivery_order->post_save_sanity_check; # just a hint at e8521eee (#90 od)
+        1;
+      })) {
+        die $db->error;
+      }
+
+      $data->{num_created}++;
+      push @{ $data->{invoice_ids} }, $invoice->id;
+      push @{ $self->{invoices}    }, $invoice;
+
+      1;
+    } or do {
+      push @{ $data->{conversion_errors} }, { id => $delivery_order_id, number => $number, message => $@ };
+    };
+
+    $job_obj->update_attributes(data_as_hash => $data);
+  }
+}
+
+sub convert_invoices_to_pdf {
+  my ($self) = @_;
+
+  return if !@{ $self->{invoices} };
+
+  my $job_obj = $self->{job_obj};
+  my $db      = $job_obj->db;
+
+  $job_obj->set_data(status => PRINTING_INVOICES())->save;
+
+  require SL::Controller::MassInvoiceCreatePrint;
+
+  my $printer_id = $job_obj->data_as_hash->{printer_id};
+  my $ctrl       = SL::Controller::MassInvoiceCreatePrint->new;
+  my %variables  = (
+    type         => 'invoice',
+    formname     => 'invoice',
+    format       => 'pdf',
+    media        => $printer_id ? 'printer' : 'file',
+  );
+
+  my @pdf_file_names;
+
+  foreach my $invoice (@{ $self->{invoices} }) {
+    my $data = $job_obj->data_as_hash;
+
+    eval {
+      my %create_params = (
+        template  => $ctrl->find_template(name => 'invoice', printer_id => $printer_id),
+        variables => Form->new(''),
+        return    => 'file_name',
+      );
+
+      $create_params{variables}->{$_} = $variables{$_} for keys %variables;
+
+      $invoice->flatten_to_form($create_params{variables}, format_amounts => 1);
+      $create_params{variables}->prepare_for_printing;
+
+      push @pdf_file_names, $ctrl->create_pdf(%create_params);
+
+      $data->{num_printed}++;
+
+      1;
+
+    } or do {
+      push @{ $data->{print_errors} }, { id => $invoice->id, number => $invoice->invnumber, message => $@ };
+    };
+
+    $job_obj->update_attributes(data_as_hash => $data);
+  }
+
+  if (@pdf_file_names) {
+    my $data = $job_obj->data_as_hash;
+
+    eval {
+      $self->{merged_pdf} = $ctrl->merge_pdfs(file_names => \@pdf_file_names);
+      unlink @pdf_file_names;
+
+      if (!$printer_id) {
+        my $file_name = 'mass_invoice' . $job_obj->id . '.pdf';
+        my $sfile     = SL::SessionFile->new($file_name, mode => 'w');
+        $sfile->fh->print($self->{merged_pdf});
+        $sfile->fh->close;
+
+        $data->{pdf_file_name} = $file_name;
+      }
+
+      1;
+
+    } or do {
+      push @{ $data->{print_errors} }, { message => $@ };
+    };
+
+    $job_obj->update_attributes(data_as_hash => $data);
+  }
+}
+
+sub print_pdfs {
+  my ($self)     = @_;
+
+  my $job_obj    = $self->{job_obj};
+  my $data       = $job_obj->data_as_hash;
+  my $printer_id = $data->{printer_id};
+
+  return if !$printer_id;
+
+  my $printer = SL::DB::Printer->new(id => $printer_id)->load;
+  my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($printer->printer_command);
+  my $out;
+
+  if (!open $out, '|-', $command) {
+    push @{ $data->{print_errors} }, { message => $::locale->text('Could not execute printer command: #1', $!) };
+    $job_obj->update_attributes(data_as_hash => $data);
+    return;
+  }
+
+  binmode $out;
+  print $out $self->{merged_pdf};
+  close $out;
+}
+
+sub run {
+  my ($self, $job_obj) = @_;
+
+  $self->{job_obj}         = $job_obj;
+  $self->{invoices} = [];
+
+  $self->create_invoices;
+  $self->convert_invoices_to_pdf;
+  $self->print_pdfs;
+
+  $job_obj->set_data(status => DONE())->save;
+
+  return 1;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::BackgroundJob::MassRecordCreationAndPrinting
+
+
+=head1 SYNOPSIS
+
+In controller:
+
+use SL::BackgroundJob::MassRecordCreationAndPrinting
+
+my $job              = SL::DB::BackgroundJob->new(
+    type               => 'once',
+    active             => 1,
+    package_name       => 'MassRecordCreationAndPrinting',
+
+  )->set_data(
+    record_ids         => [ map { $_->id } @records[0..$num - 1] ],
+    printer_id         => $::form->{printer_id},
+    status             => SL::BackgroundJob::MassRecordCreationAndPrinting->WAITING_FOR_EXECUTION(),
+    num_created        => 0,
+    num_printed        => 0,
+    invoice_ids        => [ ],
+    conversion_errors  => [ ],
+    print_errors       => [ ],
+
+  )->update_next_run_at;
+  SL::System::TaskServer->new->wake_up;
+
+=head1 OVERVIEW
+
+This background job has 4 states which are described by the four constants above.
+
+=over 2
+
+=item * WAITING_FOR_EXECUTION
+  Background has been initialised and needs to be picked up by the task_server
+
+=item * CONVERTING_DELIVERY_ORDERS
+   Object conversion
+
+=item * PRINTING_INVOICES
+  Printing, if done via print command
+
+=item * DONE
+  To release the process and for the user information
+
+=back
+
+=head1 FUNCTIONS
+
+=over 2
+
+=item C<create_invoices>
+
+Converts the source objects (DeliveryOrder) to destination objects (Invoice).
+On success objects will be saved.
+
+=item C<convert_invoices_to_pdf>
+
+Takes the new destination objects and merges them via print template in one pdf.
+
+=item C<print_pdfs>
+
+Sent the pdf to the printer command (if checked).
+
+=back
+
+=head1 BUGS
+Currently the calculation from the gui (form) differs from the calculation via convert (PTC).
+Furthermore mass conversion with foreign currencies could lead to problems (daily rate check).
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+Jan Büren E<lt>jan@kivitendo-premium.deE<gt>
+=cut
diff --git a/SL/Controller/MassInvoiceCreatePrint.pm b/SL/Controller/MassInvoiceCreatePrint.pm
new file mode 100644 (file)
index 0000000..0714757
--- /dev/null
@@ -0,0 +1,497 @@
+package SL::Controller::MassInvoiceCreatePrint;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use File::Slurp ();
+use List::MoreUtils qw(all uniq);
+use List::Util qw(first min);
+
+use SL::BackgroundJob::MassRecordCreationAndPrinting;
+use SL::Controller::Helper::GetModels;
+use SL::DB::DeliveryOrder;
+use SL::DB::Order;
+use SL::DB::Printer;
+use SL::Helper::CreatePDF qw(:all);
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::SessionFile;
+use SL::System::TaskServer;
+
+use Rose::Object::MakeMethods::Generic
+(
+  'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id js) ],
+);
+
+__PACKAGE__->run_before('setup');
+
+#
+# actions
+#
+
+sub action_list_sales_delivery_orders {
+  my ($self) = @_;
+
+  # default is usually no show, exception here
+  my $show = ($::form->{noshow} ? 0 : 1);
+  delete $::form->{noshow};
+
+  # if a filter is choosen, the filter info should be visible
+  $self->make_filter_summary;
+  $self->sales_delivery_order_models->get;
+  $self->render('mass_invoice_create_print_from_do/list_sales_delivery_orders',
+                noshow  => $show,
+                title   => $::locale->text('Open sales delivery orders'));
+}
+
+sub action_create_invoices {
+  my ($self) = @_;
+
+  my @sales_delivery_order_ids = @{ $::form->{id} || [] };
+  if (!@sales_delivery_order_ids) {
+    # should never be executed, double catch via js
+    flash_later('error', t8('No delivery orders have been selected.'));
+    return $self->redirect_to(action => 'list_sales_delivery_orders');
+  }
+
+  my $db = SL::DB::Invoice->new->db;
+
+  if (!$db->do_transaction(sub {
+    my @invoices;
+    foreach my $id (@sales_delivery_order_ids) {
+      my $delivery_order    = SL::DB::DeliveryOrder->new(id => $id)->load;
+
+      my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
+      push @invoices, $invoice;
+    }
+
+    my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
+    $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
+
+    flash_later('info', t8('The invoices have been created. They\'re pre-selected below.'));
+    $self->redirect_to(action => 'list_invoices', ids => $key);
+
+    1;
+  })) {
+    $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
+    $::form->error($db->error);
+  }
+}
+
+sub action_list_invoices {
+  my ($self) = @_;
+
+  my $show = $::form->{noshow} ? 0 : 1;
+  delete $::form->{noshow};
+
+  if ($::form->{ids}) {
+    my $key = 'MassInvoiceCreatePrint::ids-' . $::form->{ids};
+    $self->invoice_ids($::auth->get_session_value($key) || []);
+    $self->invoice_models->add_additional_url_params(ids => $::form->{ids});
+  }
+
+  my %selected_ids = map { +($_ => 1) } @{ $self->invoice_ids };
+
+  $::form->{printer_id} ||= $self->default_printer_id;
+
+  $self->render('mass_invoice_create_print_from_do/list_invoices',
+                title        => $::locale->text('Open invoice'),
+                noshow       => $show,
+                selected_ids => \%selected_ids);
+}
+
+sub action_print {
+  my ($self) = @_;
+
+  my @invoices = map { SL::DB::Invoice->new(id => $_)->load } @{ $::form->{id} || [] };
+  if (!@invoices) {
+    flash_later('error', t8('No invoices have been selected.'));
+    return $self->redirect_to(action => 'list_invoices');
+  }
+
+  $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices);
+}
+
+sub action_create_print_all_start {
+  my ($self) = @_;
+
+  $self->sales_delivery_order_models->disable_plugin('paginated');
+
+  my @records           = @{ $self->sales_delivery_order_models->get };
+  my $num              = min(scalar(@records), $::form->{number_of_invoices} // scalar(@records));
+
+  my $job              = SL::DB::BackgroundJob->new(
+    type               => 'once',
+    active             => 1,
+    package_name       => 'MassRecordCreationAndPrinting',
+
+  )->set_data(
+    record_ids         => [ map { $_->id } @records[0..$num - 1] ],
+    printer_id         => $::form->{printer_id},
+    status             => SL::BackgroundJob::MassRecordCreationAndPrinting->WAITING_FOR_EXECUTION(),
+    num_created        => 0,
+    num_printed        => 0,
+    invoice_ids        => [ ],
+    conversion_errors  => [ ],
+    print_errors       => [ ],
+
+  )->update_next_run_at;
+
+  SL::System::TaskServer->new->wake_up;
+
+  my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
+
+  $self->js
+    ->html('#create_print_all_dialog', $html)
+    ->run('kivi.MassInvoiceCreatePrint.massConversionStarted')
+    ->render;
+}
+
+sub action_create_print_all_status {
+  my ($self) = @_;
+  my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
+  my $html   = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
+
+  $self->js->html('#create_print_all_dialog', $html);
+  $self->js->run('kivi.MassInvoiceCreatePrint.massConversionFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::MassRecordCreationAndPrinting->DONE();
+  $self->js->render;
+}
+
+sub action_create_print_all_download {
+  my ($self) = @_;
+  my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
+
+  my $sfile  = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
+  die $! if !$sfile->fh;
+
+  my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
+  $sfile->fh->close;
+
+  my $type      = 'Invoices';
+  my $file_name =  t8($type) . '-' . DateTime->today_local->strftime('%Y%m%d%H%M%S') . '.pdf';
+  $file_name    =~ s{[^\w\.]+}{_}g;
+
+  return $self->send_file(
+    \$merged_pdf,
+    type => 'application/pdf',
+    name => $file_name,
+  );
+}
+
+#
+# filters
+#
+
+sub init_js       { SL::ClientJS->new(controller => $_[0]) }
+sub init_printers { SL::DB::Manager::Printer->get_all_sorted }
+sub init_invoice_ids { [] }
+
+sub init_sales_delivery_order_models {
+  my ($self) = @_;
+  return $self->_init_sales_delivery_order_models(sortby => 'donumber');
+}
+
+sub _init_sales_delivery_order_models {
+  my ($self, %params) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller   => $_[0],
+    model        => 'DeliveryOrder',
+    # model        => 'Order',
+    sorted       => {
+      _default     => {
+        by           => $params{sortby},
+        dir          => 1,
+      },
+      customer     => t8('Customer'),
+      employee     => t8('Employee'),
+      transdate    => t8('Date'),
+      donumber     => t8('Delivery Order Number'),
+      ordnumber     => t8('Order Number'),
+    },
+    with_objects => [ qw(customer employee) ],
+   query        => [
+      '!customer_id' => undef,
+      or             => [ closed    => undef, closed    => 0 ],
+      or             => [ delivered => undef, delivered => 0 ],
+    ],
+  );
+}
+
+
+sub init_invoice_models {
+  my ($self)             = @_;
+  my @invoice_ids = @{ $self->invoice_ids };
+
+  SL::Controller::Helper::GetModels->new(
+    controller   => $_[0],
+    model        => 'Invoice',
+    (paginated   => 0,) x !!@invoice_ids,
+    sorted       => {
+      _default     => {
+        by           => 'transdate',
+        dir          => 0,
+      },
+      customer     => t8('Customer'),
+      invnumber    => t8('Invoice Number'),
+      employee     => t8('Employee'),
+      donumber     => t8('Delivery Order Number'),
+      ordnumber    => t8('Order Number'),
+      reqdate      => t8('Delivery Date'),
+      transdate    => t8('Date'),
+    },
+    with_objects => [ qw(customer employee) ],
+    query        => [
+      '!customer_id' => undef,
+      (id            => \@invoice_ids) x !!@invoice_ids,
+    ],
+  );
+}
+
+
+sub init_default_printer_id {
+  my $pr = SL::DB::Manager::Printer->find_by(printer_description => $::locale->text("sales_invoice_printer"));
+  return $pr ? $pr->id : undef;
+}
+
+sub setup {
+  my ($self) = @_;
+  $::auth->assert('invoice_edit');
+
+  $::request->layout->use_javascript("${_}.js")  for qw(kivi.MassInvoiceCreatePrint);
+}
+
+#
+# helpers
+#
+
+sub create_pdfs {
+  my ($self, %params) = @_;
+
+  my @pdf_file_names;
+  foreach my $invoice (@{ $params{invoices} }) {
+    my %create_params = (
+      template  => $self->find_template(name => 'invoice', printer_id => $params{printer_id}),
+      variables => Form->new(''),
+      return    => 'file_name',
+    );
+
+    $create_params{variables}->{$_} = $params{variables}->{$_} for keys %{ $params{variables} };
+
+    $invoice->flatten_to_form($create_params{variables}, format_amounts => 1);
+    $create_params{variables}->prepare_for_printing;
+
+    push @pdf_file_names, $self->create_pdf(%create_params);
+  }
+
+  return @pdf_file_names;
+}
+
+sub download_or_print_documents {
+  my ($self, %params) = @_;
+
+  my @pdf_file_names;
+
+  eval {
+    my %pdf_params = (
+      invoices        => $params{invoices},
+      printer_id      => $params{printer_id},
+      variables       => {
+        type        => 'invoice',
+        formname    => 'invoice',
+        format      => 'pdf',
+        media       => $params{printer_id} ? 'printer' : 'file',
+      });
+
+    @pdf_file_names = $self->create_pdfs(%pdf_params);
+    my $merged_pdf  = $self->merge_pdfs(file_names => \@pdf_file_names);
+    unlink @pdf_file_names;
+
+    if (!$params{printer_id}) {
+      my $file_name =  t8("Invoices") . '-' . DateTime->today_local->strftime('%Y%m%d%H%M%S') . '.pdf';
+      $file_name    =~ s{[^\w\.]+}{_}g;
+
+      return $self->send_file(
+        \$merged_pdf,
+        type => 'application/pdf',
+        name => $file_name,
+      );
+    }
+
+    my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
+    my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($printer->printer_command);
+
+    open my $out, '|-', $command or die $!;
+    binmode $out;
+    print $out $merged_pdf;
+    close $out;
+
+    flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
+    return $self->redirect_to(action => 'list_invoices', printer_id => $params{printer_id});
+
+  } or do {
+    unlink @pdf_file_names;
+    $::form->error(t8("Creating the PDF failed:") . " " . $@);
+  };
+}
+
+sub make_filter_summary {
+  my ($self) = @_;
+
+  my $filter = $::form->{filter} || {};
+  my @filter_strings;
+
+  my @filters = (
+    [ $filter->{customer}{"name:substr::ilike"}, t8('Customer') ],
+    [ $filter->{"transdate:date::ge"},           t8('Delivery Date') . " " . t8('From Date') ],
+    [ $filter->{"transdate:date::le"},           t8('Delivery Date') . " " . t8('To Date')   ],
+  );
+
+  for (@filters) {
+    push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
+  }
+
+  $self->{filter_summary} = join ', ', @filter_strings;
+}
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
+
+=head2 OVERVIEW
+
+Controller class for the conversion and processing (printing) of objects.
+
+
+Inherited from the base controller class, this controller implements the Sales Mass Invoice Creation.
+In general there are two major distinctions:
+This class implements the conversion and the printing via clickable action AND triggers the same
+conversion towards a Background-Job with a good user interaction.
+
+Analysis hints: All this is more or less boilerplate code around the great convert_to_invoice method
+in DeliverOrder.pm. If you need to debug stuff, take a look at the corresponding test case
+($ t/test.pl t/db_helper/convert_invoices.t). There are some redundant code parts in this controller
+and in the background job, i.e. compare the actions create and print.
+From a reverse engineering point of view the actions in this controller were written before the
+background job existed, therefore if anything goes boom take a look at the single steps done via gui
+in this controller and after that take a deeper look at the MassRecordCreationAndPrinting job.
+
+
+=head1 FUNCTIONS
+
+=over 2
+
+=item C<action_list_sales_delivery_orders>
+
+List all open sales delivery orders. The filter can be in two states show or "no show" the
+original, probably gorash idea, is to increase performance and not to be forced to click on the
+next button (like in all other reports). Therefore use this option and this filter for a good
+project default and hide it again. Filters can be added in _filter.html. Take a look at
+  SL::Controlle::Helper::GetModels::Filtered.pm and SL::DB::Helper::Filtered.
+
+=item C<action_create_invoices>
+
+Creates or to be more correctly converts delivery orders to invoices. All items are
+converted 1:1 and after conversion the delivery order(s) are closed.
+
+=item C<action_list_invoices>
+
+List the created invoices, if created via gui (see action above)
+
+=item C<action_print>
+
+Print the selected invoices. Yes, it really is all boring linear (see action above).
+Calls download_or_print method.
+
+=item C<action_create_print_all_start>
+
+Initialises the webform for the creation and printing via background job. Now we get to
+the more fun part ...  Mosu did a great user interaction job here, we can choose how many
+objects are converted in one strike and if or if not they are downloadable or will be sent to
+a printer (if defined as a printing command) right away.
+Background job is started and status is watched via js and the next action.
+
+=item C<action_create_print_all_status>
+
+Action for watching status, default is refreshing every 5 seconds
+
+=item C<action_create_print_all_download>
+
+If the above is done (did I already said: boring linear?). Documents will
+be either printed or downloaded.
+
+=item C<init_js>
+
+Inits js/kivi.MassInvoiceCreatePrint;
+
+=item C<init_printers>
+
+Gets all printer commands
+
+=item C<init_invoice_ids>
+
+Gets a list of (empty) invoice ids
+
+=item C<init_sales_delivery_order_models>
+
+Calls _init_sales_delivery_order_models with a param
+
+=item C<_init_sales_delivery_order_models>
+
+Private function, called by init_sales_delivery_order_models.
+Gets all open sales delivery orders.
+
+=item C<init_invoice_models>
+
+Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
+
+=item C<init_default_printer_id>
+
+Gets the default printer for sales_invoices. Maybe this function is not used, but
+might be useful in the next version (working in client project).
+
+=item C<setup>
+
+Currently sets / checks only the access right.
+
+=item C<create_pdfs>
+
+=item C<download_or_print_documents>
+
+Backend function for printing or downloading docs. Only used for gui processing (called
+via action_print).
+
+=item C<make_filter_summary>
+Creates the filter option summary in the header. By the time of writing three filters are
+supported: Customer and date from/to of the Delivery Order (database field transdate).
+
+=back
+
+=head1 TODO
+
+Should be more generalized. Right now just one conversion (delivery order to invoice) is supported.
+Using BackgroundJobs to mass create / transfer stuff is the way to do it. The original idea
+was taken from one client project (mosu) with some extra (maybe not standard compliant) customized
+stuff (using cvars for extra filters and a very compressed Controller for linking (ODSalesOrder.pm)).
+
+Filtering needs to be extended for Delivery Order Number (Natural Sort).
+
+A second printer (copy) needs to be implemented.
+
+Both todos are marked in the template code.
+
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+Jan Büren E<lt>jan@kivitendo-premium.deE<gt>
+=cut
index 7cfc2ea..b6f1f95 100644 (file)
@@ -187,6 +187,39 @@ sub customervendor {
   $_[0]->is_sales ? $_[0]->customer : $_[0]->vendor;
 }
 
+sub convert_to_invoice {
+  my ($self, %params) = @_;
+
+  croak("Conversion to invoices is only supported for sales records") unless $self->customer_id;
+
+  my $invoice;
+  if (!$self->db->with_transaction(sub {
+    require SL::DB::Invoice;
+    $invoice = SL::DB::Invoice->new_from($self)->post(%params) || die;
+    $self->link_to_record($invoice);
+    foreach my $item (@{ $invoice->items }) {
+      foreach (qw(delivery_order_items)) {    # expand if needed (delivery_order_items)
+        if ($item->{"converted_from_${_}_id"}) {
+          die unless $item->{id};
+          RecordLinks->create_links('mode'       => 'ids',
+                                    'from_table' => $_,
+                                    'from_ids'   => $item->{"converted_from_${_}_id"},
+                                    'to_table'   => 'invoice',
+                                    'to_id'      => $item->{id},
+          ) || die;
+          delete $item->{"converted_from_${_}_id"};
+        }
+      }
+    }
+    $self->update_attributes(closed => 1);
+    1;
+  })) {
+    return undef;
+  }
+
+  return $invoice;
+}
+
 1;
 __END__
 
@@ -288,6 +321,21 @@ TODO: Describe sales_order
 Returns a string describing this record's type: either
 C<sales_delivery_order> or C<purchase_delivery_order>.
 
+=item C<convert_to_invoice %params>
+
+Creates a new invoice with C<$self> as the basis by calling
+L<SL::DB::Invoice::new_from>. That invoice is posted, and C<$self> is
+linked to the new invoice via L<SL::DB::RecordLink>. C<$self>'s
+C<closed> attribute is set to C<true>, and C<$self> is saved.
+
+The arguments in C<%params> are passed to L<SL::DB::Invoice::post>.
+
+Returns the new invoice instance on success and C<undef> on
+failure. The whole process is run inside a transaction. On failure
+nothing is created or changed in the database.
+
+At the moment only sales delivery orders can be converted.
+
 =back
 
 =head1 BUGS
diff --git a/js/kivi.MassInvoiceCreatePrint.js b/js/kivi.MassInvoiceCreatePrint.js
new file mode 100644 (file)
index 0000000..b27ab6a
--- /dev/null
@@ -0,0 +1,77 @@
+namespace('kivi.MassInvoiceCreatePrint', function(ns) {
+  this.checkSalesOrderSelection = function() {
+    if ($("[data-checkall=1]:checked").size() > 0)
+      return true;
+    alert(kivi.t8('No delivery orders have been selected.'));
+    return false;
+  };
+
+  this.checkDeliveryOrderSelection = function() {
+    if ($("[data-checkall=1]:checked").size() > 0)
+      return true;
+    alert(kivi.t8('No delivery orders have been selected.'));
+    return false;
+  };
+  this.checkInvoiceSelection = function() {
+    if ($("[data-checkall=1]:checked").size() > 0)
+      return true;
+    alert(kivi.t8('No invoices have been selected.'));
+    return false;
+  };
+
+  this.submitMassCreationForm = function() {
+    if (!kivi.MassInvoiceCreatePrint.checkDeliveryOrderSelection())
+      return false;
+
+    $('body').addClass('loading');
+    $('form').submit();
+    return false;
+  };
+
+  this.createPrintAllInitialize = function() {
+    kivi.popup_dialog({
+      id: 'create_print_all_dialog',
+      dialog: {
+        title: kivi.t8('Create and print all invoices')
+      }
+    });
+  };
+
+  this.createPrintAllStartProcess = function() {
+    $('#cpa_start_process_button,.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', 'disabled');
+    $('#cpa_start_process_abort_link').remove();
+
+    var data = {
+      number_of_invoices: $('#cpa_number_of_invoices').val(),
+      printer_id:         $('#cpa_printer_id').val()
+    };
+    kivi.submit_ajax_form('controller.pl?action=MassInvoiceCreatePrint/create_print_all_start', '[name^=filter\\.]', data);
+  };
+
+  this.createPrintAllFinishProcess = function() {
+    $('#create_print_all_dialog').dialog('close');
+    window.location.href = 'controller.pl?action=MassInvoiceCreatePrint%2flist_invoices&noshow=1';
+  };
+
+  this.massConversionStarted = function() {
+    $('#create_print_all_dialog').data('timerId', setInterval(function() {
+      $.get("controller.pl", {
+        action: 'MassInvoiceCreatePrint/create_print_all_status',
+        job_id: $('#cpa_job_id').val()
+      }, kivi.eval_json_result);
+    }, 5000));
+  };
+
+  this.massConversionFinished = function() {
+    clearInterval($('#create_print_all_dialog').data('timerId'));
+    $('.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', '')
+  };
+
+  this.setup = function() {
+    $('#create_button').click(kivi.MassInvoiceCreatePrint.submitMassCreationForm);
+    $('#create_print_all_button').click(kivi.MassInvoiceCreatePrint.createPrintAllInitialize);
+    $('#action_print').click(kivi.MassInvoiceCreatePrint.checkInvoiceSelection);
+  };
+});
+
+$(kivi.MassInvoiceCreatePrint.setup);
index 5fba49c..64d3d71 100644 (file)
   module: letter.pl
   params:
     action: add
+- parent: ar
+  id: ar_invoices
+  name: Invoices
+  icon: sales_invoice_add
+  order: 850
+- parent: ar_invoices
+  id: ar_invoices_mass_add_sales_invoice
+  name: Mass Create Print Sales Invoice from Delivery Order
+  order: 100
+  access: invoice_edit
+  params:
+    noshow: 1
+    action: MassInvoiceCreatePrint/list_sales_delivery_orders
 - parent: ar
   id: ar_reports
   name: Reports
diff --git a/t/db_helper/convert_invoice.t b/t/db_helper/convert_invoice.t
new file mode 100644 (file)
index 0000000..088bb08
--- /dev/null
@@ -0,0 +1,340 @@
+use Test::More tests => 38;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Support::TestSetup;
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use Test::Exception;
+use List::Util qw(max);
+
+use SL::DB::Buchungsgruppe;
+use SL::DB::Currency;
+use SL::DB::Customer;
+use SL::DB::Employee;
+use SL::DB::Invoice;
+use SL::DB::Order;
+use SL::DB::DeliveryOrder;
+use SL::DB::Part;
+use SL::DB::Unit;
+use SL::DB::TaxZone;
+
+my ($customer, $currency_id, $buchungsgruppe, $employee, $vendor, $taxzone, $buchungsgruppe7, $tax, $tax7,
+    $unit, @parts);
+
+my $VISUAL_TEST = 0;  # just a sleep to click around
+
+sub clear_up {
+  foreach (qw(DeliveryOrderItem DeliveryOrder InvoiceItem Invoice Part Customer Vendor Employee Department PaymentTerm)) {
+    "SL::DB::Manager::${_}"->delete_all(all => 1);
+  }
+};
+
+sub reset_state {
+  my %params = @_;
+
+  clear_up();
+
+  $buchungsgruppe   = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group 19\%";
+  $buchungsgruppe7  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 7%', %{ $params{buchungsgruppe} })  || croak "No accounting group 7\%";
+  $taxzone          = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                                           || croak "No taxzone";
+  $tax              = SL::DB::Manager::Tax->find_by(taxkey => 3, rate => 0.19, %{ $params{tax} })                           || croak "No tax for 19\%";
+  $tax7             = SL::DB::Manager::Tax->find_by(taxkey => 2, rate => 0.07)                                              || croak "No tax for 7\%";
+  $unit             = SL::DB::Manager::Unit->find_by(name => 'kg', %{ $params{unit} })                                      || croak "No unit";
+  $currency_id     = $::instance_conf->get_currency_id;
+
+  $customer     = SL::DB::Customer->new(
+    name        => '520484567dfaedc9e60fc',
+    currency_id => $currency_id,
+    taxzone_id  => $taxzone->id,
+    %{ $params{customer} }
+  )->save;
+
+  # some od.rnr real anonym data
+  my $employee_bk = SL::DB::Employee->new(
+                'id' => 31915,
+                'login' => 'barbuschka.kappes',
+                'name' => 'Barbuschka Kappes',
+  )->save;
+
+  my $department_do = SL::DB::Department->new(
+                 'description' => 'Maisenhaus-Versand',
+                 'id' => 32149,
+                 'itime' => undef,
+                 'mtime' => undef
+  )->save;
+
+  my $payment_do = SL::DB::PaymentTerm->new(
+                 'description' => '14Tage 2%Skonto, 30Tage netto',
+                 'description_long' => "Innerhalb von 14 Tagen abzüglich 2 % Skonto, innerhalb von 30 Tagen rein netto.|Bei einer Zahlung bis zum <%skonto_date%> gewähren wir 2 % Skonto (EUR <%skonto_amount%>) entspricht EUR <%total_wo_skonto%>.Bei einer Zahlung bis zum <%netto_date%> ist der fällige Betrag in Höhe von <%total%> <%currency%> zu überweisen.",
+                 'id' => 11276,
+                 'itime' => undef,
+                 'mtime' => undef,
+                 'percent_skonto' => '0.02',
+                 'ranking' => undef,
+                 'sortkey' => 4,
+                 'terms_netto' => 30,
+                 'auto_calculation' => undef,
+                 'terms_skonto' => 14
+  )->save;
+
+  # two real parts
+  @parts = ();
+  push @parts, SL::DB::Part->new(
+                 'id' => 26321,
+                 'image' => '',
+                 'lastcost' => '49.95000',
+                 'listprice' => '0.00000',
+                 'onhand' => '5.00000',
+                 'partnumber' => 'v-519160549',
+                 #'partsgroup_id' => 111645,
+                 'rop' => '0',
+                 'sellprice' => '242.20000',
+                 #'warehouse_id' => 64702,
+                 'weight' => '0.79',
+                 description        => "Nussbaum, Gr.5, Unterfilz weinrot, genietet[[Aufschnittbreite: 11,0, Kernform: US]]\"" ,
+                 buchungsgruppen_id => $buchungsgruppe->id,
+                 unit               => $unit->name,
+                 id                 => 26321,
+  )->save;
+
+  push @parts, SL::DB::Part->new(
+                 'description' => "[[0640]]Flügel Hammerstiele bestehend aus:
+70 Stielen Standard in Weißbuche und
+20 Stielen Diskant abgekehlt in Weißbuche
+mit Röllchen aus Synthetikleder,
+Kapseln mit Yamaha Profil, Kerbenabstand 3,6 mm mit eingedrehten Abnickschrauben",
+                 'id' => 25505,
+                 'lastcost' => '153.00000',
+                 'listprice' => '0.00000',
+                 'onhand' => '9.00000',
+                 'partnumber' => 'v-120160086',
+                 # 'partsgroup_id' => 111639,
+                 'rop' => '0',
+                 'sellprice' => '344.30000',
+                 'weight' => '0.9',
+                  buchungsgruppen_id => $buchungsgruppe->id,
+                  unit               => $unit->name,
+  )->save;
+}
+
+sub new_delivery_order {
+  my %params  = @_;
+
+  return SL::DB::DeliveryOrder->new(
+   currency_id => $currency_id,
+   taxzone_id  => $taxzone->id,
+    %params,
+  )->save;
+}
+
+Support::TestSetup::login();
+
+reset_state();
+
+# we create L20199 with two items
+my $do1 = new_delivery_order('department_id'    => 32149,
+                             'donumber'         => 'L20199',
+                             'employee_id'      => 31915,
+                             'intnotes'         => 'Achtung: Neue Lieferadresse ab 16.02.2015 in der Carl-von-Ossietzky-Str.32!   13.02.2015/MH
+
+                                            Steinway-Produkte (201...) immer plus 25% dazu rechnen / BK 13.02.2014',
+                              'ordnumber'       => 'A16399',
+                              'payment_id'      => 11276,
+                              'salesman_id'     => 31915,
+                              'shippingpoint'   => 'Maisenhaus',
+                              # 'shipto_id'     => 451463,
+                              'is_sales'        => 'true',
+                              'shipvia'         => 'DHL, Versand am 06.03.2015, 1 Paket  17,00 kg',
+                              'taxzone_id'      => 4,
+                              'closed'          => undef,
+                              # 'currency_id'   => 1,
+                              'cusordnumber'    => 'b84da',
+                              'customer_id'     => $customer->id,
+                              'id'              => 464003,
+);
+
+my $do1_item1 = SL::DB::DeliveryOrderItem->new('delivery_order_id' => 464003,
+                                               'description' => "Flügel Hammerkopf bestehend aus:
+                                                                 Bass/Diskant 26/65 Stück, Gesamtlänge 80/72, Bohrlänge 56/48
+                                                                 Nussbaum, Gr.5, Unterfilz weinrot, genietet[[Aufschnittbreite: 11,0, Kernform: US]]",
+                                               'discount' => '0.25',
+                                               'id' => 144736,
+                                               'lastcost' => '49.95000',
+                                               'longdescription' => '',
+                                               'marge_price_factor' => 1,
+                                               'mtime' => undef,
+                                               'ordnumber' => 'A16399',
+                                               'parts_id' => 26321,
+                                               'position' => 1,
+                                               'price_factor' => 1,
+                                               'qty' => '2.00000',
+                                               'sellprice' => '242.20000',
+                                               'transdate' => '06.03.2015',
+                                               'unit' => 'kg')->save;
+
+my $do1_item2 = SL::DB::DeliveryOrderItem->new('delivery_order_id' => 464003,
+                 'description' => "[[0640]]Flügel Hammerstiele bestehend aus:
+70 Stielen Standard in Weißbuche und
+20 Stielen Diskant abgekehlt in Weißbuche
+mit Röllchen aus Synthetikleder,
+Kapseln mit Yamaha Profil, Kerbenabstand 3,6 mm mit eingedrehten Abnickschrauben",
+                 'discount' => '0.25',
+                 'id' => 144737,
+                 'itime' => undef,
+                 'lastcost' => '153.00000',
+                 'longdescription' => '',
+                 'marge_price_factor' => 1,
+                 'mtime' => undef,
+                 'ordnumber' => 'A16399',
+                 'parts_id' => 25505,
+                 'position' => 2,
+                 'price_factor' => 1,
+                 'price_factor_id' => undef,
+                 'pricegroup_id' => undef,
+                 'project_id' => undef,
+                 'qty' => '3.00000',
+                 'reqdate' => undef,
+                 'sellprice' => '344.30000',
+                 'serialnumber' => '',
+                 'transdate' => '06.03.2015',
+                 'unit' => 'kg')->save;
+
+# TESTS
+
+
+# test delivery order before any conversion
+ok($do1->donumber eq "L20199", 'Delivery Order Number created');
+ok((not $do1->closed) , 'Delivery Order is not closed');
+ok($do1_item1->parts_id eq '26321', 'doi linked with part');
+ok($do1_item1->qty == 2, 'qty check doi');
+ok($do1_item2->position == 2, 'doi2 position check');
+ok(2 ==  scalar@{ SL::DB::Manager::DeliveryOrderItem->get_all(where => [ delivery_order_id => $do1->id ]) }, 'two doi linked');
+
+
+# convert this do to invoice
+my $invoice = $do1->convert_to_invoice();
+
+sleep (300) if $VISUAL_TEST; # we can do a real visual test via gui login
+# test invoice afterwards
+
+ok ($invoice->shipvia eq "DHL, Versand am 06.03.2015, 1 Paket  17,00 kg", "ship via check");
+ok ($invoice->shippingpoint eq "Maisenhaus", "shipping point check");
+ok ($invoice->ordnumber eq "A16399", "ordnumber check");
+ok ($invoice->donumber eq "L20199", "donumber check");
+ok(($do1->closed) , 'Delivery Order is closed after conversion');
+ok (SL::DB::PaymentTerm->new(id => $invoice->{payment_id})->load->description eq "14Tage 2%Skonto, 30Tage netto", 'payment term description check');
+
+# some test data from original client invoice console (!)
+# my $invoice3 = SL::DB::Manager::Invoice->find_by( ordnumber => 'A16399' );
+# which will fail due to PTC Calculation differs from GUI-Calculation, see issue: http://redmine.kivitendo-premium.de/issues/82
+# pp $invoice3
+# values from gui should be:
+#ok($invoice->amount == 1354.20000, 'amount check');
+#ok($invoice->marge_percent == 50.88666, 'marge percent check');
+#ok($invoice->marge_total == 579.08000, 'marge total check');
+#ok($invoice->netamount == 1137.98000, 'netamount check');
+
+
+# the values change if one reloads the object
+# without reloading we get this failures
+#not ok 17 - amount check
+#   Failed test 'amount check'
+#   at t/db_helper/convert_invoice.t line 272.
+#          got: '1354.17'
+#     expected: '1354.17000'
+#not ok 18 - marge percent check
+#   Failed test 'marge percent check'
+#   at t/db_helper/convert_invoice.t line 273.
+#          got: '50.8857956342929'
+#     expected: '50.88580'
+#not ok 19 - marge total check
+#   Failed test 'marge total check'
+#   at t/db_helper/convert_invoice.t line 274.
+#          got: '579.06'
+#     expected: '579.06000'
+#not ok 20 - netamount check
+#   Failed test 'netamount check'
+#   at t/db_helper/convert_invoice.t line 275.
+#          got: '1137.96'
+#     expected: '1137.96000'
+
+$invoice->load;
+
+ok($invoice->currency_id eq '1', 'currency_id');
+ok($invoice->cusordnumber eq 'b84da', 'cusordnumber check');
+ok(SL::DB::Department->new(id => $invoice->{department_id})->load->description eq "Maisenhaus-Versand", 'department description');
+is($invoice->amount, '1354.17000', 'amount check');
+is($invoice->marge_percent, '50.88580', 'marge percent check');
+is($invoice->marge_total, '579.06000', 'marge total check');
+is($invoice->netamount, '1137.96000', 'netamount check');
+
+# some item checks
+ok(@ {$invoice->items_sorted}[0]->parts_id eq '26321', 'invoiceitem 1 linked with part');
+ok(2 ==  scalar@{ $invoice->invoiceitems }, 'two invoice items linked with invoice');
+is(@ {$invoice->items_sorted}[0]->position, 1, "position 1 order correct");
+is(@ {$invoice->items_sorted}[1]->position, 2, "position 2 order correct");
+is(@ {$invoice->items_sorted}[0]->part->partnumber, 'v-519160549', "partnumber 1 correct");
+is(@ {$invoice->items_sorted}[1]->part->partnumber, 'v-120160086', "partnumber 2 correct");
+is(@ {$invoice->items_sorted}[0]->qty, '2.00000', "pos 1 qty");
+is(@ {$invoice->items_sorted}[1]->qty, '3.00000', "pos 2 qty");
+is(@ {$invoice->items_sorted}[0]->discount, 0.25, "pos 1 discount");
+is(@ {$invoice->items_sorted}[1]->discount, 0.25, "pos 2 discount");
+
+# more ideas: check onhand, lastcost (parsed lastcost)
+
+
+
+# check linked records AND linked items
+
+# we expect something like this in record links:
+# delivery_order_items |  144736 | invoice  |     9 | 2015-09-02 16:29:32.362562 |  5
+# delivery_order_items |  144737 | invoice  |    10 | 2015-09-02 16:29:32.362562 |  6
+# delivery_orders      |  464003 | ar       |     5 | 2015-09-02 16:29:32.362562 |  7
+# wir erwarten:
+# verkn�pfter beleg$VAR1 = {
+#           'from_id' => 464003,
+#           'from_table' => 'delivery_orders',
+#           'to_id' => 11,
+#           'to_table' => 'ar'
+#         };
+# verkn�pfte positionen$VAR1 = {
+#           'from_id' => 144737,
+#           'from_table' => 'delivery_order_items',
+#           'to_id' => 22,
+#           'to_table' => 'invoice'
+#         };
+# $VAR2 = {
+#           'from_id' => 144736,
+#           'from_table' => 'delivery_order_items',
+#           'to_id' => 21,
+#           'to_table' => 'invoice'
+#         };
+
+
+
+my @links_record    = RecordLinks->get_links('from_table' => 'delivery_orders',
+                                             'to_table'   => 'ar',
+                                             'from_id'      => 464003);
+is($links_record[0]->{from_id}, '464003', "record from id check");
+is($links_record[0]->{from_table}, 'delivery_orders', "record from table check");
+is($links_record[0]->{to_table}, 'ar', "record to table check");
+
+foreach (qw(144736 144737)) {
+  my @links_record_item1 = RecordLinks->get_links('from_table' => 'delivery_order_items',
+                                                 'to_table'   => 'invoice',
+                                                 'from_id'      => $_);
+  is($links_record_item1[0]->{from_id}, $_, "record from id check $_");
+  is($links_record_item1[0]->{from_table}, 'delivery_order_items', "record from table check $_");
+  is($links_record_item1[0]->{to_table}, 'invoice', "record to table check $_");
+}
+
+
+clear_up();
+
+1;
diff --git a/templates/webpages/mass_invoice_create_print_from_do/_create_print_all_status.html b/templates/webpages/mass_invoice_create_print_from_do/_create_print_all_status.html
new file mode 100644 (file)
index 0000000..15f25f0
--- /dev/null
@@ -0,0 +1,106 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[% SET data = job.data_as_hash %]
+<h2>[% LxERP.t8("Step 2 -- Watch status") %]</h2>
+
+[% L.hidden_tag('', job.id, id="cpa_job_id") %]
+
+<p>
+ [% LxERP.t8("This status output will be refreshed every five seconds.") %]
+</p>
+
+<p>
+ [% IF data.status < 3 %]
+  [% L.link("login.pl?action=company_logo", LxERP.t8("Open new tab"), target="_blank") %]
+
+ [% ELSE %]
+ [% IF data.pdf_file_name %]
+   [% L.link(SELF.url_for(action="create_print_all_download", job_id=job.id), LxERP.t8("Download PDF")) %]
+ [% END %]
+ [% L.link("#", LxERP.t8("Close window"), onclick="kivi.MassInvoiceCreatePrint.createPrintAllFinishProcess();") %]
+[% END %]
+</p>
+
+<p>
+ <table>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Current status:") %]</th>
+   <td valign="top">
+    [% IF !data.status %]
+     [% LxERP.t8("waiting for job to be started") %]
+    [% ELSIF data.status == 1 %]
+     [% LxERP.t8("Creating invoices") %]
+    [% ELSIF data.status == 2 %]
+     [% LxERP.t8("Printing invoices (this can take a while)") %]
+    [% ELSE %]
+     [% LxERP.t8("Done.") %]
+     [% IF data.pdf_file_name %]
+      [% LxERP.t8("The file is available for download.") %]
+     [% ELSIF data.printer_id %]
+      [% LxERP.t8("The file has been sent to the printer.") %]
+     [% END %]
+    [% END %]
+   </td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Number of invoices created:") %]</th>
+   <td valign="top">[% IF data.status > 0 %][% HTML.escape(data.num_created) %] / [% HTML.escape(data.record_ids.size) %][% ELSE %]–[% END %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Number of invoices printed:") %]</th>
+   <td valign="top">[% IF data.status > 1 %][% HTML.escape(data.num_printed) %] / [% HTML.escape(data.invoice_ids.size) %][% ELSE %]–[% END %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Errors during conversion:") %]</th>
+   <td valign="top">
+[% IF !data.status %]
+  –
+[% ELSIF !data.conversion_errors.size %]
+ [% LxERP.t8("No errors have occurred.") %]
+[% ELSE %]
+    <table>
+     <tr class="listheader">
+      <th>[% LxERP.t8("Delivery Order") %]</th>
+      <th>[% LxERP.t8("Error") %]</th>
+     </tr>
+
+ [% FOREACH error = data.conversion_errors %]
+     <tr>
+      <td valign="top">[% IF error.id %][% L.link(SELF.url_for(controller='do.pl', action='edit', type='sales_delivery_order', id=error.id), HTML.escape(error.number), target="_blank") %][% ELSE %]–[% END %]</td>
+      <td valign="top">[% HTML.escape(error.message) %]</td>
+     </tr>
+ [% END %]
+    </table>
+[% END %]
+   </td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Errors during printing:") %]</th>
+   <td valign="top">
+[% IF data.status < 2 %]
+ –
+[% ELSIF !data.print_errors.size %]
+ [% LxERP.t8("No errors have occurred.") %]
+[% ELSE %]
+    <table>
+     <tr class="listheader">
+      <th>[% LxERP.t8("Invoice") %]</th>
+      <th>[% LxERP.t8("Error") %]</th>
+     </tr>
+
+ [% FOREACH error = data.print_errors %]
+     <tr>
+      <td valign="top">[% IF error.id %][% L.link(SELF.url_for(controller='is.pl', action='edit', type='sales_invoice',id=error.id), HTML.escape(error.number), target="_blank") %][% ELSE %]–[% END %]</td>
+      <td valign="top">[% HTML.escape(error.message) %]</td>
+     </tr>
+ [% END %]
+    </table>
+[% END %]
+   </td>
+  </tr>
+
+ </table>
+</p>
diff --git a/templates/webpages/mass_invoice_create_print_from_do/_create_print_all_step_1.html b/templates/webpages/mass_invoice_create_print_from_do/_create_print_all_step_1.html
new file mode 100644 (file)
index 0000000..794f518
--- /dev/null
@@ -0,0 +1,37 @@
+[%- USE LxERP -%][%- USE L -%]
+[%- SET num_delivery_orders = SELF.sales_delivery_order_models.count %]
+<h2>[% LxERP.t8("Step 1 -- limit number of delivery orders to process") %]</h2>
+
+<p>
+ [% LxERP.t8("Currently #1 delivery orders can be converted into invoices and printed.", num_delivery_orders) %]
+ [% LxERP.t8("How many do you want to create and print?") %]
+</p>
+
+<table>
+ <tr>
+  <td>[% LxERP.t8("Number of invoices to create") %]:</td>
+  <td>[% L.input_tag('', num_delivery_orders, size="5", id="cpa_number_of_invoices") %]</td>
+ </tr>
+
+ <tr>
+  <td>[% LxERP.t8("Print destination") %]:</td>
+  <td>
+    [% SET  printers = [ { description=LxERP.t8("Download PDF, do not print") } ] ;
+       CALL printers.import(SELF.printers);
+       L.select_tag("", printers, title_key="description", default=SELF.default_printer_id, id="cpa_printer_id") %]
+  </td>
+ </tr>
+ <!--tr>
+  <td>[% LxERP.t8("Print destination (copy)") %]:</td>
+  <td>
+    [% SET  printers = [ { description=LxERP.t8("Download PDF, do not print") } ] ;
+       CALL printers.import(SELF.printers);
+       L.select_tag("", printers, title_key="description", default=SELF.default_printer_id, id="cpa_printer_id") %]
+  </td>
+ </tr -->
+</table>
+
+<p>
+ [% L.button_tag("kivi.MassInvoiceCreatePrint.createPrintAllStartProcess();", LxERP.t8("Start process"), id="cpa_start_process_button") %]
+ [% L.link("#", LxERP.t8("Abort"), onclick="\$('#create_print_all_dialog').dialog('close');", id="cpa_start_process_abort_link") %]
+</p>
diff --git a/templates/webpages/mass_invoice_create_print_from_do/_filter.html b/templates/webpages/mass_invoice_create_print_from_do/_filter.html
new file mode 100644 (file)
index 0000000..fbddc5b
--- /dev/null
@@ -0,0 +1,46 @@
+[%- USE L %][%- USE LxERP %][%- USE HTML %]
+<div>
+ <form action="controller.pl" method="post">
+  <div class="filter_toggle" [% IF noshow == 0 %]style="display:none"[% END %]>
+   <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Show Filter') %]</a>
+      [% SELF.filter_summary %]
+  </div>
+
+  <div class="filter_toggle" [% IF noshow  == 1 %]style="display:none"[% END %]>
+   <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Hide Filter') %]</a>
+   <table id="filter_table">
+    <tr>
+     <th align="right">[% LxERP.t8('Customer') %]</th>
+     <td>[% L.input_tag('filter.customer.name:substr::ilike', filter.customer.name_substr__ilike, size = 20) %]</td>
+    </tr>
+     <th align="right">[% LxERP.t8('Delivery Date') %] [% LxERP.t8('From Date') %]</th>
+     <td>[% L.date_tag('filter.transdate:date::ge', filter.transdate_date__ge) %]</td>
+    </tr>
+    <tr>
+     <th align="right">[% LxERP.t8('Delivery Date') %] [% LxERP.t8('To Date') %]</th>
+     <td>[% L.date_tag('filter.transdate:date::le', filter.transdate_date__le) %]</td>
+    </tr>
+    <!-- TODO implement helper function nat sort here -->
+    <!-- tr>
+     <th align="right">[% LxERP.t8('From') %] [% LxERP.t8('Delivery Order Number') %]</th>
+     <td>[% L.input_tag('filter.donumber:number::ge', filter.donumber_number__ge) %]</td>
+    </tr>
+    <tr>
+     <th align="right">[% LxERP.t8('To') %] [% LxERP.t8('Delivery Order Number') %]</th>
+     <td>[% L.input_tag('filter.donumber:number::le', filter.donumber_number__le) %]</td>
+    </tr -->
+
+   </table>
+
+   [% L.hidden_tag('action', 'MassInvoiceCreatePrint/dispatch') %]
+   [% L.hidden_tag('sort_by', FORM.sort_by) %]
+   [% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+   [% L.hidden_tag('page', FORM.page) %]
+   [% L.submit_tag(LIST_ACTION, LxERP.t8('Continue'))%]
+
+   <a href="#" onClick="javascript:$('#filter_table input,#filter_table select').val('');">[% LxERP.t8('Reset') %]</a>
+
+  </div>
+
+ </form>
+</div>
diff --git a/templates/webpages/mass_invoice_create_print_from_do/list_invoices.html b/templates/webpages/mass_invoice_create_print_from_do/list_invoices.html
new file mode 100644 (file)
index 0000000..8bec62a
--- /dev/null
@@ -0,0 +1,77 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %]</h1>
+
+[%- INCLUDE "common/flash.html" %]
+
+[% LIST_ACTION     = 'action_list_invoices' %]
+[%- PROCESS 'mass_invoice_create_print_from_do/_filter.html' filter=SELF.filter %]
+
+[% IF noshow == 1 %]
+[% invoices = SELF.invoice_models.get;
+   MODELS          = SELF.invoice_models %]
+[%- IF !invoices.size %]
+ <p>
+  [%- LxERP.t8("There are currently no open invoices, or none matches your filter conditions.") %]
+ </p>
+[%- ELSE %]
+
+ <form method="post" action="controller.pl">
+  <table width="100%">
+   <thead>
+    <tr class="listheading">
+     <th>[% L.checkbox_tag("", id="check_all", checkall="[data-checkall=1]") %]</th>
+     <th>[% L.sortable_table_header("transdate") %]</th>
+     <th>[% L.sortable_table_header("reqdate") %]</th>
+     <th>[% L.sortable_table_header("invnumber") %]</th>
+     <th>[% L.sortable_table_header("donumber") %]</th>
+     <th>[% L.sortable_table_header("customer") %]</th>
+     <th>[% LxERP.t8("Shipto") %]</th>
+    </tr>
+   </thead>
+
+   <tbody>
+    [%- FOREACH invoice = invoices %]
+     [% invoice_id = invoice.id
+        delivery_order    = invoice.delivery_order %]
+     <tr class="listrow">
+      <td>[% L.checkbox_tag('id[]', value=invoice.id, "data-checkall"=1, checked=selected_ids.$invoice_id) %]</td>
+      <td>[% HTML.escape(invoice.transdate_as_date) %]</td>
+      <td>[% HTML.escape(invoice.deliverydate_as_date) %]</td>
+      <td>[% L.link(SELF.url_for(controller="is.pl", action="edit", type="sales_invoice", id=invoice.id), invoice.invnumber) %]</td>
+      <td>
+       [% IF delivery_order %]
+        [% L.link(SELF.url_for(controller="do.pl", action="edit", id=delivery_order.id), delivery_order.donumber) %]
+       [% ELSE %]
+        [% HTML.escape(invoice.donumber) %]
+       [% END %]
+      </td>
+      <td>[% HTML.escape(invoice.customer.name) %]</td>
+      <td>[% HTML.escape(SELF.make_shipto_title(invoice.shipto || delivery_order.custom_shipto)) %]</td>
+     </tr>
+    [%- END %]
+   </tbody>
+  </table>
+
+  [% IF !SELF.invoice_ids.size %]
+   [% L.paginate_controls %]
+  [% END %]
+
+  <hr size="3" noshade>
+
+  [% IF SELF.printers.size %]
+   <p>
+    [% LxERP.t8("Print destination") %]:
+    [% SET  printers = [ { description=LxERP.t8("Download PDF, do not print") } ] ;
+       CALL printers.import(SELF.printers);
+       L.select_tag("printer_id", printers, title_key="description", default=FORM.printer_id) %]
+   </p>
+  [% END %]
+
+  <p>
+   [% L.hidden_tag("action", "MassInvoiceCreatePrint/dispatch") %]
+   [% L.submit_tag("action_print", LxERP.t8("Print")) %]
+  </p>
+ </form>
+[%- END %]
+[%- END %]
diff --git a/templates/webpages/mass_invoice_create_print_from_do/list_sales_delivery_orders.html b/templates/webpages/mass_invoice_create_print_from_do/list_sales_delivery_orders.html
new file mode 100644 (file)
index 0000000..7d2ac03
--- /dev/null
@@ -0,0 +1,60 @@
+[% USE Dumper %][% USE HTML %][% USE L %][% USE LxERP %]
+
+
+<h1>[% FORM.title %]</h1>
+
+[%- INCLUDE "common/flash.html" %]
+
+[% LIST_ACTION  = 'action_list_sales_delivery_orders' %]
+[% SET MODELS = SELF.sales_delivery_order_models;
+       dummy  = MODELS.finalize %]
+
+[%- PROCESS 'mass_invoice_create_print_from_do/_filter.html' filter=SELF.sales_delivery_order_models.filtered.laundered  %]
+
+[% IF noshow == 1 %]
+  [% SET sales_delivery_orders = MODELS.get %]
+   <form method="post" action="controller.pl">
+  [% IF !sales_delivery_orders.size %]
+     <p>
+    [%- LxERP.t8("There are currently no open sales delivery orders.") %]
+     </p>
+  [%- ELSE %]
+    <table width="100%">
+     <thead>
+      <tr class="listheading">
+       <th>[% L.checkbox_tag("", id="check_all", checkall="[data-checkall=1]") %]</th>
+       <th>[% L.sortable_table_header("transdate") %]</th>
+       <th>[% L.sortable_table_header("donumber") %]</th>
+       <th>[% L.sortable_table_header("ordnumber") %]</th>
+      <th>[% L.sortable_table_header("customer") %]</th>
+     </tr>
+     </thead>
+
+     <tbody>
+     [%- FOREACH sales_delivery_order = sales_delivery_orders %]
+     <tr class="listrow">
+      <td>[% L.checkbox_tag('id[]', value=sales_delivery_order.id, "data-checkall"=1) %]</td>
+      <td>[% HTML.escape(sales_delivery_order.transdate_as_date) %]</td>
+      <td>[% L.link(SELF.url_for(controller="do.pl", action="edit", type="sales_delivery_order", id=sales_delivery_order.id), sales_delivery_order.donumber) %]</td>
+      <td>[% L.link(SELF.url_for(controller="oe.pl", action="edit", type="sales_order", id=sales_delivery_order.sales_order.id), sales_delivery_order.ordnumber) %]</td>
+      <td>[% HTML.escape(sales_delivery_order.customer.name) %]</td>
+     </tr>
+     [%- END %]
+     </tbody>
+    </table>
+
+    [% L.paginate_controls %]
+
+    <hr size="3" noshade>
+
+    <p>
+     [% L.hidden_tag("action", "MassInvoiceCreatePrint/create_invoices") %]
+     [% L.button_tag("", LxERP.t8("Create invoices"), name="create_button") %]
+     [% L.button_tag("", LxERP.t8("For all delivery orders create and print invoices"), name="create_print_all_button") %]
+    </p>
+    <div id="create_print_all_dialog" style="display: none;">
+     [%- INCLUDE 'mass_invoice_create_print_from_do/_create_print_all_step_1.html' %]
+    </div>
+  [%- END %]
+ </form>
+[%- END %]