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 foreach my $invoice( @invoices ) {
88 my @linked_shop_orders = $invoice->linked_records(
90 via => [ 'DeliveryOrder', 'Order' ],
92 #if (scalar @linked_shop_orders[0][0] >= 1){
94 my $shop_order = $linked_shop_orders[0][0];
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");
103 my $key = sprintf('%d-%d', Time::HiRes::gettimeofday());
104 $::auth->set_session_value("MassInvoiceCreatePrint::ids-${key}" => [ map { $_->id } @invoices ]);
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));
111 flash_later('info', t8('The invoices have been created. They\'re pre-selected below.')) if @invoices;
113 $self->redirect_to(action => 'list_invoices', ids => $key);
116 sub action_list_invoices {
119 my $show = $::form->{noshow} ? 0 : 1;
120 delete $::form->{noshow};
122 if ($::form->{ids}) {
123 my $key = 'MassInvoiceCreatePrint::ids-' . $::form->{ids};
124 $self->invoice_ids($::auth->get_session_value($key) || []);
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};
130 $self->invoice_models->add_additional_url_params(ids => $::form->{ids});
133 my %selected_ids = map { +($_ => 1) } @{ $self->invoice_ids };
135 $::form->{printer_id} ||= $self->default_printer_id;
137 $self->setup_list_invoices_action_bar(num_rows => scalar(@{ $self->invoice_models->get }));
139 $self->render('mass_invoice_create_print_from_do/list_invoices',
140 title => $::locale->text('Open invoice'),
142 selected_ids => \%selected_ids);
148 my @invoices = map { SL::DB::Invoice->new(id => $_)->load } @{ $::form->{id} || [] };
150 flash_later('error', t8('No invoices have been selected.'));
151 return $self->redirect_to(action => 'list_invoices');
154 $self->download_or_print_documents(printer_id => $::form->{printer_id}, invoices => \@invoices, bothsided => $::form->{bothsided});
157 sub action_create_print_all_start {
160 $self->sales_delivery_order_models->disable_plugin('paginated');
162 my @records = @{ $self->sales_delivery_order_models->get };
163 my $num = min(scalar(@records), $::form->{number_of_invoices} // scalar(@records));
165 my $job = SL::DB::BackgroundJob->new(
168 package_name => 'MassRecordCreationAndPrinting',
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(),
180 conversion_errors => [ ],
182 session_id => $::auth->get_session_id,
184 )->update_next_run_at;
186 SL::System::TaskServer->new->wake_up;
188 my $html = $self->render('mass_invoice_create_print_from_do/_create_print_all_status', { output => 0 }, job => $job);
191 ->html('#create_print_all_dialog', $html)
192 ->run('kivi.MassInvoiceCreatePrint.massConversionStarted')
196 sub action_create_print_all_status {
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);
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();
206 sub action_create_print_all_download {
208 my $job = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
210 my $sfile = SL::SessionFile->new($job->data_as_hash->{pdf_file_name}, mode => 'r');
211 die $! if !$sfile->fh;
213 my $merged_pdf = do { local $/; my $fh = $sfile->fh; <$fh> };
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;
220 return $self->send_file(
222 type => 'application/pdf',
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 }
236 sub init_sales_delivery_order_models {
238 return $self->_init_sales_delivery_order_models(sortby => 'donumber');
241 sub _init_sales_delivery_order_models {
242 my ($self, %params) = @_;
244 SL::Controller::Helper::GetModels->new(
246 model => 'DeliveryOrder',
250 by => $params{sortby},
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'),
259 with_objects => [ qw(customer employee) ],
261 '!customer_id' => undef,
262 or => [ closed => undef, closed => 0 ],
268 sub init_invoice_models {
270 my @invoice_ids = @{ $self->invoice_ids };
272 SL::Controller::Helper::GetModels->new(
275 (paginated => 0,) x !!@invoice_ids,
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'),
289 with_objects => [ qw(customer employee) ],
291 '!customer_id' => undef,
292 (id => \@invoice_ids) x !!@invoice_ids,
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;
303 sub init_all_businesses {
304 return SL::DB::Manager::Business->get_all_sorted;
309 $::auth->assert('invoice_edit');
311 $::request->layout->use_javascript("${_}.js") for qw(kivi.MassInvoiceCreatePrint);
319 sub download_or_print_documents {
320 my ($self, %params) = @_;
326 documents => $params{invoices},
329 formname => 'invoice',
331 media => $params{printer_id} ? 'printer' : 'file',
332 printer_id => $params{printer_id},
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;
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;
343 return $self->send_file(
345 type => 'application/pdf',
350 my $printer = SL::DB::Printer->new(id => $params{printer_id})->load;
351 $printer->print_document(content => $merged_pdf);
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});
357 unlink @pdf_file_names;
358 $::form->error(t8("Creating the PDF failed:") . " " . $@);
362 sub make_filter_summary {
365 my $filter = $::form->{filter} || {};
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') ],
375 push @filter_strings, "$_->[1]: " . ($_->[2] ? $_->[2]->() : $_->[0]) if $_->[0];
378 $self->{filter_summary} = join ', ', @filter_strings;
381 sub setup_list_invoices_action_bar {
382 my ($self, %params) = @_;
384 for my $bar ($::request->layout->get('actionbar')) {
388 submit => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_invoices' } ],
389 accesskey => 'enter',
392 $::locale->text('Print'),
393 call => [ 'kivi.MassInvoiceCreatePrint.showMassPrintOptionsOrDownloadDirectly' ],
394 disabled => !$params{num_rows} ? $::locale->text('The report doesn\'t contain entries.') : undef,
400 sub setup_list_sales_delivery_orders_action_bar {
401 my ($self, %params) = @_;
403 for my $bar ($::request->layout->get('actionbar')) {
406 $params{show_creation_buttons} ? t8('Update') : t8('Search'),
407 submit => [ '#search_form', { action => 'MassInvoiceCreatePrint/list_sales_delivery_orders' } ],
408 accesskey => 'enter',
414 tooltip => t8("Create and print invoices")
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' ],
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},
446 SL::Controller::MassInvoiceCreatePrint - Controller for Mass Create Print Sales Invoice from Delivery Order
450 Controller class for the conversion and processing (printing) of objects.
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.
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.
470 =item C<action_list_sales_delivery_orders>
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.
478 =item C<action_create_invoices>
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.
483 =item C<action_list_invoices>
485 List the created invoices, if created via gui (see action above)
487 =item C<action_print>
489 Print the selected invoices. Yes, it really is all boring linear (see action above).
490 Calls download_or_print method.
492 =item C<action_create_print_all_start>
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.
500 =item C<action_create_print_all_status>
502 Action for watching status, default is refreshing every 5 seconds
504 =item C<action_create_print_all_download>
506 If the above is done (did I already said: boring linear?). Documents will
507 be either printed or downloaded.
509 =item C<init_printers>
511 Gets all printer commands
513 =item C<init_invoice_ids>
515 Gets a list of (empty) invoice ids
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) %]>.
523 =item C<init_sales_delivery_order_models>
525 Calls _init_sales_delivery_order_models with a param
527 =item C<_init_sales_delivery_order_models>
529 Private function, called by init_sales_delivery_order_models.
530 Gets all open sales delivery orders.
532 =item C<init_invoice_models>
534 Gets all invoice_models via the ids in invoice_ids (at the beginning no ids exist)
536 =item C<init_default_printer_id>
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") %]>
544 Currently sets / checks only the access right.
548 =item C<download_or_print_documents>
550 Backend function for printing or downloading docs. Only used for gui processing (called
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).
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)).
569 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
571 Jan Büren E<lt>jan@kivitendo-premium.deE<gt>