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