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} = [];
 
  32   my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(query => [ active => 1 ]);
 
  34   foreach my $config (@{ $configs }) {
 
  35     my $new_end_date = $config->handle_automatic_extension;
 
  36     _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date;
 
  39   my (@new_invoices, @invoices_to_print, @invoices_to_email);
 
  41   _log_msg("Number of configs: " . scalar(@{ $configs}));
 
  43   foreach my $config (@{ $configs }) {
 
  44     # A configuration can be set to inactive by
 
  45     # $config->handle_automatic_extension. Therefore the check in
 
  46     # ...->get_all() does not suffice.
 
  47     _log_msg("Config " . $config->id . " active " . $config->active);
 
  48     next unless $config->active;
 
  50     my @dates = _calculate_dates($config);
 
  52     _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates));
 
  54     foreach my $date (@dates) {
 
  55       my $data = $self->_create_periodic_invoice($config, $date);
 
  58       _log_msg("Invoice " . $data->{invoice}->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n");
 
  60       push @new_invoices,      $data;
 
  61       push @invoices_to_print, $data if $config->print;
 
  62       push @invoices_to_email, $data if $config->send_email;
 
  68   $self->_print_invoice($_) for @invoices_to_print;
 
  69   $self->_email_invoice($_) for @invoices_to_email;
 
  71   $self->_send_summary_email(
 
  72     [ map { $_->{invoice} } @new_invoices      ],
 
  73     [ map { $_->{invoice} } @invoices_to_print ],
 
  74     [ map { $_->{invoice} } @invoices_to_email ],
 
  77   if (@{ $self->{job_errors} }) {
 
  78     my $msg = join "\n", @{ $self->{job_errors} };
 
  79     _log_msg("Errors: $msg");
 
  87   my $message  = join('', 'SL::BackgroundJob::CreatePeriodicInvoices: ', @_);
 
  88   $message    .= "\n" unless $message =~ m/\n$/;
 
  89   $::lxdebug->message(LXDebug::DEBUG1(), $message);
 
  92 sub _generate_time_period_variables {
 
  94   my $period_start_date = shift;
 
  95   my $period_end_date   = $period_start_date->clone->add(months => $config->get_billing_period_length)->subtract(days => 1);
 
  97   my @month_names       = ('',
 
  98                            $::locale->text('January'), $::locale->text('February'), $::locale->text('March'),     $::locale->text('April'),   $::locale->text('May'),      $::locale->text('June'),
 
  99                            $::locale->text('July'),    $::locale->text('August'),   $::locale->text('September'), $::locale->text('October'), $::locale->text('November'), $::locale->text('December'));
 
 102     current_quarter     => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $_[0]->quarter } ],
 
 103     previous_quarter    => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 3), sub { $_[0]->quarter } ],
 
 104     next_quarter        => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 3), sub { $_[0]->quarter } ],
 
 106     current_month       => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $_[0]->month } ],
 
 107     previous_month      => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $_[0]->month } ],
 
 108     next_month          => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 1), sub { $_[0]->month } ],
 
 110     current_month_long  => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $month_names[ $_[0]->month ] } ],
 
 111     previous_month_long => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $month_names[ $_[0]->month ] } ],
 
 112     next_month_long     => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 1), sub { $month_names[ $_[0]->month ] } ],
 
 114     current_year        => [ $period_start_date->clone->truncate(to => 'year'),                         sub { $_[0]->year } ],
 
 115     previous_year       => [ $period_start_date->clone->truncate(to => 'year')->subtract(years => 1),   sub { $_[0]->year } ],
 
 116     next_year           => [ $period_start_date->clone->truncate(to => 'year')->add(     years => 1),   sub { $_[0]->year } ],
 
 118     period_start_date   => [ $period_start_date->clone, sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
 
 119     period_end_date     => [ $period_end_date,          sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
 
 127   my $sub      = $params{attribute};
 
 128   my $str      = $params{object}->$sub // '';
 
 129   my $sub_fmt  = lc($params{attribute_format} // 'text');
 
 131   my ($start_tag, $end_tag) = $sub_fmt eq 'html' ? ('<%', '%>') : ('<%', '%>');
 
 133   $str =~ s{ ${start_tag} ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? ${end_tag} }{
 
 134     my ($key, $format) = ($1, $3);
 
 135     $key               = $::locale->unquote_special_chars('html', $key) if $sub_fmt eq 'html';
 
 138     if (!$params{vars}->{$key}) {
 
 142       $format    = $::locale->unquote_special_chars('html', $format) if $sub_fmt eq 'html';
 
 144       $new_value = DateTime::Format::Strptime->new(
 
 147         time_zone   => 'local',
 
 148       )->format_datetime($params{vars}->{$key}->[0]);
 
 151       $new_value = $params{vars}->{$1}->[1]->($params{vars}->{$1}->[0]);
 
 154     $new_value = $::locale->quote_special_chars('html', $new_value) if $sub_fmt eq 'html';
 
 160   $params{object}->$sub($str);
 
 163 sub _adjust_sellprices_for_period_lengths {
 
 166   my $billing_len     = $params{config}->get_billing_period_length;
 
 167   my $order_value_len = $params{config}->get_order_value_period_length;
 
 169   return if $billing_len == $order_value_len;
 
 171   my $is_last_invoice_in_cycle = $params{config}->is_last_bill_date_in_order_value_cycle(date => $params{period_start_date});
 
 173   _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");
 
 175   if ($order_value_len < $billing_len) {
 
 176     my $num_orders_per_invoice = $billing_len / $order_value_len;
 
 178     $_->sellprice($_->sellprice * $num_orders_per_invoice) for @{ $params{invoice}->items };
 
 183   my $num_invoices_in_cycle = $order_value_len / $billing_len;
 
 185   foreach my $item (@{ $params{invoice}->items }) {
 
 186     my $sellprice_one_invoice = $::form->round_amount($item->sellprice * $billing_len / $order_value_len, 2);
 
 188     if ($is_last_invoice_in_cycle) {
 
 189       $item->sellprice($item->sellprice - ($num_invoices_in_cycle - 1) * $sellprice_one_invoice);
 
 192       $item->sellprice($sellprice_one_invoice);
 
 197 sub _create_periodic_invoice {
 
 200   my $period_start_date = shift;
 
 202   my $time_period_vars  = _generate_time_period_variables($config, $period_start_date);
 
 204   my $invdate           = DateTime->today_local;
 
 206   my $order   = $config->order;
 
 208   if (!$self->{db_obj}->db->with_transaction(sub {
 
 209     1;                          # make Emacs happy
 
 211     $invoice = SL::DB::Invoice->new_from($order);
 
 213     my $intnotes  = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
 
 214     $intnotes    .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
 
 216     $invoice->assign_attributes(deliverydate => $period_start_date,
 
 217                                 intnotes     => $intnotes,
 
 218                                 employee     => $order->employee, # new_from sets employee to import user
 
 219                                 direct_debit => $config->direct_debit,
 
 222     _replace_vars(object => $invoice, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'notes' ? 'html' : 'text')) for qw(notes intnotes transaction_description);
 
 224     foreach my $item (@{ $invoice->items }) {
 
 225       _replace_vars(object => $item, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'longdescription' ? 'html' : 'text')) for qw(description longdescription);
 
 228     _adjust_sellprices_for_period_lengths(invoice => $invoice, config => $config, period_start_date => $period_start_date);
 
 230     $invoice->post(ar_id => $config->ar_chart_id) || die;
 
 232     $order->link_to_record($invoice);
 
 234     foreach my $item (@{ $invoice->items }) {
 
 235       foreach (qw(orderitems)) {    # expand if needed (delivery_order_items)
 
 236           if ($item->{"converted_from_${_}_id"}) {
 
 237             die unless $item->{id};
 
 238             RecordLinks->create_links('mode'       => 'ids',
 
 240                                       'from_ids'   => $item->{"converted_from_${_}_id"},
 
 241                                       'to_table'   => 'invoice',
 
 242                                       'to_id'      => $item->{id},
 
 244             delete $item->{"converted_from_${_}_id"};
 
 249     SL::DB::PeriodicInvoice->new(config_id         => $config->id,
 
 250                                  ar_id             => $invoice->id,
 
 251                                  period_start_date => $period_start_date)
 
 254     _log_msg("_create_invoice created for period start date $period_start_date id " . $invoice->id . " number " . $invoice->invnumber . " netamount " . $invoice->netamount . " amount " . $invoice->amount);
 
 256     # die $invoice->transaction_description;
 
 260     $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
 
 266     period_start_date => $period_start_date,
 
 268     time_period_vars  => $time_period_vars,
 
 272 sub _calculate_dates {
 
 274   return $config->calculate_invoice_dates(end_date => DateTime->today_local);
 
 277 sub _send_summary_email {
 
 278   my ($self, $posted_invoices, $printed_invoices, $emailed_invoices) = @_;
 
 280   my %config = %::lx_office_conf;
 
 282   return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
 
 284   my $user  = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
 
 285   my $email = $user ? $user->get_config_value('email') : undef;
 
 287   return unless $email;
 
 289   my $template = Template->new({ 'INTERPOLATE' => 0,
 
 295   return unless $template;
 
 297   my $email_template = $config{periodic_invoices}->{email_template};
 
 298   my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
 
 299   my %params         = ( POSTED_INVOICES  => $posted_invoices,
 
 300                          PRINTED_INVOICES => $printed_invoices,
 
 301                          EMAILED_INVOICES => $emailed_invoices );
 
 304   $template->process($filename, \%params, \$output);
 
 306   my $mail              = Mailer->new;
 
 307   $mail->{from}         = $config{periodic_invoices}->{email_from};
 
 308   $mail->{to}           = $email;
 
 309   $mail->{subject}      = $config{periodic_invoices}->{email_subject};
 
 310   $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
 
 311   $mail->{message}      = $output;
 
 317   my ($self, $data) = @_;
 
 319   my $invoice       = $data->{invoice};
 
 320   my $config        = $data->{config};
 
 322   return unless $config->print && $config->printer_id && $config->printer->printer_command;
 
 324   my $form = Form->new;
 
 325   $invoice->flatten_to_form($form, format_amounts => 1);
 
 327   $form->{printer_code} = $config->printer->template_code;
 
 328   $form->{copies}       = $config->copies;
 
 329   $form->{formname}     = $form->{type};
 
 330   $form->{format}       = 'pdf';
 
 331   $form->{media}        = 'printer';
 
 332   $form->{OUT}          = $config->printer->printer_command;
 
 333   $form->{OUT_MODE}     = '|-';
 
 335   $form->{TEMPLATE_DRIVER_OPTIONS} = {
 
 336     variable_content_types => {
 
 337       longdescription => 'html',
 
 343   $form->prepare_for_printing;
 
 345   $form->throw_on_error(sub {
 
 347       $form->parse_template(\%::myconfig);
 
 350       push @{ $self->{job_errors} }, $EVAL_ERROR->getMessage;
 
 356   my ($self, $data) = @_;
 
 358   $data->{config}->load;
 
 360   return unless $data->{config}->send_email;
 
 367     (split(m{,}, $data->{config}->email_recipient_address),
 
 368      $data->{config}->email_recipient_contact ? ($data->{config}->email_recipient_contact->cp_email) : ());
 
 370   return unless @recipients;
 
 372   my %create_params = (
 
 373     template               => $self->find_template(name => 'invoice'),
 
 374     variables              => Form->new(''),
 
 375     return                 => 'file_name',
 
 376     variable_content_types => {
 
 377       longdescription => 'html',
 
 383   $data->{invoice}->flatten_to_form($create_params{variables}, format_amounts => 1);
 
 384   $create_params{variables}->prepare_for_printing;
 
 389     $pdf_file_name = $self->create_pdf(%create_params);
 
 391     for (qw(email_subject email_body)) {
 
 393         object           => $data->{config},
 
 394         vars             => $data->{time_period_vars},
 
 396         attribute_format => 'text'
 
 400     for my $recipient (@recipients) {
 
 401       my $mail             = Mailer->new;
 
 402       $mail->{from}        = $data->{config}->email_sender || $::lx_office_conf{periodic_invoices}->{email_from};
 
 403       $mail->{to}          = $recipient;
 
 404       $mail->{subject}     = $data->{config}->email_subject;
 
 405       $mail->{message}     = $data->{config}->email_body;
 
 406       $mail->{attachments} = [{
 
 407         filename => $pdf_file_name,
 
 408         name     => sprintf('%s %s.pdf', $::locale->text('Invoice'), $data->{invoice}->invnumber),
 
 411       my $error        = $mail->send;
 
 413       push @{ $self->{job_errors} }, $error if $error;
 
 419     push @{ $self->{job_errors} }, $EVAL_ERROR;
 
 422   unlink $pdf_file_name if $pdf_file_name;
 
 435 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
 
 440 Iterate over all periodic invoice configurations, extend them if
 
 441 applicable, calculate the dates for which invoices have to be posted
 
 442 and post those invoices by converting the order into an invoice for
 
 451 Strings like month names are hardcoded to German in this file.
 
 457 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>