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