1 package SL::Controller::MassInvoiceCreatePrint;
5 use parent qw(SL::Controller::Base);
8 use List::MoreUtils qw(all uniq);
9 use List::Util qw(first min);
11 use SL::BackgroundJob::MassRecordCreationAndPrinting;
12 use SL::Controller::Helper::GetModels;
13 use SL::DB::DeliveryOrder;
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;
22 use SL::System::TaskServer;
23 use Rose::Object::MakeMethods::Generic
25 'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id today all_businesses) ],
28 __PACKAGE__->run_before('setup');
34 sub action_list_sales_delivery_orders {
37 # default is usually no show, exception here
38 my $show = ($::form->{noshow} ? 0 : 1);
39 delete $::form->{noshow};
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',
46 title => $::locale->text('Open sales delivery orders'));
49 sub action_create_invoices {
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');
59 my $db = SL::DB::Invoice->new->db;
61 my @already_closed_delivery_orders;
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;
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.
71 if ($delivery_order->closed) {
72 push @already_closed_delivery_orders, $delivery_order;
75 my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
76 push @invoices, $invoice;
82 $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
83 $::form->error($db->error);
86 my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
87 $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
89 if (@already_closed_delivery_orders) {
90 my $dos_list = join ' ', map { $_->donumber } @already_closed_delivery_orders;
91 flash_later('error', t8('The following delivery orders could not be processed because they are already closed: #1', $dos_list));
94 flash_later('info', t8('The invoices have been created. They\'re pre-selected below.')) if @invoices;
96 $self->redirect_to(action => 'list_invoices', ids => $key);
99 sub action_list_invoices {
102 my $show = $::form->{noshow} ? 0 : 1;
103 delete $::form->{noshow};
105 if ($::form->{ids}) {
106 my $key = 'MassInvoiceCreatePrint::ids-' . $::form->{ids};
107 $self->invoice_ids($::auth->get_session_value($key) || []);
109 # Prevent models->get to retrieve any invoices if session key is there
110 # but no ids are given.
111 $self->invoice_ids([0]) if !@{$self->invoice_ids};
113 $self->invoice_models->add_additional_url_params(ids => $::form->{ids});
116 my %selected_ids = map { +($_ => 1) } @{ $self->invoice_ids };
118 $::form->{printer_id} ||= $self->default_printer_id;
120 $self->setup_list_invoices_action_bar(num_rows => scalar(@{ $self->invoice_models->get }));
122 $self->render('mass_invoice_create_print_from_do/list_invoices',
123 title => $::locale->text('Open invoice'),
125 selected_ids => \%selected_ids);
131 my @invoices = map { SL::DB::Invoice->new(id => $_)->load } @{ $::form->{id} || [] };
133 flash_later('error', t8('No invoices have been selected.'));
134 return $self->redirect_to(action => 'list_invoices');
137 $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices, bothsided => $::form->{bothsided});
140 sub action_create_print_all_start {
143 $self->sales_delivery_order_models->disable_plugin('paginated');
145 my @records = @{ $self->sales_delivery_order_models->get };
146 my $num = min(scalar(@records), $::form->{number_of_invoices} // scalar(@records));
148 my $job = SL::DB::BackgroundJob->new(
151 package_name => 'MassRecordCreationAndPrinting',
154 record_ids => [ map { $_->id } @records[0..$num - 1] ],
155 printer_id => $::form->{printer_id},
156 copy_printer_id => $::form->{copy_printer_id},
157 bothsided => ($::form->{bothsided}?1:0),
158 transdate => $::form->{transdate},
159 status => SL::BackgroundJob::MassRecordCreationAndPrinting->WAITING_FOR_EXECUTION(),
163 conversion_errors => [ ],
165 session_id => $::auth->get_session_id,
167 )->update_next_run_at;
169 SL::System::TaskServer->new->wake_up;
171 my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
174 ->html('#create_print_all_dialog', $html)
175 ->run('kivi.MassInvoiceCreatePrint.massConversionStarted')
179 sub action_create_print_all_status {
181 my $job = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
182 my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
184 $self->js->html('#create_print_all_dialog', $html);
185 $self->js->run('kivi.MassInvoiceCreatePrint.massConversionFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::MassRecordCreationAndPrinting->DONE();
189 sub action_create_print_all_download {
191 my $job = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
193 my $sfile = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
194 die $! if !$sfile->fh;
196 my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
199 my $type = 'Invoices';
200 my $file_name = t8($type) . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
201 $file_name =~ s{[^\w\.]+}{_}g;
203 return $self->send_file(
205 type => 'application/pdf',
214 sub init_printers { SL::DB::Manager::Printer->get_all_sorted }
215 #sub init_att { require SL::Controller::Attachments; SL::Controller::Attachments->new() }
216 sub init_invoice_ids { [] }
217 sub init_today { DateTime->today_local }
219 sub init_sales_delivery_order_models {
221 return $self->_init_sales_delivery_order_models(sortby => 'donumber');
224 sub _init_sales_delivery_order_models {
225 my ($self, %params) = @_;
227 SL::Controller::Helper::GetModels->new(
229 model => 'DeliveryOrder',
233 by => $params{sortby},
236 customer => t8('Customer'),
237 employee => t8('Employee'),
238 transdate => t8('Delivery Order Date'),
239 donumber => t8('Delivery Order Number'),
240 ordnumber => t8('Order Number'),
242 with_objects => [ qw(customer employee) ],
244 '!customer_id' => undef,
245 or => [ closed => undef, closed => 0 ],
251 sub init_invoice_models {
253 my @invoice_ids = @{ $self->invoice_ids };
255 SL::Controller::Helper::GetModels->new(
258 (paginated => 0,) x !!@invoice_ids,
264 customer => t8('Customer'),
265 invnumber => t8('Invoice Number'),
266 employee => t8('Employee'),
267 donumber => t8('Delivery Order Number'),
268 ordnumber => t8('Order Number'),
269 reqdate => t8('Delivery Date'),
270 transdate => t8('Date'),
272 with_objects => [ qw(customer employee) ],
274 '!customer_id' => undef,
275 (id => \@invoice_ids) x !!@invoice_ids,
281 sub init_default_printer_id {
282 my $pr = SL::DB::Manager::Printer->find_by(printer_description => $::locale->text("sales_invoice_printer"));
283 return $pr ? $pr->id : undef;
286 sub init_all_businesses {
287 return SL::DB::Manager::Business->get_all_sorted;
292 $::auth->assert('invoice_edit');
294 $::request->layout->use_javascript("${_}.js") for qw(kivi.MassInvoiceCreatePrint);
302 sub download_or_print_documents {
303 my ($self, %params) = @_;
309 documents => $params{invoices},
312 formname => 'invoice',
314 media => $params{printer_id} ? 'printer' : 'file',
315 printer_id => $params{printer_id},
318 @pdf_file_names = $self->create_pdfs(%pdf_params);
319 my $merged_pdf = $self->merge_pdfs(file_names => \@pdf_file_names, bothsided => $params{bothsided});
320 unlink @pdf_file_names;
322 if (!$params{printer_id}) {
323 my $file_name = t8("Invoices") . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
324 $file_name =~ s{[^\w\.]+}{_}g;
326 return $self->send_file(
328 type => 'application/pdf',
333 my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
334 $printer->print_document(content => $merged_pdf);
336 flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
337 return $self->redirect_to(action => 'list_invoices', printer_id => $params{printer_id});
340 unlink @pdf_file_names;
341 $::form->error(t8("Creating the PDF failed:") . " " . $@);
345 sub make_filter_summary {
348 my $filter = $::form->{filter} || {};
352 [ $filter->{customer}{"name:substr::ilike"}, t8('Customer') ],
353 [ $filter->{"transdate:date::ge"}, t8('Delivery Order Date') . " " . t8('From Date') ],
354 [ $filter->{"transdate:date::le"}, t8('Delivery Order Date') . " " . t8('To Date') ],
358 push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
361 $self->{filter_summary} = join ', ', @filter_strings;
364 sub setup_list_invoices_action_bar {
365 my ($self, %params) = @_;
367 for my $bar ($::request->layout->get('actionbar')) {
371 submit => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_invoices' } ],
372 accesskey => 'enter',
375 $::locale->text('Print'),
376 call => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
377 disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
383 sub setup_list_sales_delivery_orders_action_bar {
384 my ($self, %params) = @_;
386 for my $bar ($::request->layout->get('actionbar')) {
389 $params{show_creation_buttons} ? t8('Update') : t8('Search'),
390 submit => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_sales_delivery_orders' } ],
391 accesskey => 'enter',
397 tooltip => t8("Create and print invoices")
400 t8("Create and print invoices for all selected delivery orders"),
401 submit => [ 'form', { action => 'MassInvoiceCreatePrint/create_invoices' } ],
402 disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
403 only_if => $params{show_creation_buttons},
404 checks => [ 'kivi.MassInvoiceCreatePrint.checkDeliveryOrderSelection' ],
409 t8("Create and print invoices for all delivery orders matching the filter"),
410 call => [ 'kivi.MassInvoiceCreatePrint.createPrintAllInitialize' ],
411 disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
412 only_if => $params{show_creation_buttons},
429 SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
433 Controller class for the conversion and processing (printing) of objects.
436 Inherited from the base controller class, this controller implements the Sales Mass Invoice Creation.
437 In general there are two major distinctions:
438 This class implements the conversion and the printing via clickable action AND triggers the same
439 conversion towards a Background-Job with a good user interaction.
441 Analysis hints: All this is more or less boilerplate code around the great convert_to_invoice method
442 in DeliverOrder.pm. If you need to debug stuff, take a look at the corresponding test case
443 ($ t/test.pl t/db_helper/convert_invoices.t). There are some redundant code parts in this controller
444 and in the background job, i.e. compare the actions create and print.
445 From a reverse engineering point of view the actions in this controller were written before the
446 background job existed, therefore if anything goes boom take a look at the single steps done via gui
447 in this controller and after that take a deeper look at the MassRecordCreationAndPrinting job.
453 =item C<action_list_sales_delivery_orders>
455 List all open sales delivery orders. The filter can be in two states show or "no show" the
456 original, probably gorash idea, is to increase performance and not to be forced to click on the
457 next button (like in all other reports). Therefore use this option and this filter for a good
458 project default and hide it again. Filters can be added in _filter.html. Take a look at
459 SL::Controlle::Helper::GetModels::Filtered.pm and SL::DB::Helper::Filtered.
461 =item C<action_create_invoices>
463 Creates or to be more correctly converts delivery orders to invoices. All items are
464 converted 1:1 and after conversion the delivery order(s) are closed.
466 =item C<action_list_invoices>
468 List the created invoices, if created via gui (see action above)
470 =item C<action_print>
472 Print the selected invoices. Yes, it really is all boring linear (see action above).
473 Calls download_or_print method.
475 =item C<action_create_print_all_start>
477 Initialises the webform for the creation and printing via background job. Now we get to
478 the more fun part ... Mosu did a great user interaction job here, we can choose how many
479 objects are converted in one strike and if or if not they are downloadable or will be sent to
480 a printer (if defined as a printing command) right away.
481 Background job is started and status is watched via js and the next action.
483 =item C<action_create_print_all_status>
485 Action for watching status, default is refreshing every 5 seconds
487 =item C<action_create_print_all_download>
489 If the above is done (did I already said: boring linear?). Documents will
490 be either printed or downloaded.
492 =item C<init_printers>
494 Gets all printer commands
496 =item C<init_invoice_ids>
498 Gets a list of (empty) invoice ids
502 Gets the current day. Currently used in custom code.
503 Has to be initialised (get_set_init) and can be used as default for
504 a date tag like C<[% L.date_tag("transdate", SELF.today, id=transdate) %]>.
506 =item C<init_sales_delivery_order_models>
508 Calls _init_sales_delivery_order_models with a param
510 =item C<_init_sales_delivery_order_models>
512 Private function, called by init_sales_delivery_order_models.
513 Gets all open sales delivery orders.
515 =item C<init_invoice_models>
517 Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
519 =item C<init_default_printer_id>
521 Gets the default printer for sales_invoices. Currently this function is not called, but
522 might be useful in the next version.Calling template code and Controller already expect a default:
523 C<L.select_tag("", printers, title_key="description", default=SELF.default_printer_id, id="cpa_printer_id") %]>
527 Currently sets / checks only the access right.
531 =item C<download_or_print_documents>
533 Backend function for printing or downloading docs. Only used for gui processing (called
536 =item C<make_filter_summary>
537 Creates the filter option summary in the header. By the time of writing three filters are
538 supported: Customer and date from/to of the Delivery Order (database field transdate).
544 pShould be more generalized. Right now just one conversion (delivery order to invoice) is supported.
545 Using BackgroundJobs to mass create / transfer stuff is the way to do it. The original idea
546 was taken from one client project (mosu) with some extra (maybe not standard compliant) customized
547 stuff (using cvars for extra filters and a very compressed Controller for linking (ODSalesOrder.pm)).
552 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
554 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>