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::Flash;
19 use SL::Locale::String;
21 use SL::System::TaskServer;
22 use Rose::Object::MakeMethods::Generic
24 'scalar --get_set_init' => [ qw(invoice_models invoice_ids sales_delivery_order_models printers default_printer_id today) ],
27 __PACKAGE__->run_before('setup');
33 sub action_list_sales_delivery_orders {
36 # default is usually no show, exception here
37 my $show = ($::form->{noshow} ? 0 : 1);
38 delete $::form->{noshow};
40 # if a filter is choosen, the filter info should be visible
41 $self->make_filter_summary;
42 $self->setup_list_sales_delivery_orders_action_bar(show_creation_buttons => $show, num_rows => scalar(@{ $self->sales_delivery_order_models->get }));
43 $self->render('mass_invoice_create_print_from_do/list_sales_delivery_orders',
45 title => $::locale->text('Open sales delivery orders'));
48 sub action_create_invoices {
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');
58 my $db = SL::DB::Invoice->new->db;
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;
65 my $invoice = $delivery_order->convert_to_invoice() || die $db->error;
66 push @invoices, $invoice;
71 $::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
72 $::form->error($db->error);
75 my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
76 $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
78 flash_later('info', t8('The invoices have been created. They\'re pre-selected below.'));
79 $self->redirect_to(action => 'list_invoices', ids => $key);
82 sub action_list_invoices {
85 my $show = $::form->{noshow} ? 0 : 1;
86 delete $::form->{noshow};
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});
94 my %selected_ids = map { +($_ => 1) } @{ $self->invoice_ids };
96 $::form->{printer_id} ||= $self->default_printer_id;
98 $self->setup_list_invoices_action_bar(num_rows => scalar(@{ $self->invoice_models->get }));
100 $self->render('mass_invoice_create_print_from_do/list_invoices',
101 title => $::locale->text('Open invoice'),
103 selected_ids => \%selected_ids);
109 my @invoices = map { SL::DB::Invoice->new(id => $_)->load } @{ $::form->{id} || [] };
111 flash_later('error', t8('No invoices have been selected.'));
112 return $self->redirect_to(action => 'list_invoices');
115 $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices);
118 sub action_create_print_all_start {
121 $self->sales_delivery_order_models->disable_plugin('paginated');
123 my @records = @{ $self->sales_delivery_order_models->get };
124 my $num = min(scalar(@records), $::form->{number_of_invoices} // scalar(@records));
126 my $job = SL::DB::BackgroundJob->new(
129 package_name => 'MassRecordCreationAndPrinting',
132 record_ids => [ map { $_->id } @records[0..$num - 1] ],
133 printer_id => $::form->{printer_id},
134 copy_printer_id => $::form->{copy_printer_id},
135 bothsided => ($::form->{bothsided}?1:0),
136 transdate => $::form->{transdate},
137 status => SL::BackgroundJob::MassRecordCreationAndPrinting->WAITING_FOR_EXECUTION(),
141 conversion_errors => [ ],
143 session_id => $::auth->get_session_id,
145 )->update_next_run_at;
147 SL::System::TaskServer->new->wake_up;
149 my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
152 ->html('#create_print_all_dialog', $html)
153 ->run('kivi.MassInvoiceCreatePrint.massConversionStarted')
157 sub action_create_print_all_status {
159 my $job = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
160 my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
162 $self->js->html('#create_print_all_dialog', $html);
163 $self->js->run('kivi.MassInvoiceCreatePrint.massConversionFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::MassRecordCreationAndPrinting->DONE();
167 sub action_create_print_all_download {
169 my $job = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
171 my $sfile = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
172 die $! if !$sfile->fh;
174 my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
177 my $type = 'Invoices';
178 my $file_name = t8($type) . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
179 $file_name =~ s{[^\w\.]+}{_}g;
181 return $self->send_file(
183 type => 'application/pdf',
192 sub init_printers { SL::DB::Manager::Printer->get_all_sorted }
193 #sub init_att { require SL::Controller::Attachments; SL::Controller::Attachments->new() }
194 sub init_invoice_ids { [] }
195 sub init_today { DateTime->today_local }
197 sub init_sales_delivery_order_models {
199 return $self->_init_sales_delivery_order_models(sortby => 'donumber');
202 sub _init_sales_delivery_order_models {
203 my ($self, %params) = @_;
205 SL::Controller::Helper::GetModels->new(
207 model => 'DeliveryOrder',
211 by => $params{sortby},
214 customer => t8('Customer'),
215 employee => t8('Employee'),
216 transdate => t8('Date'),
217 donumber => t8('Delivery Order Number'),
218 ordnumber => t8('Order Number'),
220 with_objects => [ qw(customer employee) ],
222 '!customer_id' => undef,
223 or => [ closed => undef, closed => 0 ],
229 sub init_invoice_models {
231 my @invoice_ids = @{ $self->invoice_ids };
233 SL::Controller::Helper::GetModels->new(
236 (paginated => 0,) x !!@invoice_ids,
242 customer => t8('Customer'),
243 invnumber => t8('Invoice Number'),
244 employee => t8('Employee'),
245 donumber => t8('Delivery Order Number'),
246 ordnumber => t8('Order Number'),
247 reqdate => t8('Delivery Date'),
248 transdate => t8('Date'),
250 with_objects => [ qw(customer employee) ],
252 '!customer_id' => undef,
253 (id => \@invoice_ids) x !!@invoice_ids,
259 sub init_default_printer_id {
260 my $pr = SL::DB::Manager::Printer->find_by(printer_description => $::locale->text("sales_invoice_printer"));
261 return $pr ? $pr->id : undef;
266 $::auth->assert('invoice_edit');
268 $::request->layout->use_javascript("${_}.js") for qw(kivi.MassInvoiceCreatePrint);
276 sub download_or_print_documents {
277 my ($self, %params) = @_;
283 documents => $params{invoices},
286 formname => 'invoice',
288 media => $params{printer_id} ? 'printer' : 'file',
289 printer_id => $params{printer_id},
292 @pdf_file_names = $self->create_pdfs(%pdf_params);
293 my $merged_pdf = $self->merge_pdfs(file_names => \@pdf_file_names);
294 unlink @pdf_file_names;
296 if (!$params{printer_id}) {
297 my $file_name = t8("Invoices") . '-' . DateTime->now_local->strftime('%Y%m%d%H%M%S') . '.pdf';
298 $file_name =~ s{[^\w\.]+}{_}g;
300 return $self->send_file(
302 type => 'application/pdf',
307 my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
308 $printer->print_document(content => $merged_pdf);
310 flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
311 return $self->redirect_to(action => 'list_invoices', printer_id => $params{printer_id});
314 unlink @pdf_file_names;
315 $::form->error(t8("Creating the PDF failed:") . " " . $@);
319 sub make_filter_summary {
322 my $filter = $::form->{filter} || {};
326 [ $filter->{customer}{"name:substr::ilike"}, t8('Customer') ],
327 [ $filter->{"transdate:date::ge"}, t8('Transdate') . " " . t8('From Date') ],
328 [ $filter->{"transdate:date::le"}, t8('Transdate') . " " . t8('To Date') ],
332 push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
335 $self->{filter_summary} = join ', ', @filter_strings;
338 sub setup_list_invoices_action_bar {
339 my ($self, %params) = @_;
341 for my $bar ($::request->layout->get('actionbar')) {
345 submit => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_invoices' } ],
346 accesskey => 'enter',
350 call => [ 'kivi.call_jquery', '#search_form', 'resetForm' ],
353 $::locale->text('Print'),
354 call => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
355 disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
361 sub setup_list_sales_delivery_orders_action_bar {
362 my ($self, %params) = @_;
364 for my $bar ($::request->layout->get('actionbar')) {
368 submit => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_sales_delivery_orders' } ],
369 accesskey => 'enter',
373 call => [ 'kivi.call_jquery', '#search_form', 'resetForm' ],
379 tooltip => t8("Create and print invoices")
382 t8('for selected entries'),
383 call => [ 'kivi.MassInvoiceCreatePrint.submitMassCreationForm' ],
384 tooltip => t8("Create and print invoices for all selected delivery orders"),
385 disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
386 only_if => $params{show_creation_buttons},
390 t8('for all entries'),
391 call => [ 'kivi.MassInvoiceCreatePrint.createPrintAllInitialize' ],
392 tooltip => t8("Create and print invoices for all delivery orders matching the filter"),
393 disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
394 only_if => $params{show_creation_buttons},
411 SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
415 Controller class for the conversion and processing (printing) of objects.
418 Inherited from the base controller class, this controller implements the Sales Mass Invoice Creation.
419 In general there are two major distinctions:
420 This class implements the conversion and the printing via clickable action AND triggers the same
421 conversion towards a Background-Job with a good user interaction.
423 Analysis hints: All this is more or less boilerplate code around the great convert_to_invoice method
424 in DeliverOrder.pm. If you need to debug stuff, take a look at the corresponding test case
425 ($ t/test.pl t/db_helper/convert_invoices.t). There are some redundant code parts in this controller
426 and in the background job, i.e. compare the actions create and print.
427 From a reverse engineering point of view the actions in this controller were written before the
428 background job existed, therefore if anything goes boom take a look at the single steps done via gui
429 in this controller and after that take a deeper look at the MassRecordCreationAndPrinting job.
435 =item C<action_list_sales_delivery_orders>
437 List all open sales delivery orders. The filter can be in two states show or "no show" the
438 original, probably gorash idea, is to increase performance and not to be forced to click on the
439 next button (like in all other reports). Therefore use this option and this filter for a good
440 project default and hide it again. Filters can be added in _filter.html. Take a look at
441 SL::Controlle::Helper::GetModels::Filtered.pm and SL::DB::Helper::Filtered.
443 =item C<action_create_invoices>
445 Creates or to be more correctly converts delivery orders to invoices. All items are
446 converted 1:1 and after conversion the delivery order(s) are closed.
448 =item C<action_list_invoices>
450 List the created invoices, if created via gui (see action above)
452 =item C<action_print>
454 Print the selected invoices. Yes, it really is all boring linear (see action above).
455 Calls download_or_print method.
457 =item C<action_create_print_all_start>
459 Initialises the webform for the creation and printing via background job. Now we get to
460 the more fun part ... Mosu did a great user interaction job here, we can choose how many
461 objects are converted in one strike and if or if not they are downloadable or will be sent to
462 a printer (if defined as a printing command) right away.
463 Background job is started and status is watched via js and the next action.
465 =item C<action_create_print_all_status>
467 Action for watching status, default is refreshing every 5 seconds
469 =item C<action_create_print_all_download>
471 If the above is done (did I already said: boring linear?). Documents will
472 be either printed or downloaded.
474 =item C<init_printers>
476 Gets all printer commands
478 =item C<init_invoice_ids>
480 Gets a list of (empty) invoice ids
484 Gets the current day. Currently used in custom code.
485 Has to be initialised (get_set_init) and can be used as default for
486 a date tag like C<[% L.date_tag("transdate", SELF.today, id=transdate) %]>.
488 =item C<init_sales_delivery_order_models>
490 Calls _init_sales_delivery_order_models with a param
492 =item C<_init_sales_delivery_order_models>
494 Private function, called by init_sales_delivery_order_models.
495 Gets all open sales delivery orders.
497 =item C<init_invoice_models>
499 Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
501 =item C<init_default_printer_id>
503 Gets the default printer for sales_invoices. Currently this function is not called, but
504 might be useful in the next version.Calling template code and Controller already expect a default:
505 C<L.select_tag("", printers, title_key="description", default=SELF.default_printer_id, id="cpa_printer_id") %]>
509 Currently sets / checks only the access right.
513 =item C<download_or_print_documents>
515 Backend function for printing or downloading docs. Only used for gui processing (called
518 =item C<make_filter_summary>
519 Creates the filter option summary in the header. By the time of writing three filters are
520 supported: Customer and date from/to of the Delivery Order (database field transdate).
526 pShould be more generalized. Right now just one conversion (delivery order to invoice) is supported.
527 Using BackgroundJobs to mass create / transfer stuff is the way to do it. The original idea
528 was taken from one client project (mosu) with some extra (maybe not standard compliant) customized
529 stuff (using cvars for extra filters and a very compressed Controller for linking (ODSalesOrder.pm)).
534 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
536 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>