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