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;
 
  23 use SL::System::TaskServer;
 
  24 use Rose::Object::MakeMethods::Generic
 
  26   'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id today all_businesses) ],
 
  29 __PACKAGE__->run_before('setup');
 
  35 sub action_list_sales_delivery_orders {
 
  38   # default is usually no show, exception here
 
  39   my $show = ($::form->{noshow} ? 0 : 1);
 
  40   delete $::form->{noshow};
 
  42   # if a filter is choosen, the filter info should be visible
 
  43   $self->make_filter_summary;
 
  44   $self->setup_list_sales_delivery_orders_action_bar(show_creation_buttons => $show, num_rows => scalar(@{ $self->sales_delivery_order_models->get }));
 
  45   $self->render('mass_invoice_create_print_from_do/list_sales_delivery_orders',
 
  47                 title   => $::locale->text('Open sales delivery orders'));
 
  50 sub action_create_invoices {
 
  53   my @sales_delivery_order_ids = @{ $::form->{id} || [] };
 
  54   if (!@sales_delivery_order_ids) {
 
  55     # should never be executed, double catch via js
 
  56     flash_later('error', t8('No delivery orders have been selected.'));
 
  57     return $self->redirect_to(action => 'list_sales_delivery_orders');
 
  60   my $db = SL::DB::Invoice->new->db;
 
  63   my @already_closed_delivery_orders;
 
  65   if (!$db->with_transaction(sub {
 
  66     foreach my $id (@sales_delivery_order_ids) {
 
  67       my $delivery_order    = SL::DB::DeliveryOrder->new(id => $id)->load;
 
  69       # Only process open delivery orders. In this list should only be open
 
  70       # delivery orders, but if the user clicked browser back, a new creation
 
  71       # of invoices for delivery orders which are closed now can be triggered.
 
  73       if ($delivery_order->closed) {
 
  74         push @already_closed_delivery_orders, $delivery_order;
 
  77         my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
 
  79         ARAP->close_orders_if_billed('dbh'     => $dbh,
 
  80                                      'arap_id' => $invoice->id,
 
  83         push @invoices, $invoice;
 
  89     $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
 
  90     $::form->error($db->error);
 
  93   foreach my $invoice( @invoices ) {
 
  95     my @linked_shop_orders = $invoice->linked_records(
 
  97       via       => [ 'DeliveryOrder', 'Order' ],
 
  99     #if (scalar @linked_shop_orders[0][0] >= 1){
 
 101     my $shop_order = $linked_shop_orders[0][0];
 
 104       my $shop_config = SL::DB::Manager::Shop->get_first( query => [ id => $shop_order->shop_id ] );
 
 105       my $shop = SL::Shop->new( config => $shop_config );
 
 106       $shop->connector->set_orderstatus($shop_order->shop_trans_id, "completed");
 
 110   my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
 
 111   $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
 
 113   if (@already_closed_delivery_orders) {
 
 114     my $dos_list = join ' ', map { $_->donumber } @already_closed_delivery_orders;
 
 115     flash_later('error', t8('The following delivery orders could not be processed because they are already closed: #1', $dos_list));
 
 118   flash_later('info', t8('The invoices have been created. They\'re pre-selected below.')) if @invoices;
 
 120   $self->redirect_to(action => 'list_invoices', ids => $key);
 
 123 sub action_list_invoices {
 
 126   my $show = $::form->{noshow} ? 0 : 1;
 
 127   delete $::form->{noshow};
 
 129   if ($::form->{ids}) {
 
 130     my $key = 'MassInvoiceCreatePrint::ids-' . $::form->{ids};
 
 131     $self->invoice_ids($::auth->get_session_value($key) || []);
 
 133     # Prevent models->get to retrieve any invoices if session key is there
 
 134     # but no ids are given.
 
 135     $self->invoice_ids([0]) if !@{$self->invoice_ids};
 
 137     $self->invoice_models->add_additional_url_params(ids => $::form->{ids});
 
 140   my %selected_ids = map { +($_ => 1) } @{ $self->invoice_ids };
 
 142   $::form->{printer_id} ||= $self->default_printer_id;
 
 144   $self->setup_list_invoices_action_bar(num_rows => scalar(@{ $self->invoice_models->get }));
 
 146   $self->render('mass_invoice_create_print_from_do/list_invoices',
 
 147                 title        => $::locale->text('Open invoice'),
 
 149                 selected_ids => \%selected_ids);
 
 155   my @invoices = map { SL::DB::Invoice->new(id => $_)->load } @{ $::form->{id} || [] };
 
 157     flash_later('error', t8('No invoices have been selected.'));
 
 158     return $self->redirect_to(action => 'list_invoices');
 
 161   $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices, bothsided => $::form->{bothsided});
 
 164 sub action_create_print_all_start {
 
 167   $self->sales_delivery_order_models->disable_plugin('paginated');
 
 169   my @records          = @{ $self->sales_delivery_order_models->get };
 
 170   my $num              = min(scalar(@records), $::form->{number_of_invoices} // scalar(@records));
 
 172   my $job              = SL::DB::BackgroundJob->new(
 
 175     package_name       => 'MassRecordCreationAndPrinting',
 
 178     record_ids         => [ map { $_->id } @records[0..$num - 1] ],
 
 179     printer_id         => $::form->{printer_id},
 
 180     copy_printer_id    => $::form->{copy_printer_id},
 
 181     bothsided          => ($::form->{bothsided}?1:0),
 
 182     transdate          => $::form->{transdate},
 
 183     status             => SL::BackgroundJob::MassRecordCreationAndPrinting->WAITING_FOR_EXECUTION(),
 
 187     conversion_errors  => [ ],
 
 189     session_id         => $::auth->get_session_id,
 
 191   )->update_next_run_at;
 
 193   SL::System::TaskServer->new->wake_up;
 
 195   my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
 
 198     ->html('#create_print_all_dialog', $html)
 
 199     ->run('kivi.MassInvoiceCreatePrint.massConversionStarted')
 
 203 sub action_create_print_all_status {
 
 205   my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
 
 206   my $html   = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
 
 208   $self->js->html('#create_print_all_dialog', $html);
 
 209   $self->js->run('kivi.MassInvoiceCreatePrint.massConversionFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::MassRecordCreationAndPrinting->DONE();
 
 213 sub action_create_print_all_download {
 
 215   my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
 
 217   my $sfile  = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
 
 218   die $! if !$sfile->fh;
 
 220   my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
 
 223   my $type      = 'Invoices';
 
 224   my $file_name =  t8($type) . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
 
 225   $file_name    =~ s{[^\w\.]+}{_}g;
 
 227   return $self->send_file(
 
 229     type => 'application/pdf',
 
 238 sub init_printers { SL::DB::Manager::Printer->get_all_sorted }
 
 239 #sub init_att      { require SL::Controller::Attachments; SL::Controller::Attachments->new() }
 
 240 sub init_invoice_ids { [] }
 
 241 sub init_today         { DateTime->today_local }
 
 243 sub init_sales_delivery_order_models {
 
 245   return $self->_init_sales_delivery_order_models(sortby => 'donumber');
 
 248 sub _init_sales_delivery_order_models {
 
 249   my ($self, %params) = @_;
 
 251   SL::Controller::Helper::GetModels->new(
 
 253     model        => 'DeliveryOrder',
 
 257         by           => $params{sortby},
 
 260       customer     => t8('Customer'),
 
 261       employee     => t8('Employee'),
 
 262       transdate    => t8('Delivery Order Date'),
 
 263       donumber     => t8('Delivery Order Number'),
 
 264       ordnumber     => t8('Order Number'),
 
 266     with_objects => [ qw(customer employee) ],
 
 268       '!customer_id' => undef,
 
 269       or             => [ closed    => undef, closed    => 0 ],
 
 275 sub init_invoice_models {
 
 277   my @invoice_ids = @{ $self->invoice_ids };
 
 279   SL::Controller::Helper::GetModels->new(
 
 282     (paginated   => 0,) x !!@invoice_ids,
 
 288       customer     => t8('Customer'),
 
 289       invnumber    => t8('Invoice Number'),
 
 290       employee     => t8('Employee'),
 
 291       donumber     => t8('Delivery Order Number'),
 
 292       ordnumber    => t8('Order Number'),
 
 293       reqdate      => t8('Delivery Date'),
 
 294       transdate    => t8('Date'),
 
 296     with_objects => [ qw(customer employee) ],
 
 298       '!customer_id' => undef,
 
 299       (id            => \@invoice_ids) x !!@invoice_ids,
 
 305 sub init_default_printer_id {
 
 306   my $pr = SL::DB::Manager::Printer->find_by(printer_description => $::locale->text("sales_invoice_printer"));
 
 307   return $pr ? $pr->id : undef;
 
 310 sub init_all_businesses {
 
 311   return SL::DB::Manager::Business->get_all_sorted;
 
 316   $::auth->assert('invoice_edit');
 
 318   $::request->layout->use_javascript("${_}.js")  for qw(kivi.MassInvoiceCreatePrint);
 
 326 sub download_or_print_documents {
 
 327   my ($self, %params) = @_;
 
 333       documents       => $params{invoices},
 
 336         formname    => 'invoice',
 
 338         media       => $params{printer_id} ? 'printer' : 'file',
 
 339         printer_id  => $params{printer_id},
 
 342     @pdf_file_names = $self->create_pdfs(%pdf_params);
 
 343     my $merged_pdf  = $self->merge_pdfs(file_names => \@pdf_file_names, bothsided => $params{bothsided});
 
 344     unlink @pdf_file_names;
 
 346     if (!$params{printer_id}) {
 
 347       my $file_name =  t8("Invoices") . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
 
 348       $file_name    =~ s{[^\w\.]+}{_}g;
 
 350       return $self->send_file(
 
 352         type => 'application/pdf',
 
 357     my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
 
 358     $printer->print_document(content => $merged_pdf);
 
 360     flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
 
 361     return $self->redirect_to(action => 'list_invoices', printer_id => $params{printer_id});
 
 364     unlink @pdf_file_names;
 
 365     $::form->error(t8("Creating the PDF failed:") . " " . $@);
 
 369 sub make_filter_summary {
 
 372   my $filter = $::form->{filter} || {};
 
 376     [ $filter->{customer}{"name:substr::ilike"}, t8('Customer') ],
 
 377     [ $filter->{"transdate:date::ge"},           t8('Delivery Order Date') . " " . t8('From Date') ],
 
 378     [ $filter->{"transdate:date::le"},           t8('Delivery Order Date') . " " . t8('To Date')   ],
 
 382     push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
 
 385   $self->{filter_summary} = join ', ', @filter_strings;
 
 388 sub setup_list_invoices_action_bar {
 
 389   my ($self, %params) = @_;
 
 391   for my $bar ($::request->layout->get('actionbar')) {
 
 395         submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_invoices' } ],
 
 396         accesskey => 'enter',
 
 399         $::locale->text('Print'),
 
 400         call     => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
 
 401         disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
 
 407 sub setup_list_sales_delivery_orders_action_bar {
 
 408   my ($self, %params) = @_;
 
 410   for my $bar ($::request->layout->get('actionbar')) {
 
 413         $params{show_creation_buttons} ? t8('Update') : t8('Search'),
 
 414         submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_sales_delivery_orders' } ],
 
 415         accesskey => 'enter',
 
 421           tooltip => t8("Create and print invoices")
 
 424           t8("Create and print invoices for all selected delivery orders"),
 
 425           submit    => [ 'form', { action => 'MassInvoiceCreatePrint/create_invoices' } ],
 
 426           disabled  => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
 
 427           only_if   => $params{show_creation_buttons},
 
 428           checks    => [ 'kivi.MassInvoiceCreatePrint.checkDeliveryOrderSelection' ],
 
 433           t8("Create and print invoices for all delivery orders matching the filter"),
 
 434           call     => [ 'kivi.MassInvoiceCreatePrint.createPrintAllInitialize' ],
 
 435           disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
 
 436           only_if  => $params{show_creation_buttons},
 
 453 SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
 
 457 Controller class for the conversion and processing (printing) of objects.
 
 460 Inherited from the base controller class, this controller implements the Sales Mass Invoice Creation.
 
 461 In general there are two major distinctions:
 
 462 This class implements the conversion and the printing via clickable action AND triggers the same
 
 463 conversion towards a Background-Job with a good user interaction.
 
 465 Analysis hints: All this is more or less boilerplate code around the great convert_to_invoice method
 
 466 in DeliverOrder.pm. If you need to debug stuff, take a look at the corresponding test case
 
 467 ($ t/test.pl t/db_helper/convert_invoices.t). There are some redundant code parts in this controller
 
 468 and in the background job, i.e. compare the actions create and print.
 
 469 From a reverse engineering point of view the actions in this controller were written before the
 
 470 background job existed, therefore if anything goes boom take a look at the single steps done via gui
 
 471 in this controller and after that take a deeper look at the MassRecordCreationAndPrinting job.
 
 477 =item C<action_list_sales_delivery_orders>
 
 479 List all open sales delivery orders. The filter can be in two states show or "no show" the
 
 480 original, probably gorash idea, is to increase performance and not to be forced to click on the
 
 481 next button (like in all other reports). Therefore use this option and this filter for a good
 
 482 project default and hide it again. Filters can be added in _filter.html. Take a look at
 
 483   SL::Controlle::Helper::GetModels::Filtered.pm and SL::DB::Helper::Filtered.
 
 485 =item C<action_create_invoices>
 
 487 Creates or to be more correctly converts delivery orders to invoices. All items are
 
 488 converted 1:1 and after conversion the delivery order(s) are closed.
 
 490 =item C<action_list_invoices>
 
 492 List the created invoices, if created via gui (see action above)
 
 494 =item C<action_print>
 
 496 Print the selected invoices. Yes, it really is all boring linear (see action above).
 
 497 Calls download_or_print method.
 
 499 =item C<action_create_print_all_start>
 
 501 Initialises the webform for the creation and printing via background job. Now we get to
 
 502 the more fun part ...  Mosu did a great user interaction job here, we can choose how many
 
 503 objects are converted in one strike and if or if not they are downloadable or will be sent to
 
 504 a printer (if defined as a printing command) right away.
 
 505 Background job is started and status is watched via js and the next action.
 
 507 =item C<action_create_print_all_status>
 
 509 Action for watching status, default is refreshing every 5 seconds
 
 511 =item C<action_create_print_all_download>
 
 513 If the above is done (did I already said: boring linear?). Documents will
 
 514 be either printed or downloaded.
 
 516 =item C<init_printers>
 
 518 Gets all printer commands
 
 520 =item C<init_invoice_ids>
 
 522 Gets a list of (empty) invoice ids
 
 526 Gets the current day. Currently used in custom code.
 
 527 Has to be initialised (get_set_init) and can be used as default for
 
 528 a date tag like C<[% L.date_tag("transdate", SELF.today, id=transdate) %]>.
 
 530 =item C<init_sales_delivery_order_models>
 
 532 Calls _init_sales_delivery_order_models with a param
 
 534 =item C<_init_sales_delivery_order_models>
 
 536 Private function, called by init_sales_delivery_order_models.
 
 537 Gets all open sales delivery orders.
 
 539 =item C<init_invoice_models>
 
 541 Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
 
 543 =item C<init_default_printer_id>
 
 545 Gets the default printer for sales_invoices. Currently this function is not called, but
 
 546 might be useful in the next version.Calling template code and Controller already expect a default:
 
 547 C<L.select_tag("", printers, title_key="description", default=SELF.default_printer_id, id="cpa_printer_id") %]>
 
 551 Currently sets / checks only the access right.
 
 555 =item C<download_or_print_documents>
 
 557 Backend function for printing or downloading docs. Only used for gui processing (called
 
 560 =item C<make_filter_summary>
 
 561 Creates the filter option summary in the header. By the time of writing three filters are
 
 562 supported: Customer and date from/to of the Delivery Order (database field transdate).
 
 568 pShould be more generalized. Right now just one conversion (delivery order to invoice) is supported.
 
 569 Using BackgroundJobs to mass create / transfer stuff is the way to do it. The original idea
 
 570 was taken from one client project (mosu) with some extra (maybe not standard compliant) customized
 
 571 stuff (using cvars for extra filters and a very compressed Controller for linking (ODSalesOrder.pm)).
 
 576 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
 
 578 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>