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