acc300e0ae9f17698c0935fc34efefa992ea7306
[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::System::TaskServer;
23 use Rose::Object::MakeMethods::Generic
24 (
25   'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id today all_businesses) ],
26 );
27
28 __PACKAGE__->run_before('setup');
29
30 #
31 # actions
32 #
33
34 sub action_list_sales_delivery_orders {
35   my ($self) = @_;
36
37   # default is usually no show, exception here
38   my $show = ($::form->{noshow} ? 0 : 1);
39   delete $::form->{noshow};
40
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',
45                 noshow  => $show,
46                 title   => $::locale->text('Open sales delivery orders'));
47 }
48
49 sub action_create_invoices {
50   my ($self) = @_;
51
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');
57   }
58
59   my $db = SL::DB::Invoice->new->db;
60   my @invoices;
61   my @already_closed_delivery_orders;
62
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;
66
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.
70       # Prevent this.
71       if ($delivery_order->closed) {
72         push @already_closed_delivery_orders, $delivery_order;
73
74       } else {
75         my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
76         push @invoices, $invoice;
77       }
78     }
79
80     1;
81   })) {
82     $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
83     $::form->error($db->error);
84   }
85
86   my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
87   $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
88
89   if (@already_closed_delivery_orders) {
90     my $dos_list = join ' ', map { $_->donumber } @already_closed_delivery_orders;
91     flash_later('error', t8('The following delivery orders could not be processed because they are already closed: #1', $dos_list));
92   }
93
94   flash_later('info', t8('The invoices have been created. They\'re pre-selected below.')) if @invoices;
95
96   $self->redirect_to(action => 'list_invoices', ids => $key);
97 }
98
99 sub action_list_invoices {
100   my ($self) = @_;
101
102   my $show = $::form->{noshow} ? 0 : 1;
103   delete $::form->{noshow};
104
105   if ($::form->{ids}) {
106     my $key = 'MassInvoiceCreatePrint::ids-' . $::form->{ids};
107     $self->invoice_ids($::auth->get_session_value($key) || []);
108
109     # Prevent models->get to retrieve any invoices if session key is there
110     # but no ids are given.
111     $self->invoice_ids([0]) if !@{$self->invoice_ids};
112
113     $self->invoice_models->add_additional_url_params(ids => $::form->{ids});
114   }
115
116   my %selected_ids = map { +($_ => 1) } @{ $self->invoice_ids };
117
118   $::form->{printer_id} ||= $self->default_printer_id;
119
120   $self->setup_list_invoices_action_bar(num_rows => scalar(@{ $self->invoice_models->get }));
121
122   $self->render('mass_invoice_create_print_from_do/list_invoices',
123                 title        => $::locale->text('Open invoice'),
124                 noshow       => $show,
125                 selected_ids => \%selected_ids);
126 }
127
128 sub action_print {
129   my ($self) = @_;
130
131   my @invoices = map { SL::DB::Invoice->new(id => $_)->load } @{ $::form->{id} || [] };
132   if (!@invoices) {
133     flash_later('error', t8('No invoices have been selected.'));
134     return $self->redirect_to(action => 'list_invoices');
135   }
136
137   $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices, bothsided => $::form->{bothsided});
138 }
139
140 sub action_create_print_all_start {
141   my ($self) = @_;
142
143   $self->sales_delivery_order_models->disable_plugin('paginated');
144
145   my @records          = @{ $self->sales_delivery_order_models->get };
146   my $num              = min(scalar(@records), $::form->{number_of_invoices} // scalar(@records));
147
148   my $job              = SL::DB::BackgroundJob->new(
149     type               => 'once',
150     active             => 1,
151     package_name       => 'MassRecordCreationAndPrinting',
152
153   )->set_data(
154     record_ids         => [ map { $_->id } @records[0..$num - 1] ],
155     printer_id         => $::form->{printer_id},
156     copy_printer_id    => $::form->{copy_printer_id},
157     bothsided          => ($::form->{bothsided}?1:0),
158     transdate          => $::form->{transdate},
159     status             => SL::BackgroundJob::MassRecordCreationAndPrinting->WAITING_FOR_EXECUTION(),
160     num_created        => 0,
161     num_printed        => 0,
162     invoice_ids        => [ ],
163     conversion_errors  => [ ],
164     print_errors       => [ ],
165     session_id         => $::auth->get_session_id,
166
167   )->update_next_run_at;
168
169   SL::System::TaskServer->new->wake_up;
170
171   my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
172
173   $self->js
174     ->html('#create_print_all_dialog', $html)
175     ->run('kivi.MassInvoiceCreatePrint.massConversionStarted')
176     ->render;
177 }
178
179 sub action_create_print_all_status {
180   my ($self) = @_;
181   my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
182   my $html   = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
183
184   $self->js->html('#create_print_all_dialog', $html);
185   $self->js->run('kivi.MassInvoiceCreatePrint.massConversionFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::MassRecordCreationAndPrinting->DONE();
186   $self->js->render;
187 }
188
189 sub action_create_print_all_download {
190   my ($self) = @_;
191   my $job    = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
192
193   my $sfile  = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
194   die $! if !$sfile->fh;
195
196   my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
197   $sfile->fh->close;
198
199   my $type      = 'Invoices';
200   my $file_name =  t8($type) . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
201   $file_name    =~ s{[^\w\.]+}{_}g;
202
203   return $self->send_file(
204     \$merged_pdf,
205     type => 'application/pdf',
206     name => $file_name,
207   );
208 }
209
210 #
211 # filters
212 #
213
214 sub init_printers { SL::DB::Manager::Printer->get_all_sorted }
215 #sub init_att      { require SL::Controller::Attachments; SL::Controller::Attachments->new() }
216 sub init_invoice_ids { [] }
217 sub init_today         { DateTime->today_local }
218
219 sub init_sales_delivery_order_models {
220   my ($self) = @_;
221   return $self->_init_sales_delivery_order_models(sortby => 'donumber');
222 }
223
224 sub _init_sales_delivery_order_models {
225   my ($self, %params) = @_;
226
227   SL::Controller::Helper::GetModels->new(
228     controller   => $_[0],
229     model        => 'DeliveryOrder',
230     # model        => 'Order',
231     sorted       => {
232       _default     => {
233         by           => $params{sortby},
234         dir          => 1,
235       },
236       customer     => t8('Customer'),
237       employee     => t8('Employee'),
238       transdate    => t8('Delivery Order Date'),
239       donumber     => t8('Delivery Order Number'),
240       ordnumber     => t8('Order Number'),
241     },
242     with_objects => [ qw(customer employee) ],
243    query        => [
244       '!customer_id' => undef,
245       or             => [ closed    => undef, closed    => 0 ],
246     ],
247   );
248 }
249
250
251 sub init_invoice_models {
252   my ($self)             = @_;
253   my @invoice_ids = @{ $self->invoice_ids };
254
255   SL::Controller::Helper::GetModels->new(
256     controller   => $_[0],
257     model        => 'Invoice',
258     (paginated   => 0,) x !!@invoice_ids,
259     sorted       => {
260       _default     => {
261         by           => 'transdate',
262         dir          => 0,
263       },
264       customer     => t8('Customer'),
265       invnumber    => t8('Invoice Number'),
266       employee     => t8('Employee'),
267       donumber     => t8('Delivery Order Number'),
268       ordnumber    => t8('Order Number'),
269       reqdate      => t8('Delivery Date'),
270       transdate    => t8('Date'),
271     },
272     with_objects => [ qw(customer employee) ],
273     query        => [
274       '!customer_id' => undef,
275       (id            => \@invoice_ids) x !!@invoice_ids,
276     ],
277   );
278 }
279
280
281 sub init_default_printer_id {
282   my $pr = SL::DB::Manager::Printer->find_by(printer_description => $::locale->text("sales_invoice_printer"));
283   return $pr ? $pr->id : undef;
284 }
285
286 sub init_all_businesses {
287   return SL::DB::Manager::Business->get_all_sorted;
288 }
289
290 sub setup {
291   my ($self) = @_;
292   $::auth->assert('invoice_edit');
293
294   $::request->layout->use_javascript("${_}.js")  for qw(kivi.MassInvoiceCreatePrint);
295 }
296
297 #
298 # helpers
299 #
300
301
302 sub download_or_print_documents {
303   my ($self, %params) = @_;
304
305   my @pdf_file_names;
306
307   eval {
308     my %pdf_params = (
309       documents       => $params{invoices},
310       variables       => {
311         type        => 'invoice',
312         formname    => 'invoice',
313         format      => 'pdf',
314         media       => $params{printer_id} ? 'printer' : 'file',
315         printer_id  => $params{printer_id},
316       });
317
318     @pdf_file_names = $self->create_pdfs(%pdf_params);
319     my $merged_pdf  = $self->merge_pdfs(file_names => \@pdf_file_names, bothsided => $params{bothsided});
320     unlink @pdf_file_names;
321
322     if (!$params{printer_id}) {
323       my $file_name =  t8("Invoices") . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
324       $file_name    =~ s{[^\w\.]+}{_}g;
325
326       return $self->send_file(
327         \$merged_pdf,
328         type => 'application/pdf',
329         name => $file_name,
330       );
331     }
332
333     my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
334     $printer->print_document(content => $merged_pdf);
335
336     flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
337     return $self->redirect_to(action => 'list_invoices', printer_id => $params{printer_id});
338
339   } or do {
340     unlink @pdf_file_names;
341     $::form->error(t8("Creating the PDF failed:") . " " . $@);
342   };
343 }
344
345 sub make_filter_summary {
346   my ($self) = @_;
347
348   my $filter = $::form->{filter} || {};
349   my @filter_strings;
350
351   my @filters = (
352     [ $filter->{customer}{"name:substr::ilike"}, t8('Customer') ],
353     [ $filter->{"transdate:date::ge"},           t8('Delivery Order Date') . " " . t8('From Date') ],
354     [ $filter->{"transdate:date::le"},           t8('Delivery Order Date') . " " . t8('To Date')   ],
355   );
356
357   for (@filters) {
358     push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
359   }
360
361   $self->{filter_summary} = join ', ', @filter_strings;
362 }
363
364 sub setup_list_invoices_action_bar {
365   my ($self, %params) = @_;
366
367   for my $bar ($::request->layout->get('actionbar')) {
368     $bar->add(
369       action => [
370         t8('Update'),
371         submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_invoices' } ],
372         accesskey => 'enter',
373       ],
374       action => [
375         $::locale->text('Print'),
376         call     => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
377         disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
378       ],
379     );
380   }
381 }
382
383 sub setup_list_sales_delivery_orders_action_bar {
384   my ($self, %params) = @_;
385
386   for my $bar ($::request->layout->get('actionbar')) {
387     $bar->add(
388       action => [
389         $params{show_creation_buttons} ? t8('Update') : t8('Search'),
390         submit    => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_sales_delivery_orders' } ],
391         accesskey => 'enter',
392       ],
393
394       combobox => [
395         action => [
396           t8('Invoices'),
397           tooltip => t8("Create and print invoices")
398         ],
399         action => [
400           t8("Create and print invoices for all selected delivery orders"),
401           submit    => [ 'form', { action => 'MassInvoiceCreatePrint/create_invoices' } ],
402           disabled  => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
403           only_if   => $params{show_creation_buttons},
404           checks    => [ 'kivi.MassInvoiceCreatePrint.checkDeliveryOrderSelection' ],
405           only_once => 1,
406         ],
407
408         action => [
409           t8("Create and print invoices for all delivery orders matching the filter"),
410           call     => [ 'kivi.MassInvoiceCreatePrint.createPrintAllInitialize' ],
411           disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
412           only_if  => $params{show_creation_buttons},
413         ],
414       ],
415     );
416   }
417 }
418
419 1;
420
421 __END__
422
423 =pod
424
425 =encoding utf8
426
427 =head1 NAME
428
429 SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
430
431 =head2 OVERVIEW
432
433 Controller class for the conversion and processing (printing) of objects.
434
435
436 Inherited from the base controller class, this controller implements the Sales Mass Invoice Creation.
437 In general there are two major distinctions:
438 This class implements the conversion and the printing via clickable action AND triggers the same
439 conversion towards a Background-Job with a good user interaction.
440
441 Analysis hints: All this is more or less boilerplate code around the great convert_to_invoice method
442 in DeliverOrder.pm. If you need to debug stuff, take a look at the corresponding test case
443 ($ t/test.pl t/db_helper/convert_invoices.t). There are some redundant code parts in this controller
444 and in the background job, i.e. compare the actions create and print.
445 From a reverse engineering point of view the actions in this controller were written before the
446 background job existed, therefore if anything goes boom take a look at the single steps done via gui
447 in this controller and after that take a deeper look at the MassRecordCreationAndPrinting job.
448
449 =head1 FUNCTIONS
450
451 =over 2
452
453 =item C<action_list_sales_delivery_orders>
454
455 List all open sales delivery orders. The filter can be in two states show or "no show" the
456 original, probably gorash idea, is to increase performance and not to be forced to click on the
457 next button (like in all other reports). Therefore use this option and this filter for a good
458 project default and hide it again. Filters can be added in _filter.html. Take a look at
459   SL::Controlle::Helper::GetModels::Filtered.pm and SL::DB::Helper::Filtered.
460
461 =item C<action_create_invoices>
462
463 Creates or to be more correctly converts delivery orders to invoices. All items are
464 converted 1:1 and after conversion the delivery order(s) are closed.
465
466 =item C<action_list_invoices>
467
468 List the created invoices, if created via gui (see action above)
469
470 =item C<action_print>
471
472 Print the selected invoices. Yes, it really is all boring linear (see action above).
473 Calls download_or_print method.
474
475 =item C<action_create_print_all_start>
476
477 Initialises the webform for the creation and printing via background job. Now we get to
478 the more fun part ...  Mosu did a great user interaction job here, we can choose how many
479 objects are converted in one strike and if or if not they are downloadable or will be sent to
480 a printer (if defined as a printing command) right away.
481 Background job is started and status is watched via js and the next action.
482
483 =item C<action_create_print_all_status>
484
485 Action for watching status, default is refreshing every 5 seconds
486
487 =item C<action_create_print_all_download>
488
489 If the above is done (did I already said: boring linear?). Documents will
490 be either printed or downloaded.
491
492 =item C<init_printers>
493
494 Gets all printer commands
495
496 =item C<init_invoice_ids>
497
498 Gets a list of (empty) invoice ids
499
500 =item C<init_today>
501
502 Gets the current day. Currently used in custom code.
503 Has to be initialised (get_set_init) and can be used as default for
504 a date tag like C<[% L.date_tag("transdate", SELF.today, id=transdate) %]>.
505
506 =item C<init_sales_delivery_order_models>
507
508 Calls _init_sales_delivery_order_models with a param
509
510 =item C<_init_sales_delivery_order_models>
511
512 Private function, called by init_sales_delivery_order_models.
513 Gets all open sales delivery orders.
514
515 =item C<init_invoice_models>
516
517 Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
518
519 =item C<init_default_printer_id>
520
521 Gets the default printer for sales_invoices. Currently this function is not called, but
522 might be useful in the next version.Calling template code and Controller already expect a default:
523 C<L.select_tag("", printers, title_key="description", default=SELF.default_printer_id, id="cpa_printer_id") %]>
524
525 =item C<setup>
526
527 Currently sets / checks only the access right.
528
529 =item C<create_pdfs>
530
531 =item C<download_or_print_documents>
532
533 Backend function for printing or downloading docs. Only used for gui processing (called
534 via action_print).
535
536 =item C<make_filter_summary>
537 Creates the filter option summary in the header. By the time of writing three filters are
538 supported: Customer and date from/to of the Delivery Order (database field transdate).
539
540 =back
541
542 =head1 TODO
543
544 pShould be more generalized. Right now just one conversion (delivery order to invoice) is supported.
545 Using BackgroundJobs to mass create / transfer stuff is the way to do it. The original idea
546 was taken from one client project (mosu) with some extra (maybe not standard compliant) customized
547 stuff (using cvars for extra filters and a very compressed Controller for linking (ODSalesOrder.pm)).
548
549
550 =head1 AUTHOR
551
552 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
553
554 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>
555
556 =cut