7fc3388e7530d7117668279498376de99ec34210
[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     ],
220   );
221 }
222
223
224 sub init_invoice_models {
225   my ($self)             = @_;
226   my @invoice_ids = @{ $self->invoice_ids };
227
228   SL::Controller::Helper::GetModels->new(
229     controller   => $_[0],
230     model        => 'Invoice',
231     (paginated   => 0,) x !!@invoice_ids,
232     sorted       => {
233       _default     => {
234         by           => 'transdate',
235         dir          => 0,
236       },
237       customer     => t8('Customer'),
238       invnumber    => t8('Invoice Number'),
239       employee     => t8('Employee'),
240       donumber     => t8('Delivery Order Number'),
241       ordnumber    => t8('Order Number'),
242       reqdate      => t8('Delivery Date'),
243       transdate    => t8('Date'),
244     },
245     with_objects => [ qw(customer employee) ],
246     query        => [
247       '!customer_id' => undef,
248       (id            => \@invoice_ids) x !!@invoice_ids,
249     ],
250   );
251 }
252
253
254 sub init_default_printer_id {
255   my $pr = SL::DB::Manager::Printer->find_by(printer_description => $::locale->text("sales_invoice_printer"));
256   return $pr ? $pr->id : undef;
257 }
258
259 sub setup {
260   my ($self) = @_;
261   $::auth->assert('invoice_edit');
262
263   $::request->layout->use_javascript("${_}.js")  for qw(kivi.MassInvoiceCreatePrint);
264 }
265
266 #
267 # helpers
268 #
269
270 sub create_pdfs {
271   my ($self, %params) = @_;
272
273   my @pdf_file_names;
274   foreach my $invoice (@{ $params{invoices} }) {
275     my %create_params = (
276       template  => $self->find_template(name => 'invoice', printer_id => $params{printer_id}),
277       variables => Form->new(''),
278       return    => 'file_name',
279       variable_content_types => { longdescription => 'html',
280                                   partnotes       => 'html',
281                                   notes           => 'html',}
282     );
283
284     $create_params{variables}->{$_} = $params{variables}->{$_} for keys %{ $params{variables} };
285
286     $invoice->flatten_to_form($create_params{variables}, format_amounts => 1);
287     $create_params{variables}->prepare_for_printing;
288
289     push @pdf_file_names, $self->create_pdf(%create_params);
290   }
291
292   return @pdf_file_names;
293 }
294
295 sub download_or_print_documents {
296   my ($self, %params) = @_;
297
298   my @pdf_file_names;
299
300   eval {
301     my %pdf_params = (
302       invoices        => $params{invoices},
303       printer_id      => $params{printer_id},
304       variables       => {
305         type        => 'invoice',
306         formname    => 'invoice',
307         format      => 'pdf',
308         media       => $params{printer_id} ? 'printer' : 'file',
309       });
310
311     @pdf_file_names = $self->create_pdfs(%pdf_params);
312     my $merged_pdf  = $self->merge_pdfs(file_names => \@pdf_file_names);
313     unlink @pdf_file_names;
314
315     if (!$params{printer_id}) {
316       my $file_name =  t8("Invoices") . '-' . DateTime->today_local->strftime('%Y%m%d%H%M%S') . '.pdf';
317       $file_name    =~ s{[^\w\.]+}{_}g;
318
319       return $self->send_file(
320         \$merged_pdf,
321         type => 'application/pdf',
322         name => $file_name,
323       );
324     }
325
326     my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
327     my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($printer->printer_command);
328
329     open my $out, '|-', $command or die $!;
330     binmode $out;
331     print $out $merged_pdf;
332     close $out;
333
334     flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
335     return $self->redirect_to(action => 'list_invoices', printer_id => $params{printer_id});
336
337   } or do {
338     unlink @pdf_file_names;
339     $::form->error(t8("Creating the PDF failed:") . " " . $@);
340   };
341 }
342
343 sub make_filter_summary {
344   my ($self) = @_;
345
346   my $filter = $::form->{filter} || {};
347   my @filter_strings;
348
349   my @filters = (
350     [ $filter->{customer}{"name:substr::ilike"}, t8('Customer') ],
351     [ $filter->{"transdate:date::ge"},           t8('Transdate') . " " . t8('From Date') ],
352     [ $filter->{"transdate:date::le"},           t8('Transdate') . " " . t8('To Date')   ],
353   );
354
355   for (@filters) {
356     push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
357   }
358
359   $self->{filter_summary} = join ', ', @filter_strings;
360 }
361 1;
362
363 __END__
364
365 =pod
366
367 =encoding utf8
368
369 =head1 NAME
370
371 SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
372
373 =head2 OVERVIEW
374
375 Controller class for the conversion and processing (printing) of objects.
376
377
378 Inherited from the base controller class, this controller implements the Sales Mass Invoice Creation.
379 In general there are two major distinctions:
380 This class implements the conversion and the printing via clickable action AND triggers the same
381 conversion towards a Background-Job with a good user interaction.
382
383 Analysis hints: All this is more or less boilerplate code around the great convert_to_invoice method
384 in DeliverOrder.pm. If you need to debug stuff, take a look at the corresponding test case
385 ($ t/test.pl t/db_helper/convert_invoices.t). There are some redundant code parts in this controller
386 and in the background job, i.e. compare the actions create and print.
387 From a reverse engineering point of view the actions in this controller were written before the
388 background job existed, therefore if anything goes boom take a look at the single steps done via gui
389 in this controller and after that take a deeper look at the MassRecordCreationAndPrinting job.
390
391 =head1 FUNCTIONS
392
393 =over 2
394
395 =item C<action_list_sales_delivery_orders>
396
397 List all open sales delivery orders. The filter can be in two states show or "no show" the
398 original, probably gorash idea, is to increase performance and not to be forced to click on the
399 next button (like in all other reports). Therefore use this option and this filter for a good
400 project default and hide it again. Filters can be added in _filter.html. Take a look at
401   SL::Controlle::Helper::GetModels::Filtered.pm and SL::DB::Helper::Filtered.
402
403 =item C<action_create_invoices>
404
405 Creates or to be more correctly converts delivery orders to invoices. All items are
406 converted 1:1 and after conversion the delivery order(s) are closed.
407
408 =item C<action_list_invoices>
409
410 List the created invoices, if created via gui (see action above)
411
412 =item C<action_print>
413
414 Print the selected invoices. Yes, it really is all boring linear (see action above).
415 Calls download_or_print method.
416
417 =item C<action_create_print_all_start>
418
419 Initialises the webform for the creation and printing via background job. Now we get to
420 the more fun part ...  Mosu did a great user interaction job here, we can choose how many
421 objects are converted in one strike and if or if not they are downloadable or will be sent to
422 a printer (if defined as a printing command) right away.
423 Background job is started and status is watched via js and the next action.
424
425 =item C<action_create_print_all_status>
426
427 Action for watching status, default is refreshing every 5 seconds
428
429 =item C<action_create_print_all_download>
430
431 If the above is done (did I already said: boring linear?). Documents will
432 be either printed or downloaded.
433
434 =item C<init_printers>
435
436 Gets all printer commands
437
438 =item C<init_invoice_ids>
439
440 Gets a list of (empty) invoice ids
441
442 =item C<init_today>
443
444 Gets the current day. Currently used in custom code.
445 Has to be initialised (get_set_init) and can be used as default for
446 a date tag like C<[% L.date_tag("transdate", SELF.today, id=transdate) %]>.
447
448 =item C<init_sales_delivery_order_models>
449
450 Calls _init_sales_delivery_order_models with a param
451
452 =item C<_init_sales_delivery_order_models>
453
454 Private function, called by init_sales_delivery_order_models.
455 Gets all open sales delivery orders.
456
457 =item C<init_invoice_models>
458
459 Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
460
461 =item C<init_default_printer_id>
462
463 Gets the default printer for sales_invoices. Currently this function is not called, but
464 might be useful in the next version.Calling template code and Controller already expect a default:
465 C<L.select_tag("", printers, title_key="description", default=SELF.default_printer_id, id="cpa_printer_id") %]>
466
467 =item C<setup>
468
469 Currently sets / checks only the access right.
470
471 =item C<create_pdfs>
472
473 =item C<download_or_print_documents>
474
475 Backend function for printing or downloading docs. Only used for gui processing (called
476 via action_print).
477
478 =item C<make_filter_summary>
479 Creates the filter option summary in the header. By the time of writing three filters are
480 supported: Customer and date from/to of the Delivery Order (database field transdate).
481
482 =back
483
484 =head1 TODO
485
486 Should be more generalized. Right now just one conversion (delivery order to invoice) is supported.
487 Using BackgroundJobs to mass create / transfer stuff is the way to do it. The original idea
488 was taken from one client project (mosu) with some extra (maybe not standard compliant) customized
489 stuff (using cvars for extra filters and a very compressed Controller for linking (ODSalesOrder.pm)).
490
491
492 =head1 AUTHOR
493
494 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
495
496 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>
497
498 =cut