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