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