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);
 
  16 use SL::DB::PeriodicInvoice;
 
  17 use SL::DB::PeriodicInvoicesConfig;
 
  18 use SL::Helper::CreatePDF qw(create_pdf find_template);
 
  20 use SL::Util qw(trim);
 
  23   $_[0]->create_standard_job('0 3 1 * *'); # first day of month at 3:00 am
 
  28   $self->{db_obj} = shift;
 
  30   $self->{job_errors} = [];
 
  31   if (!$self->{db_obj}->db->with_transaction(sub {
 
  34     my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(query => [ active => 1 ]);
 
  36     foreach my $config (@{ $configs }) {
 
  37       my $new_end_date = $config->handle_automatic_extension;
 
  38       _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date;
 
  41     my (@new_invoices, @invoices_to_print, @invoices_to_email, @disabled_orders);
 
  43     _log_msg("Number of configs: " . scalar(@{ $configs}));
 
  45     foreach my $config (@{ $configs }) {
 
  46       # A configuration can be set to inactive by
 
  47       # $config->handle_automatic_extension. Therefore the check in
 
  48       # ...->get_all() does not suffice.
 
  49       _log_msg("Config " . $config->id . " active " . $config->active);
 
  50       next unless $config->active;
 
  52       my @dates = _calculate_dates($config);
 
  54       _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates));
 
  56       foreach my $date (@dates) {
 
  57         my $data = $self->_create_periodic_invoice($config, $date);
 
  60         _log_msg("Invoice " . $data->{invoice}->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n");
 
  62         push @new_invoices,      $data;
 
  63         push @invoices_to_print, $data if $config->print;
 
  64         push @invoices_to_email, $data if $config->send_email;
 
  66         my $inactive_ordnumber = $config->disable_one_time_config;
 
  67         if ($inactive_ordnumber) {
 
  68           # disable one time configs and skip eventual invoices
 
  69           _log_msg("Order " . $inactive_ordnumber . " deavtivated \n");
 
  70           push @disabled_orders, $inactive_ordnumber;
 
  76     foreach my $inv ( @invoices_to_print ) { $self->_print_invoice($inv); }
 
  77     foreach my $inv ( @invoices_to_email ) { $self->_email_invoice($inv); }
 
  79     $self->_send_summary_email(
 
  80       [ map { $_->{invoice} } @new_invoices      ],
 
  81       [ map { $_->{invoice} } @invoices_to_print ],
 
  82       [ map { $_->{invoice} } @invoices_to_email ],
 
  88       $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
 
  92     if (@{ $self->{job_errors} }) {
 
  93       my $msg = join "\n", @{ $self->{job_errors} };
 
  94       _log_msg("Errors: $msg");
 
 102   my $message  = join('', 'SL::BackgroundJob::CreatePeriodicInvoices: ', @_);
 
 103   $message    .= "\n" unless $message =~ m/\n$/;
 
 104   $::lxdebug->message(LXDebug::DEBUG1(), $message);
 
 107 sub _generate_time_period_variables {
 
 109   my $period_start_date = shift;
 
 110   my $period_end_date   = $period_start_date->clone->add(months => $config->get_billing_period_length)->subtract(days => 1);
 
 112   my @month_names       = ('',
 
 113                            $::locale->text('January'), $::locale->text('February'), $::locale->text('March'),     $::locale->text('April'),   $::locale->text('May'),      $::locale->text('June'),
 
 114                            $::locale->text('July'),    $::locale->text('August'),   $::locale->text('September'), $::locale->text('October'), $::locale->text('November'), $::locale->text('December'));
 
 117     current_quarter     => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $_[0]->quarter } ],
 
 118     previous_quarter    => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 3), sub { $_[0]->quarter } ],
 
 119     next_quarter        => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 3), sub { $_[0]->quarter } ],
 
 121     current_month       => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $_[0]->month } ],
 
 122     previous_month      => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $_[0]->month } ],
 
 123     next_month          => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 1), sub { $_[0]->month } ],
 
 125     current_month_long  => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $month_names[ $_[0]->month ] } ],
 
 126     previous_month_long => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $month_names[ $_[0]->month ] } ],
 
 127     next_month_long     => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 1), sub { $month_names[ $_[0]->month ] } ],
 
 129     current_year        => [ $period_start_date->clone->truncate(to => 'year'),                         sub { $_[0]->year } ],
 
 130     previous_year       => [ $period_start_date->clone->truncate(to => 'year')->subtract(years => 1),   sub { $_[0]->year } ],
 
 131     next_year           => [ $period_start_date->clone->truncate(to => 'year')->add(     years => 1),   sub { $_[0]->year } ],
 
 133     period_start_date   => [ $period_start_date->clone, sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
 
 134     period_end_date     => [ $period_end_date,          sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
 
 142   my $sub      = $params{attribute};
 
 143   my $str      = $params{object}->$sub // '';
 
 144   my $sub_fmt  = lc($params{attribute_format} // 'text');
 
 146   my ($start_tag, $end_tag) = $sub_fmt eq 'html' ? ('<%', '%>') : ('<%', '%>');
 
 148   $str =~ s{ ${start_tag} ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? ${end_tag} }{
 
 149     my ($key, $format) = ($1, $3);
 
 150     $key               = $::locale->unquote_special_chars('html', $key) if $sub_fmt eq 'html';
 
 153     if (!$params{vars}->{$key}) {
 
 157       $format    = $::locale->unquote_special_chars('html', $format) if $sub_fmt eq 'html';
 
 159       $new_value = DateTime::Format::Strptime->new(
 
 162         time_zone   => 'local',
 
 163       )->format_datetime($params{vars}->{$key}->[0]);
 
 166       $new_value = $params{vars}->{$1}->[1]->($params{vars}->{$1}->[0]);
 
 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 $intnotes  = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
 
 229     $intnotes    .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
 
 231     $invoice->assign_attributes(deliverydate => $period_start_date,
 
 232                                 intnotes     => $intnotes,
 
 233                                 employee     => $order->employee, # new_from sets employee to import user
 
 234                                 direct_debit => $config->direct_debit,
 
 237     _replace_vars(object => $invoice, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'notes' ? 'html' : 'text')) for qw(notes intnotes transaction_description);
 
 239     foreach my $item (@{ $invoice->items }) {
 
 240       _replace_vars(object => $item, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'longdescription' ? 'html' : 'text')) for qw(description longdescription);
 
 243     _adjust_sellprices_for_period_lengths(invoice => $invoice, config => $config, period_start_date => $period_start_date);
 
 245     $invoice->post(ar_id => $config->ar_chart_id) || die;
 
 247     $order->link_to_record($invoice);
 
 249     foreach my $item (@{ $invoice->items }) {
 
 250       foreach (qw(orderitems)) {    # expand if needed (delivery_order_items)
 
 251           if ($item->{"converted_from_${_}_id"}) {
 
 252             die unless $item->{id};
 
 253             RecordLinks->create_links('mode'       => 'ids',
 
 255                                       'from_ids'   => $item->{"converted_from_${_}_id"},
 
 256                                       'to_table'   => 'invoice',
 
 257                                       'to_id'      => $item->{id},
 
 259             delete $item->{"converted_from_${_}_id"};
 
 264     SL::DB::PeriodicInvoice->new(config_id         => $config->id,
 
 265                                  ar_id             => $invoice->id,
 
 266                                  period_start_date => $period_start_date)
 
 269     _log_msg("_create_invoice created for period start date $period_start_date id " . $invoice->id . " number " . $invoice->invnumber . " netamount " . $invoice->netamount . " amount " . $invoice->amount);
 
 271     # die $invoice->transaction_description;
 
 275     $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
 
 281     period_start_date => $period_start_date,
 
 283     time_period_vars  => $time_period_vars,
 
 287 sub _calculate_dates {
 
 289   return $config->calculate_invoice_dates(end_date => DateTime->today_local);
 
 292 sub _send_summary_email {
 
 293   my ($self, $posted_invoices, $printed_invoices, $emailed_invoices,
 
 294       $disabled_orders) = @_;
 
 295   my %config = %::lx_office_conf;
 
 297   return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
 
 299   my $user  = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
 
 300   my $email = $user ? $user->get_config_value('email') : undef;
 
 302   return unless $email;
 
 304   my $template = Template->new({ 'INTERPOLATE' => 0,
 
 310   return unless $template;
 
 312   my $email_template = $config{periodic_invoices}->{email_template};
 
 313   my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
 
 314   my %params         = ( POSTED_INVOICES  => $posted_invoices,
 
 315                          PRINTED_INVOICES => $printed_invoices,
 
 316                          EMAILED_INVOICES => $emailed_invoices,
 
 317                          DISABLED_ORDERS  => $disabled_orders );
 
 320   $template->process($filename, \%params, \$output);
 
 322   my $mail              = Mailer->new;
 
 323   $mail->{from}         = $config{periodic_invoices}->{email_from};
 
 324   $mail->{to}           = $email;
 
 325   $mail->{subject}      = $config{periodic_invoices}->{email_subject};
 
 326   $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
 
 327   $mail->{message}      = $output;
 
 333   my ($self, $data) = @_;
 
 335   my $invoice       = $data->{invoice};
 
 336   my $config        = $data->{config};
 
 338   return unless $config->print && $config->printer_id && $config->printer->printer_command;
 
 340   my $form = Form->new;
 
 341   $invoice->flatten_to_form($form, format_amounts => 1);
 
 343   $form->{printer_code} = $config->printer->template_code;
 
 344   $form->{copies}       = $config->copies;
 
 345   $form->{formname}     = $form->{type};
 
 346   $form->{format}       = 'pdf';
 
 347   $form->{media}        = 'printer';
 
 348   $form->{OUT}          = $config->printer->printer_command;
 
 349   $form->{OUT_MODE}     = '|-';
 
 351   $form->{TEMPLATE_DRIVER_OPTIONS} = { };
 
 352   $form->{TEMPLATE_DRIVER_OPTIONS}->{variable_content_types} = $form->get_variable_content_types();
 
 354   $form->prepare_for_printing;
 
 356   $form->throw_on_error(sub {
 
 358       $form->parse_template(\%::myconfig);
 
 361       push @{ $self->{job_errors} }, $EVAL_ERROR->error;
 
 367   my ($self, $data) = @_;
 
 369   $data->{config}->load;
 
 371   return unless $data->{config}->send_email;
 
 378     (split(m{,}, $data->{config}->email_recipient_address),
 
 379      $data->{config}->email_recipient_contact   ? ($data->{config}->email_recipient_contact->cp_email) : (),
 
 380      $data->{invoice}->{customer}->invoice_mail ? ($data->{invoice}->{customer}->invoice_mail) : ()
 
 383   return unless @recipients;
 
 385   my $language      = $data->{invoice}->language ? $data->{invoice}->language->template_code : undef;
 
 386   my %create_params = (
 
 387     template               => scalar($self->find_template(name => 'invoice', language => $language)),
 
 388     variables              => Form->new(''),
 
 389     return                 => 'file_name',
 
 390     variable_content_types => {
 
 391       longdescription => 'html',
 
 397   $data->{invoice}->flatten_to_form($create_params{variables}, format_amounts => 1);
 
 398   $create_params{variables}->prepare_for_printing;
 
 401   my $label = $language && Locale::is_supported($language) ? Locale->new($language)->text('Invoice') : $::locale->text('Invoice');
 
 404     $pdf_file_name = $self->create_pdf(%create_params);
 
 406     for (qw(email_subject email_body)) {
 
 408         object           => $data->{config},
 
 409         vars             => $data->{time_period_vars},
 
 411         attribute_format => 'text'
 
 415     my $global_bcc = SL::DB::Default->get->global_bcc;
 
 417     for my $recipient (@recipients) {
 
 418       my $mail             = Mailer->new;
 
 419       $mail->{record_id}   = $data->{invoice}->id,
 
 420       $mail->{record_type} = 'invoice',
 
 421       $mail->{from}        = $data->{config}->email_sender || $::lx_office_conf{periodic_invoices}->{email_from};
 
 422       $mail->{to}          = $recipient;
 
 423       $mail->{bcc}         = $global_bcc;
 
 424       $mail->{subject}     = $data->{config}->email_subject;
 
 425       $mail->{message}     = $data->{config}->email_body;
 
 426       $mail->{attachments} = [{
 
 427         path     => $pdf_file_name,
 
 428         name     => sprintf('%s %s.pdf', $label, $data->{invoice}->invnumber),
 
 431       my $error        = $mail->send;
 
 433       push @{ $self->{job_errors} }, $error if $error;
 
 439     push @{ $self->{job_errors} }, $EVAL_ERROR;
 
 442   unlink $pdf_file_name if $pdf_file_name;
 
 455 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
 
 460 Iterate over all periodic invoice configurations, extend them if
 
 461 applicable, calculate the dates for which invoices have to be posted
 
 462 and post those invoices by converting the order into an invoice for
 
 471 Strings like month names are hardcoded to German in this file.
 
 477 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>