1 package SL::Controller::MassInvoiceCreatePrint;
 
   5 use parent qw(SL::Controller::Base);
 
   8 use List::MoreUtils qw(all uniq);
 
   9 use List::Util qw(first min);
 
  11 use SL::BackgroundJob::MassRecordCreationAndPrinting;
 
  12 use SL::Controller::Helper::GetModels;
 
  13 use SL::DB::DeliveryOrder;
 
  16 use SL::Helper::MassPrintCreatePDF qw(:all);
 
  17 use SL::Helper::CreatePDF qw(:all);
 
  18 use SL::Helper::File qw(store_pdf append_general_pdf_attachments doc_storage_enabled);
 
  19 use SL::Helper::Flash;
 
  20 use SL::Locale::String;
 
  22 use SL::System::TaskServer;
 
  23 use Rose::Object::MakeMethods::Generic
 
  25   'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id today all_businesses) ],
 
  28 __PACKAGE__->run_before('setup');
 
  34 sub action_list_sales_delivery_orders {
 
  37   # default is usually no show, exception here
 
  38   my $show = ($::form->{noshow} ? 0 : 1);
 
  39   delete $::form->{noshow};
 
  41   # if a filter is choosen, the filter info should be visible
 
  42   $self->make_filter_summary;
 
  43   $self->setup_list_sales_delivery_orders_action_bar(show_creation_buttons => $show, num_rows => scalar(@{ $self->sales_delivery_order_models->get }));
 
  44   $self->render('mass_invoice_create_print_from_do/list_sales_delivery_orders',
 
  46                 title   => $::locale->text('Open sales delivery orders'));
 
  49 sub action_create_invoices {
 
  52   my @sales_delivery_order_ids = @{ $::form->{id} || [] };
 
  53   if (!@sales_delivery_order_ids) {
 
  54     # should never be executed, double catch via js
 
  55     flash_later('error', t8('No delivery orders have been selected.'));
 
  56     return $self->redirect_to(action => 'list_sales_delivery_orders');
 
  59   my $db = SL::DB::Invoice->new->db;
 
  61   my @already_closed_delivery_orders;
 
  63   if (!$db->with_transaction(sub {
 
  64     foreach my $id (@sales_delivery_order_ids) {
 
  65       my $delivery_order    = SL::DB::DeliveryOrder->new(id => $id)->load;
 
  67       # Only process open delivery orders. In this list should only be open
 
  68       # delivery orders, but if the user clicked browser back, a new creation
 
  69       # of invoices for delivery orders which are closed now can be triggered.
 
  71       if ($delivery_order->closed) {
 
  72         push @already_closed_delivery_orders, $delivery_order;
 
  75         my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
 
  76         push @invoices, $invoice;
 
  82     $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
 
  83     $::form->error($db->error);
 
  86   foreach my $invoice( @invoices ) {
 
  88     my @linked_shop_orders = $invoice->linked_records(
 
  90       via       => [ 'DeliveryOrder', 'Order' ],
 
  92     #if (scalar @linked_shop_orders[0][0] >= 1){
 
  94     my $shop_order = $linked_shop_orders[0][0];
 
  97       my $shop_config = SL::DB::Manager::Shop->get_first( query => [ id => $shop_order->shop_id ] );
 
  98       my $shop = SL::Shop->new( config => $shop_config );
 
  99       $shop->connector->set_orderstatus($shop_order->shop_trans_id, "completed");
 
 103   my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
 
 104   $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
 
 106   if (@already_closed_delivery_orders) {
 
 107     my $dos_list = join ' ', map { $_->donumber } @already_closed_delivery_orders;
 
 108     flash_later('error', t8('The following delivery orders could not be processed because they are already closed: #1', $dos_list));
 
 111   flash_later('info', t8('The invoices have been created. They\'re pre-selected below.')) if @invoices;
 
 113   $self->redirect_to(action => 'list_invoices', ids => $key);
 
 116 sub action_list_invoices {
 
 119   my $show = $::form->{noshow} ? 0 : 1;
 
 120   delete $::form->{noshow};
 
 122   if ($::form->{ids}) {
 
 123     my $key = 'MassInvoiceCreatePrint::ids-' . $::form->{ids};
 
 124     $self->invoice_ids($::auth->get_session_value($key) || []);
 
 126     # Prevent models->get to retrieve any invoices if session key is there
 
 127     # but no ids are given.
 
 128     $self->invoice_ids([0]) if !@{$self->invoice_ids};
 
 130     $self->invoice_models->add_additional_url_params(ids => $::form->{ids});
 
 133   my %selected_ids = map { +($_ => 1) } @{ $self->invoice_ids };
 
 135   $::form->{printer_id} ||= $self->default_printer_id;
 
 137   $self->setup_list_invoices_action_bar(num_rows => scalar(@{ $self->invoice_models->get }));
 
 139   $self->render('mass_invoice_create_print_from_do/list_invoices',
 
 140                 title        => $::locale->text('Open invoice'),
 
 142                 selected_ids => \%selected_ids);
 
 148   my @invoices = map { SL::DB::Invoice->new(id => $_)->load } @{ $::form->{id} || [] };
 
 150     flash_later('error', t8('No invoices have been selected.'));
 
 151     return $self->redirect_to(action => 'list_invoices');
 
 154   $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices, bothsided => $::form->{bothsided});
 
 157 sub action_create_print_all_start {
 
 160   $self->sales_delivery_order_models->disable_plugin('paginated');
 
 162   my @records          = @{ $self->sales_delivery_order_models->get };
 
 163   my $num              = min(scalar(@records), $::form->{number_of_invoices} // scalar(@records));
 
 165   my $job              = SL::DB::BackgroundJob->new(
 
 168     package_name       => 'MassRecordCreationAndPrinting',
 
 171     record_ids         => [ map { $_->id } @records[0..$num - 1] ],
 
 172     printer_id         => $::form->{printer_id},
 
 173     copy_printer_id    => $::form->{copy_printer_id},
 
 174     bothsided          => ($::form->{bothsided}?1:0),
 
 175     transdate          => $::form->{transdate},
 
 176     status             => SL::BackgroundJob::MassRecordCreationAndPrinting->WAITING_FOR_EXECUTION(),
 
 180     conversion_errors  => [ ],
 
 182     session_id         => $::auth->get_session_id,
 
 184   )->update_next_run_at;
 
 186   SL::System::TaskServer->new->wake_up;
 
 188   my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
 
 191     ->html('#create_print_all_dialog', $html)
 
 192     ->run('kivi.MassInvoiceCreatePrint.massConversionStarted')
 
 196 sub action_create_print_all_status {
 
 198   my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
 
 199   my $html   = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
 
 201   $self->js->html('#create_print_all_dialog', $html);
 
 202   $self->js->run('kivi.MassInvoiceCreatePrint.massConversionFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::MassRecordCreationAndPrinting->DONE();
 
 206 sub action_create_print_all_download {
 
 208   my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
 
 210   my $sfile  = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
 
 211   die $! if !$sfile->fh;
 
 213   my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
 
 216   my $type      = 'Invoices';
 
 217   my $file_name =  t8($type) . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
 
 218   $file_name    =~ s{[^\w\.]+}{_}g;
 
 220   return $self->send_file(
 
 222     type => 'application/pdf',
 
 231 sub init_printers { SL::DB::Manager::Printer->get_all_sorted }
 
 232 #sub init_att      { require SL::Controller::Attachments; SL::Controller::Attachments->new() }
 
 233 sub init_invoice_ids { [] }
 
 234 sub init_today         { DateTime->today_local }
 
 236 sub init_sales_delivery_order_models {
 
 238   return $self->_init_sales_delivery_order_models(sortby => 'donumber');
 
 241 sub _init_sales_delivery_order_models {
 
 242   my ($self, %params) = @_;
 
 244   SL::Controller::Helper::GetModels->new(
 
 246     model        => 'DeliveryOrder',
 
 250         by           => $params{sortby},
 
 253       customer     => t8('Customer'),
 
 254       employee     => t8('Employee'),
 
 255       transdate    => t8('Delivery Order Date'),
 
 256       donumber     => t8('Delivery Order Number'),
 
 257       ordnumber     => t8('Order Number'),
 
 259     with_objects => [ qw(customer employee) ],
 
 261       '!customer_id' => undef,
 
 262       or             => [ closed    => undef, closed    => 0 ],
 
 268 sub init_invoice_models {
 
 270   my @invoice_ids = @{ $self->invoice_ids };
 
 272   SL::Controller::Helper::GetModels->new(
 
 275     (paginated   => 0,) x !!@invoice_ids,
 
 281       customer     => t8('Customer'),
 
 282       invnumber    => t8('Invoice Number'),
 
 283       employee     => t8('Employee'),
 
 284       donumber     => t8('Delivery Order Number'),
 
 285       ordnumber    => t8('Order Number'),
 
 286       reqdate      => t8('Delivery Date'),
 
 287       transdate    => t8('Date'),
 
 289     with_objects => [ qw(customer employee) ],
 
 291       '!customer_id' => undef,
 
 292       (id            => \@invoice_ids) x !!@invoice_ids,
 
 298 sub init_default_printer_id {
 
 299   my $pr = SL::DB::Manager::Printer->find_by(printer_description => $::locale->text("sales_invoice_printer"));
 
 300   return $pr ? $pr->id : undef;
 
 303 sub init_all_businesses {
 
 304   return SL::DB::Manager::Business->get_all_sorted;
 
 309   $::auth->assert('invoice_edit');
 
 311   $::request->layout->use_javascript("${_}.js")  for qw(kivi.MassInvoiceCreatePrint);
 
 319 sub download_or_print_documents {
 
 320   my ($self, %params) = @_;
 
 326       documents       => $params{invoices},
 
 329         formname    => 'invoice',
 
 331         media       => $params{printer_id} ? 'printer' : 'file',
 
 332         printer_id  => $params{printer_id},
 
 335     @pdf_file_names = $self->create_pdfs(%pdf_params);
 
 336     my $merged_pdf  = $self->merge_pdfs(file_names => \@pdf_file_names, bothsided => $params{bothsided});
 
 337     unlink @pdf_file_names;
 
 339     if (!$params{printer_id}) {
 
 340       my $file_name =  t8("Invoices") . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
 
 341       $file_name    =~ s{[^\w\.]+}{_}g;
 
 343       return $self->send_file(
 
 345         type => 'application/pdf',
 
 350     my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
 
 351     $printer->print_document(content => $merged_pdf);
 
 353     flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
 
 354     return $self->redirect_to(action => 'list_invoices', printer_id => $params{printer_id});
 
 357     unlink @pdf_file_names;
 
 358     $::form->error(t8("Creating the PDF failed:") . " " . $@);
 
 362 sub make_filter_summary {
 
 365   my $filter = $::form->{filter} || {};
 
 369     [ $filter->{customer}{"name:substr::ilike"}, t8('Customer') ],
 
 370     [ $filter->{"transdate:date::ge"},           t8('Delivery Order Date') . " " . t8('From Date') ],
 
 371     [ $filter->{"transdate:date::le"},           t8('Delivery Order Date') . " " . t8('To Date')   ],
 
 375     push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
 
 378   $self->{filter_summary} = join ', ', @filter_strings;
 
 381 sub setup_list_invoices_action_bar {
 
 382   my ($self, %params) = @_;
 
 384   for my $bar ($::request->layout->get('actionbar')) {
 
 388         submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_invoices' } ],
 
 389         accesskey => 'enter',
 
 392         $::locale->text('Print'),
 
 393         call     => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
 
 394         disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
 
 400 sub setup_list_sales_delivery_orders_action_bar {
 
 401   my ($self, %params) = @_;
 
 403   for my $bar ($::request->layout->get('actionbar')) {
 
 406         $params{show_creation_buttons} ? t8('Update') : t8('Search'),
 
 407         submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_sales_delivery_orders' } ],
 
 408         accesskey => 'enter',
 
 414           tooltip => t8("Create and print invoices")
 
 417           t8("Create and print invoices for all selected delivery orders"),
 
 418           submit    => [ 'form', { action => 'MassInvoiceCreatePrint/create_invoices' } ],
 
 419           disabled  => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
 
 420           only_if   => $params{show_creation_buttons},
 
 421           checks    => [ 'kivi.MassInvoiceCreatePrint.checkDeliveryOrderSelection' ],
 
 426           t8("Create and print invoices for all delivery orders matching the filter"),
 
 427           call     => [ 'kivi.MassInvoiceCreatePrint.createPrintAllInitialize' ],
 
 428           disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
 
 429           only_if  => $params{show_creation_buttons},
 
 446 SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
 
 450 Controller class for the conversion and processing (printing) of objects.
 
 453 Inherited from the base controller class, this controller implements the Sales Mass Invoice Creation.
 
 454 In general there are two major distinctions:
 
 455 This class implements the conversion and the printing via clickable action AND triggers the same
 
 456 conversion towards a Background-Job with a good user interaction.
 
 458 Analysis hints: All this is more or less boilerplate code around the great convert_to_invoice method
 
 459 in DeliverOrder.pm. If you need to debug stuff, take a look at the corresponding test case
 
 460 ($ t/test.pl t/db_helper/convert_invoices.t). There are some redundant code parts in this controller
 
 461 and in the background job, i.e. compare the actions create and print.
 
 462 From a reverse engineering point of view the actions in this controller were written before the
 
 463 background job existed, therefore if anything goes boom take a look at the single steps done via gui
 
 464 in this controller and after that take a deeper look at the MassRecordCreationAndPrinting job.
 
 470 =item C<action_list_sales_delivery_orders>
 
 472 List all open sales delivery orders. The filter can be in two states show or "no show" the
 
 473 original, probably gorash idea, is to increase performance and not to be forced to click on the
 
 474 next button (like in all other reports). Therefore use this option and this filter for a good
 
 475 project default and hide it again. Filters can be added in _filter.html. Take a look at
 
 476   SL::Controlle::Helper::GetModels::Filtered.pm and SL::DB::Helper::Filtered.
 
 478 =item C<action_create_invoices>
 
 480 Creates or to be more correctly converts delivery orders to invoices. All items are
 
 481 converted 1:1 and after conversion the delivery order(s) are closed.
 
 483 =item C<action_list_invoices>
 
 485 List the created invoices, if created via gui (see action above)
 
 487 =item C<action_print>
 
 489 Print the selected invoices. Yes, it really is all boring linear (see action above).
 
 490 Calls download_or_print method.
 
 492 =item C<action_create_print_all_start>
 
 494 Initialises the webform for the creation and printing via background job. Now we get to
 
 495 the more fun part ...  Mosu did a great user interaction job here, we can choose how many
 
 496 objects are converted in one strike and if or if not they are downloadable or will be sent to
 
 497 a printer (if defined as a printing command) right away.
 
 498 Background job is started and status is watched via js and the next action.
 
 500 =item C<action_create_print_all_status>
 
 502 Action for watching status, default is refreshing every 5 seconds
 
 504 =item C<action_create_print_all_download>
 
 506 If the above is done (did I already said: boring linear?). Documents will
 
 507 be either printed or downloaded.
 
 509 =item C<init_printers>
 
 511 Gets all printer commands
 
 513 =item C<init_invoice_ids>
 
 515 Gets a list of (empty) invoice ids
 
 519 Gets the current day. Currently used in custom code.
 
 520 Has to be initialised (get_set_init) and can be used as default for
 
 521 a date tag like C<[% L.date_tag("transdate", SELF.today, id=transdate) %]>.
 
 523 =item C<init_sales_delivery_order_models>
 
 525 Calls _init_sales_delivery_order_models with a param
 
 527 =item C<_init_sales_delivery_order_models>
 
 529 Private function, called by init_sales_delivery_order_models.
 
 530 Gets all open sales delivery orders.
 
 532 =item C<init_invoice_models>
 
 534 Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
 
 536 =item C<init_default_printer_id>
 
 538 Gets the default printer for sales_invoices. Currently this function is not called, but
 
 539 might be useful in the next version.Calling template code and Controller already expect a default:
 
 540 C<L.select_tag("", printers, title_key="description", default=SELF.default_printer_id, id="cpa_printer_id") %]>
 
 544 Currently sets / checks only the access right.
 
 548 =item C<download_or_print_documents>
 
 550 Backend function for printing or downloading docs. Only used for gui processing (called
 
 553 =item C<make_filter_summary>
 
 554 Creates the filter option summary in the header. By the time of writing three filters are
 
 555 supported: Customer and date from/to of the Delivery Order (database field transdate).
 
 561 pShould be more generalized. Right now just one conversion (delivery order to invoice) is supported.
 
 562 Using BackgroundJobs to mass create / transfer stuff is the way to do it. The original idea
 
 563 was taken from one client project (mosu) with some extra (maybe not standard compliant) customized
 
 564 stuff (using cvars for extra filters and a very compressed Controller for linking (ODSalesOrder.pm)).
 
 569 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
 
 571 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>