1 package SL::BackgroundJob::CreatePeriodicInvoices;
 
   5 use parent qw(SL::BackgroundJob::Base);
 
   8 use DateTime::Format::Strptime;
 
   9 use English qw(-no_match_vars);
 
  10 use List::MoreUtils qw(uniq);
 
  17 use SL::DB::PeriodicInvoice;
 
  18 use SL::DB::PeriodicInvoicesConfig;
 
  20 use SL::Helper::CreatePDF qw(create_pdf find_template);
 
  22 use SL::Util qw(trim);
 
  23 use SL::System::Process;
 
  26   $_[0]->create_standard_job('0 3 1 * *'); # first day of month at 3:00 am
 
  31   $self->{db_obj} = shift;
 
  33   $self->{$_} = [] for qw(job_errors posted_invoices printed_invoices printed_failed emailed_invoices emailed_failed disabled_orders);
 
  35   if (!$self->{db_obj}->db->with_transaction(sub {
 
  38     my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(query => [ active => 1 ]);
 
  40     foreach my $config (@{ $configs }) {
 
  41       my $new_end_date = $config->handle_automatic_extension;
 
  42       _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date;
 
  45     my (@invoices_to_print, @invoices_to_email);
 
  47     _log_msg("Number of configs: " . scalar(@{ $configs}));
 
  49     foreach my $config (@{ $configs }) {
 
  50       # A configuration can be set to inactive by
 
  51       # $config->handle_automatic_extension. Therefore the check in
 
  52       # ...->get_all() does not suffice.
 
  53       _log_msg("Config " . $config->id . " active " . $config->active);
 
  54       next unless $config->active;
 
  56       my @dates = _calculate_dates($config);
 
  58       _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates));
 
  60       foreach my $date (@dates) {
 
  61         my $data = $self->_create_periodic_invoice($config, $date);
 
  64         _log_msg("Invoice " . $data->{invoice}->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n");
 
  66         push @{ $self->{posted_invoices} }, $data->{invoice};
 
  67         push @invoices_to_print, $data if $config->print;
 
  68         push @invoices_to_email, $data if $config->send_email;
 
  70         my $inactive_ordnumber = $config->disable_one_time_config;
 
  71         if ($inactive_ordnumber) {
 
  72           # disable one time configs and skip eventual invoices
 
  73           _log_msg("Order " . $inactive_ordnumber . " deavtivated \n");
 
  74           push @{ $self->{disabled_orders} }, $inactive_ordnumber;
 
  80     foreach my $inv ( @invoices_to_print ) { $self->_print_invoice($inv); }
 
  81     foreach my $inv ( @invoices_to_email ) { $self->_email_invoice($inv); }
 
  83     $self->_send_summary_email;
 
  87       $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
 
  91     if (@{ $self->{job_errors} }) {
 
  92       my $msg = join "\n", @{ $self->{job_errors} };
 
  93       _log_msg("Errors: $msg");
 
 101   my $message  = join('', 'SL::BackgroundJob::CreatePeriodicInvoices: ', @_);
 
 102   $message    .= "\n" unless $message =~ m/\n$/;
 
 103   $::lxdebug->message(LXDebug::DEBUG1(), $message);
 
 106 sub _generate_time_period_variables {
 
 108   my $period_start_date = shift;
 
 109   my $period_end_date   = $period_start_date->clone->add(months => $config->get_billing_period_length)->subtract(days => 1);
 
 111   my @month_names       = ('',
 
 112                            $::locale->text('January'), $::locale->text('February'), $::locale->text('March'),     $::locale->text('April'),   $::locale->text('May'),      $::locale->text('June'),
 
 113                            $::locale->text('July'),    $::locale->text('August'),   $::locale->text('September'), $::locale->text('October'), $::locale->text('November'), $::locale->text('December'));
 
 116     current_quarter     => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $_[0]->quarter } ],
 
 117     previous_quarter    => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 3), sub { $_[0]->quarter } ],
 
 118     next_quarter        => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 3), sub { $_[0]->quarter } ],
 
 120     current_month       => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $_[0]->month } ],
 
 121     previous_month      => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $_[0]->month } ],
 
 122     next_month          => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 1), sub { $_[0]->month } ],
 
 124     current_month_long  => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $month_names[ $_[0]->month ] } ],
 
 125     previous_month_long => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $month_names[ $_[0]->month ] } ],
 
 126     next_month_long     => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 1), sub { $month_names[ $_[0]->month ] } ],
 
 128     current_year        => [ $period_start_date->clone->truncate(to => 'year'),                         sub { $_[0]->year } ],
 
 129     previous_year       => [ $period_start_date->clone->truncate(to => 'year')->subtract(years => 1),   sub { $_[0]->year } ],
 
 130     next_year           => [ $period_start_date->clone->truncate(to => 'year')->add(     years => 1),   sub { $_[0]->year } ],
 
 132     period_start_date   => [ $period_start_date->clone, sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
 
 133     period_end_date     => [ $period_end_date,          sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
 
 141   my $sub      = $params{attribute};
 
 142   my $str      = $params{object}->$sub // '';
 
 143   my $sub_fmt  = lc($params{attribute_format} // 'text');
 
 145   my ($start_tag, $end_tag) = $sub_fmt eq 'html' ? ('<%', '%>') : ('<%', '%>');
 
 147   $str =~ s{ ${start_tag} ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? ${end_tag} }{
 
 148     my ($key, $format) = ($1, $3);
 
 149     $key               = $::locale->unquote_special_chars('html', $key) if $sub_fmt eq 'html';
 
 152     if ($params{vars}->{$key} && $format) {
 
 153       $format    = $::locale->unquote_special_chars('html', $format) if $sub_fmt eq 'html';
 
 155       $new_value = DateTime::Format::Strptime->new(
 
 158         time_zone   => 'local',
 
 159       )->format_datetime($params{vars}->{$key}->[0]);
 
 161     } elsif ($params{vars}->{$key}) {
 
 162       $new_value = $params{vars}->{$1}->[1]->($params{vars}->{$1}->[0]);
 
 164     } elsif ($params{invoice} && $params{invoice}->can($key)) {
 
 165       $new_value = $params{invoice}->$key;
 
 169     $new_value   = $::locale->quote_special_chars('html', $new_value) if $sub_fmt eq 'html';
 
 175   $params{object}->$sub($str);
 
 178 sub _adjust_sellprices_for_period_lengths {
 
 181   my $billing_len     = $params{config}->get_billing_period_length;
 
 182   my $order_value_len = $params{config}->get_order_value_period_length;
 
 184   return if $billing_len == $order_value_len;
 
 186   my $is_last_invoice_in_cycle = $params{config}->is_last_bill_date_in_order_value_cycle(date => $params{period_start_date});
 
 188   _log_msg("_adjust_sellprices_for_period_lengths: period_start_date $params{period_start_date} is_last_invoice_in_cycle $is_last_invoice_in_cycle billing_len $billing_len order_value_len $order_value_len");
 
 190   if ($order_value_len < $billing_len) {
 
 191     my $num_orders_per_invoice = $billing_len / $order_value_len;
 
 193     $_->sellprice($_->sellprice * $num_orders_per_invoice) for @{ $params{invoice}->items };
 
 198   my $num_invoices_in_cycle = $order_value_len / $billing_len;
 
 200   foreach my $item (@{ $params{invoice}->items }) {
 
 201     my $sellprice_one_invoice = $::form->round_amount($item->sellprice * $billing_len / $order_value_len, 2);
 
 203     if ($is_last_invoice_in_cycle) {
 
 204       $item->sellprice($item->sellprice - ($num_invoices_in_cycle - 1) * $sellprice_one_invoice);
 
 207       $item->sellprice($sellprice_one_invoice);
 
 212 sub _create_periodic_invoice {
 
 215   my $period_start_date = shift;
 
 217   my $time_period_vars  = _generate_time_period_variables($config, $period_start_date);
 
 219   my $invdate           = DateTime->today_local;
 
 221   my $order   = $config->order;
 
 223   if (!$self->{db_obj}->db->with_transaction(sub {
 
 224     1;                          # make Emacs happy
 
 226     $invoice = SL::DB::Invoice->new_from($order);
 
 228     my $tax_point = ($invoice->tax_point // $time_period_vars->{period_end_date}->[0])->clone;
 
 230     while ($tax_point < $period_start_date) {
 
 231       $tax_point->add(months => $config->get_billing_period_length);
 
 234     my $intnotes  = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
 
 235     $intnotes    .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
 
 237     $invoice->assign_attributes(deliverydate => $period_start_date,
 
 238                                 tax_point    => $tax_point,
 
 239                                 intnotes     => $intnotes,
 
 240                                 employee     => $order->employee, # new_from sets employee to import user
 
 241                                 direct_debit => $config->direct_debit,
 
 244     _replace_vars(object => $invoice, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'notes' ? 'html' : 'text')) for qw(notes intnotes transaction_description);
 
 246     foreach my $item (@{ $invoice->items }) {
 
 247       _replace_vars(object => $item, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'longdescription' ? 'html' : 'text')) for qw(description longdescription);
 
 250     _adjust_sellprices_for_period_lengths(invoice => $invoice, config => $config, period_start_date => $period_start_date);
 
 252     $invoice->post(ar_id => $config->ar_chart_id) || die;
 
 254     $order->link_to_record($invoice);
 
 256     foreach my $item (@{ $invoice->items }) {
 
 257       foreach (qw(orderitems)) {    # expand if needed (delivery_order_items)
 
 258           if ($item->{"converted_from_${_}_id"}) {
 
 259             die unless $item->{id};
 
 260             RecordLinks->create_links('mode'       => 'ids',
 
 262                                       'from_ids'   => $item->{"converted_from_${_}_id"},
 
 263                                       'to_table'   => 'invoice',
 
 264                                       'to_id'      => $item->{id},
 
 266             delete $item->{"converted_from_${_}_id"};
 
 271     SL::DB::PeriodicInvoice->new(config_id         => $config->id,
 
 272                                  ar_id             => $invoice->id,
 
 273                                  period_start_date => $period_start_date)
 
 276     _log_msg("_create_invoice created for period start date $period_start_date id " . $invoice->id . " number " . $invoice->invnumber . " netamount " . $invoice->netamount . " amount " . $invoice->amount);
 
 278     # die $invoice->transaction_description;
 
 282     $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
 
 288     period_start_date => $period_start_date,
 
 290     time_period_vars  => $time_period_vars,
 
 294 sub _calculate_dates {
 
 296   return $config->calculate_invoice_dates(end_date => DateTime->today_local);
 
 299 sub _send_summary_email {
 
 301   my %config = %::lx_office_conf;
 
 303   return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $self->{posted_invoices} };
 
 305   return if $config{periodic_invoices}->{send_for_errors_only} && !@{ $self->{printed_failed} } && !@{ $self->{emailed_failed} };
 
 307   my $email = $config{periodic_invoices}->{send_email_to};
 
 308   if ($email !~ m{\@}) {
 
 309     my $user = SL::DB::Manager::AuthUser->find_by(login => $email);
 
 310     $email   = $user ? $user->get_config_value('email') : undef;
 
 313   _log_msg("_send_summary_email: about to send to '" . ($email || '') . "'");
 
 315   return unless $email;
 
 317   my $template = Template->new({ 'INTERPOLATE' => 0,
 
 323   return unless $template;
 
 325   my $email_template = $config{periodic_invoices}->{email_template};
 
 326   my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
 
 327   my %params         = map { (uc($_) => $self->{$_}) } qw(posted_invoices printed_invoices printed_failed emailed_invoices emailed_failed disabled_orders);
 
 330   $template->process($filename, \%params, \$output) || die $template->error;
 
 332   my $mail              = Mailer->new;
 
 333   $mail->{from}         = $config{periodic_invoices}->{email_from};
 
 334   $mail->{to}           = $email;
 
 335   $mail->{subject}      = $config{periodic_invoices}->{email_subject};
 
 336   $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
 
 337   $mail->{message}      = $output;
 
 342 sub _store_pdf_in_webdav {
 
 343   my ($self, $pdf_file_name, $invoice) = @_;
 
 345   return unless $::instance_conf->get_webdav_documents;
 
 347   my $form = Form->new('');
 
 349   $form->{cwd}              = SL::System::Process->exe_dir;
 
 350   $form->{tmpdir}           = ($pdf_file_name =~ m{(.+)/})[0];
 
 351   $form->{tmpfile}          = ($pdf_file_name =~ m{.+/(.+)})[0];
 
 352   $form->{format}           = 'pdf';
 
 353   $form->{formname}         = 'invoice';
 
 354   $form->{type}             = 'invoice';
 
 355   $form->{vc}               = 'customer';
 
 356   $form->{invnumber}        = $invoice->invnumber;
 
 357   $form->{recipient_locale} = $invoice->language ? $invoice->language->template_code : '';
 
 359   Common::copy_file_to_webdav_folder($form);
 
 362 sub _store_pdf_in_filemanagement {
 
 363   my ($self, $pdf_file, $invoice) = @_;
 
 365   return unless $::instance_conf->get_doc_storage;
 
 367   # create a form for generate_attachment_filename
 
 368   my $form = Form->new('');
 
 369   $form->{invnumber} = $invoice->invnumber;
 
 370   $form->{type}      = 'invoice';
 
 371   $form->{format}    = 'pdf';
 
 372   $form->{formname}  = 'invoice';
 
 373   $form->{language}  = '_' . $invoice->language->template_code if $invoice->language;
 
 374   my $doc_name       = $form->generate_attachment_filename();
 
 376   SL::File->save(object_id   => $invoice->id,
 
 377                  object_type => 'invoice',
 
 378                  mime_type   => 'application/pdf',
 
 380                  file_type   => 'document',
 
 381                  file_name   => $doc_name,
 
 382                  file_path   => $pdf_file);
 
 386   my ($self, $data) = @_;
 
 388   my $invoice       = $data->{invoice};
 
 389   my $config        = $data->{config};
 
 391   return unless $config->print && $config->printer_id && $config->printer->printer_command;
 
 393   my $form = Form->new;
 
 394   $invoice->flatten_to_form($form, format_amounts => 1);
 
 396   $form->{printer_code} = $config->printer->template_code;
 
 397   $form->{copies}       = $config->copies;
 
 398   $form->{formname}     = $form->{type};
 
 399   $form->{format}       = 'pdf';
 
 400   $form->{media}        = 'printer';
 
 401   $form->{OUT}          = $config->printer->printer_command;
 
 402   $form->{OUT_MODE}     = '|-';
 
 404   $form->{TEMPLATE_DRIVER_OPTIONS} = { };
 
 405   $form->{TEMPLATE_DRIVER_OPTIONS}->{variable_content_types} = $form->get_variable_content_types();
 
 407   $form->prepare_for_printing;
 
 409   $form->throw_on_error(sub {
 
 411       $form->parse_template(\%::myconfig);
 
 412       push @{ $self->{printed_invoices} }, $invoice;
 
 415       push @{ $self->{job_errors} }, $EVAL_ERROR->error;
 
 416       push @{ $self->{printed_failed} }, [ $invoice, $EVAL_ERROR->error ];
 
 422   my ($self, $data) = @_;
 
 424   $data->{config}->load;
 
 426   return unless $data->{config}->send_email;
 
 433     (split(m{,}, $data->{config}->email_recipient_address),
 
 434      $data->{config}->email_recipient_contact   ? ($data->{config}->email_recipient_contact->cp_email) : (),
 
 435      $data->{invoice}->{customer}->invoice_mail ? ($data->{invoice}->{customer}->invoice_mail) : ()
 
 438   return unless @recipients;
 
 440   my $language      = $data->{invoice}->language ? $data->{invoice}->language->template_code : undef;
 
 441   my %create_params = (
 
 442     template               => scalar($self->find_template(name => 'invoice', language => $language)),
 
 443     variables              => Form->new(''),
 
 444     return                 => 'file_name',
 
 445     record                 => $data->{invoice},
 
 446     variable_content_types => {
 
 447       longdescription => 'html',
 
 453   $data->{invoice}->flatten_to_form($create_params{variables}, format_amounts => 1);
 
 454   $create_params{variables}->prepare_for_printing;
 
 457   my $label = $language && Locale::is_supported($language) ? Locale->new($language)->text('Invoice') : $::locale->text('Invoice');
 
 460     $pdf_file_name = $self->create_pdf(%create_params);
 
 462     $self->_store_pdf_in_webdav        ($pdf_file_name, $data->{invoice});
 
 463     $self->_store_pdf_in_filemanagement($pdf_file_name, $data->{invoice});
 
 465     for (qw(email_subject email_body)) {
 
 467         object           => $data->{config},
 
 468         invoice          => $data->{invoice},
 
 469         vars             => $data->{time_period_vars},
 
 471         attribute_format => 'text'
 
 475     my $global_bcc = SL::DB::Default->get->global_bcc;
 
 478     for my $recipient (@recipients) {
 
 479       my $mail             = Mailer->new;
 
 480       $mail->{record_id}   = $data->{invoice}->id,
 
 481       $mail->{record_type} = 'invoice',
 
 482       $mail->{from}        = $data->{config}->email_sender || $::lx_office_conf{periodic_invoices}->{email_from};
 
 483       $mail->{to}          = $recipient;
 
 484       $mail->{bcc}         = $global_bcc;
 
 485       $mail->{subject}     = $data->{config}->email_subject;
 
 486       $mail->{message}     = $data->{config}->email_body;
 
 487       $mail->{attachments} = [{
 
 488         path     => $pdf_file_name,
 
 489         name     => sprintf('%s %s.pdf', $label, $data->{invoice}->invnumber),
 
 492       my $error        = $mail->send;
 
 495         push @{ $self->{job_errors} }, $error;
 
 496         push @{ $self->{emailed_failed} }, [ $data->{invoice}, $error ];
 
 501     push @{ $self->{emailed_invoices} }, $data->{invoice} unless $overall_error;
 
 506     push @{ $self->{job_errors} }, $EVAL_ERROR;
 
 507     push @{ $self->{emailed_failed} }, [ $data->{invoice}, $EVAL_ERROR ];
 
 510   unlink $pdf_file_name if $pdf_file_name;
 
 523 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
 
 528 Iterate over all periodic invoice configurations, extend them if
 
 529 applicable, calculate the dates for which invoices have to be posted
 
 530 and post those invoices by converting the order into an invoice for
 
 539 Strings like month names are hardcoded to German in this file.
 
 545 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>