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