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