Merge branch 'test' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / SL / Controller / MassInvoiceCreatePrint.pm
1 package SL::Controller::MassInvoiceCreatePrint;
2
3 use strict;
4
5 use parent qw(SL::Controller::Base);
6
7 use File::Slurp ();
8 use List::MoreUtils qw(all uniq);
9 use List::Util qw(first min);
10
11 use SL::BackgroundJob::MassRecordCreationAndPrinting;
12 use SL::Controller::Helper::GetModels;
13 use SL::DB::DeliveryOrder;
14 use SL::DB::Order;
15 use SL::DB::Printer;
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;
21 use SL::SessionFile;
22 use SL::ARAP;
23 use SL::System::TaskServer;
24 use Rose::Object::MakeMethods::Generic
25 (
26   'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id today all_businesses) ],
27 );
28
29 __PACKAGE__->run_before('setup');
30
31 #
32 # actions
33 #
34
35 sub action_list_sales_delivery_orders {
36   my ($self) = @_;
37
38   # default is usually no show, exception here
39   my $show = ($::form->{noshow} ? 0 : 1);
40   delete $::form->{noshow};
41
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',
46                 noshow  => $show,
47                 title   => $::locale->text('Open sales delivery orders'));
48 }
49
50 sub action_create_invoices {
51   my ($self) = @_;
52
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');
58   }
59
60   my $db = SL::DB::Invoice->new->db;
61   my $dbh = $db->dbh;
62   my @invoices;
63   my @already_closed_delivery_orders;
64
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;
68
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.
72       # Prevent this.
73       if ($delivery_order->closed) {
74         push @already_closed_delivery_orders, $delivery_order;
75
76       } else {
77         my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
78
79         ARAP->close_orders_if_billed('dbh'     => $dbh,
80                                      'arap_id' => $invoice->id,
81                                      'table'   => 'ar',);
82
83         push @invoices, $invoice;
84       }
85     }
86
87     1;
88   })) {
89     $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
90     $::form->error($db->error);
91   }
92
93   foreach my $invoice( @invoices ) {
94     # update shop status
95     my @linked_shop_orders = $invoice->linked_records(
96       from      => 'ShopOrder',
97       via       => [ 'DeliveryOrder', 'Order' ],
98     );
99     #if (scalar @linked_shop_orders[0][0] >= 1){
100       #do update
101     my $shop_order = $linked_shop_orders[0][0];
102     if ($shop_order){
103     require SL::Shop;
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");
107     }
108   }
109
110   my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
111   $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
112
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));
116   }
117
118   flash_later('info', t8('The invoices have been created. They\'re pre-selected below.')) if @invoices;
119
120   $self->redirect_to(action => 'list_invoices', ids => $key);
121 }
122
123 sub action_list_invoices {
124   my ($self) = @_;
125
126   my $show = $::form->{noshow} ? 0 : 1;
127   delete $::form->{noshow};
128
129   if ($::form->{ids}) {
130     my $key = 'MassInvoiceCreatePrint::ids-' . $::form->{ids};
131     $self->invoice_ids($::auth->get_session_value($key) || []);
132
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};
136
137     $self->invoice_models->add_additional_url_params(ids => $::form->{ids});
138   }
139
140   my %selected_ids = map { +($_ => 1) } @{ $self->invoice_ids };
141
142   $::form->{printer_id} ||= $self->default_printer_id;
143
144   $self->setup_list_invoices_action_bar(num_rows => scalar(@{ $self->invoice_models->get }));
145
146   $self->render('mass_invoice_create_print_from_do/list_invoices',
147                 title        => $::locale->text('Open invoice'),
148                 noshow       => $show,
149                 selected_ids => \%selected_ids);
150 }
151
152 sub action_print {
153   my ($self) = @_;
154
155   my @invoices = map { SL::DB::Invoice->new(id => $_)->load } @{ $::form->{id} || [] };
156   if (!@invoices) {
157     flash_later('error', t8('No invoices have been selected.'));
158     return $self->redirect_to(action => 'list_invoices');
159   }
160
161   $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices, bothsided => $::form->{bothsided});
162 }
163
164 sub action_create_print_all_start {
165   my ($self) = @_;
166
167   $self->sales_delivery_order_models->disable_plugin('paginated');
168
169   my @records          = @{ $self->sales_delivery_order_models->get };
170   my $num              = min(scalar(@records), $::form->{number_of_invoices} // scalar(@records));
171
172   my $job              = SL::DB::BackgroundJob->new(
173     type               => 'once',
174     active             => 1,
175     package_name       => 'MassRecordCreationAndPrinting',
176
177   )->set_data(
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(),
184     num_created        => 0,
185     num_printed        => 0,
186     invoice_ids        => [ ],
187     conversion_errors  => [ ],
188     print_errors       => [ ],
189     session_id         => $::auth->get_session_id,
190
191   )->update_next_run_at;
192
193   SL::System::TaskServer->new->wake_up;
194
195   my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
196
197   $self->js
198     ->html('#create_print_all_dialog', $html)
199     ->run('kivi.MassInvoiceCreatePrint.massConversionStarted')
200     ->render;
201 }
202
203 sub action_create_print_all_status {
204   my ($self) = @_;
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);
207
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();
210   $self->js->render;
211 }
212
213 sub action_create_print_all_download {
214   my ($self) = @_;
215   my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
216
217   my $sfile  = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
218   die $! if !$sfile->fh;
219
220   my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
221   $sfile->fh->close;
222
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;
226
227   return $self->send_file(
228     \$merged_pdf,
229     type => 'application/pdf',
230     name => $file_name,
231   );
232 }
233
234 #
235 # filters
236 #
237
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 }
242
243 sub init_sales_delivery_order_models {
244   my ($self) = @_;
245   return $self->_init_sales_delivery_order_models(sortby => 'donumber');
246 }
247
248 sub _init_sales_delivery_order_models {
249   my ($self, %params) = @_;
250
251   SL::Controller::Helper::GetModels->new(
252     controller   => $_[0],
253     model        => 'DeliveryOrder',
254     # model        => 'Order',
255     sorted       => {
256       _default     => {
257         by           => $params{sortby},
258         dir          => 1,
259       },
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'),
265     },
266     with_objects => [ qw(customer employee) ],
267    query        => [
268       '!customer_id' => undef,
269       or             => [ closed    => undef, closed    => 0 ],
270     ],
271   );
272 }
273
274
275 sub init_invoice_models {
276   my ($self)             = @_;
277   my @invoice_ids = @{ $self->invoice_ids };
278
279   SL::Controller::Helper::GetModels->new(
280     controller   => $_[0],
281     model        => 'Invoice',
282     (paginated   => 0,) x !!@invoice_ids,
283     sorted       => {
284       _default     => {
285         by           => 'transdate',
286         dir          => 0,
287       },
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'),
295     },
296     with_objects => [ qw(customer employee) ],
297     query        => [
298       '!customer_id' => undef,
299       (id            => \@invoice_ids) x !!@invoice_ids,
300     ],
301   );
302 }
303
304
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;
308 }
309
310 sub init_all_businesses {
311   return SL::DB::Manager::Business->get_all_sorted;
312 }
313
314 sub setup {
315   my ($self) = @_;
316   $::auth->assert('invoice_edit');
317
318   $::request->layout->use_javascript("${_}.js")  for qw(kivi.MassInvoiceCreatePrint);
319 }
320
321 #
322 # helpers
323 #
324
325
326 sub download_or_print_documents {
327   my ($self, %params) = @_;
328
329   my @pdf_file_names;
330
331   eval {
332     my %pdf_params = (
333       documents       => $params{invoices},
334       variables       => {
335         type        => 'invoice',
336         formname    => 'invoice',
337         format      => 'pdf',
338         media       => $params{printer_id} ? 'printer' : 'file',
339         printer_id  => $params{printer_id},
340       });
341
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;
345
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;
349
350       return $self->send_file(
351         \$merged_pdf,
352         type => 'application/pdf',
353         name => $file_name,
354       );
355     }
356
357     my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
358     $printer->print_document(content => $merged_pdf);
359
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});
362
363   } or do {
364     unlink @pdf_file_names;
365     $::form->error(t8("Creating the PDF failed:") . " " . $@);
366   };
367 }
368
369 sub make_filter_summary {
370   my ($self) = @_;
371
372   my $filter = $::form->{filter} || {};
373   my @filter_strings;
374
375   my @filters = (
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')   ],
379   );
380
381   for (@filters) {
382     push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
383   }
384
385   $self->{filter_summary} = join ', ', @filter_strings;
386 }
387
388 sub setup_list_invoices_action_bar {
389   my ($self, %params) = @_;
390
391   for my $bar ($::request->layout->get('actionbar')) {
392     $bar->add(
393       action => [
394         t8('Update'),
395         submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_invoices' } ],
396         accesskey => 'enter',
397       ],
398       action => [
399         $::locale->text('Print'),
400         call     => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
401         disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
402       ],
403     );
404   }
405 }
406
407 sub setup_list_sales_delivery_orders_action_bar {
408   my ($self, %params) = @_;
409
410   for my $bar ($::request->layout->get('actionbar')) {
411     $bar->add(
412       action => [
413         $params{show_creation_buttons} ? t8('Update') : t8('Search'),
414         submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_sales_delivery_orders' } ],
415         accesskey => 'enter',
416       ],
417
418       combobox => [
419         action => [
420           t8('Invoices'),
421           tooltip => t8("Create and print invoices")
422         ],
423         action => [
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' ],
429           only_once => 1,
430         ],
431
432         action => [
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},
437         ],
438       ],
439     );
440   }
441 }
442
443 1;
444
445 __END__
446
447 =pod
448
449 =encoding utf8
450
451 =head1 NAME
452
453 SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
454
455 =head2 OVERVIEW
456
457 Controller class for the conversion and processing (printing) of objects.
458
459
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.
464
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.
472
473 =head1 FUNCTIONS
474
475 =over 2
476
477 =item C<action_list_sales_delivery_orders>
478
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.
484
485 =item C<action_create_invoices>
486
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.
489
490 =item C<action_list_invoices>
491
492 List the created invoices, if created via gui (see action above)
493
494 =item C<action_print>
495
496 Print the selected invoices. Yes, it really is all boring linear (see action above).
497 Calls download_or_print method.
498
499 =item C<action_create_print_all_start>
500
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.
506
507 =item C<action_create_print_all_status>
508
509 Action for watching status, default is refreshing every 5 seconds
510
511 =item C<action_create_print_all_download>
512
513 If the above is done (did I already said: boring linear?). Documents will
514 be either printed or downloaded.
515
516 =item C<init_printers>
517
518 Gets all printer commands
519
520 =item C<init_invoice_ids>
521
522 Gets a list of (empty) invoice ids
523
524 =item C<init_today>
525
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) %]>.
529
530 =item C<init_sales_delivery_order_models>
531
532 Calls _init_sales_delivery_order_models with a param
533
534 =item C<_init_sales_delivery_order_models>
535
536 Private function, called by init_sales_delivery_order_models.
537 Gets all open sales delivery orders.
538
539 =item C<init_invoice_models>
540
541 Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
542
543 =item C<init_default_printer_id>
544
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") %]>
548
549 =item C<setup>
550
551 Currently sets / checks only the access right.
552
553 =item C<create_pdfs>
554
555 =item C<download_or_print_documents>
556
557 Backend function for printing or downloading docs. Only used for gui processing (called
558 via action_print).
559
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).
563
564 =back
565
566 =head1 TODO
567
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)).
572
573
574 =head1 AUTHOR
575
576 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
577
578 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>
579
580 =cut