1 package SL::BackgroundJob::CreatePeriodicInvoices;
 
   5 use parent qw(SL::BackgroundJob::Base);
 
   8 use DateTime::Format::Strptime;
 
   9 use English qw(-no_match_vars);
 
  15 use SL::DB::PeriodicInvoice;
 
  16 use SL::DB::PeriodicInvoicesConfig;
 
  20   $_[0]->create_standard_job('0 3 1 * *'); # first day of month at 3:00 am
 
  25   $self->{db_obj} = shift;
 
  27   my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(query => [ active => 1 ]);
 
  29   foreach my $config (@{ $configs }) {
 
  30     my $new_end_date = $config->handle_automatic_extension;
 
  31     _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date;
 
  34   my (@new_invoices, @invoices_to_print);
 
  36   _log_msg("Number of configs: " . scalar(@{ $configs}));
 
  38   foreach my $config (@{ $configs }) {
 
  39     # A configuration can be set to inactive by
 
  40     # $config->handle_automatic_extension. Therefore the check in
 
  41     # ...->get_all() does not suffice.
 
  42     _log_msg("Config " . $config->id . " active " . $config->active);
 
  43     next unless $config->active;
 
  45     my @dates = _calculate_dates($config);
 
  47     _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates));
 
  49     foreach my $date (@dates) {
 
  50       my $invoice = $self->_create_periodic_invoice($config, $date);
 
  53       _log_msg("Invoice " . $invoice->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n");
 
  54       push @new_invoices,      $invoice;
 
  55       push @invoices_to_print, [ $invoice, $config ] if $config->print;
 
  61   map { _print_invoice(@{ $_ }) } @invoices_to_print;
 
  63   _send_email(\@new_invoices, [ map { $_->[0] } @invoices_to_print ]) if @new_invoices;
 
  69   my $message  = join('', @_);
 
  70   $message    .= "\n" unless $message =~ m/\n$/;
 
  71   $::lxdebug->message(LXDebug::DEBUG1(), $message);
 
  74 sub _generate_time_period_variables {
 
  76   my $period_start_date = shift;
 
  77   my $period_end_date   = $period_start_date->clone->truncate(to => 'month')->add(months => $config->get_period_length)->subtract(days => 1);
 
  79   my @month_names       = ('',
 
  80                            $::locale->text('January'), $::locale->text('February'), $::locale->text('March'),     $::locale->text('April'),   $::locale->text('May'),      $::locale->text('June'),
 
  81                            $::locale->text('July'),    $::locale->text('August'),   $::locale->text('September'), $::locale->text('October'), $::locale->text('November'), $::locale->text('December'));
 
  84     current_quarter     => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $_[0]->quarter } ],
 
  85     previous_quarter    => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 3), sub { $_[0]->quarter } ],
 
  86     next_quarter        => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 3), sub { $_[0]->quarter } ],
 
  88     current_month       => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $_[0]->month } ],
 
  89     previous_month      => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $_[0]->month } ],
 
  90     next_month          => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 1), sub { $_[0]->month } ],
 
  92     current_month_long  => [ $period_start_date->clone->truncate(to => 'month'),                        sub { $month_names[ $_[0]->month ] } ],
 
  93     previous_month_long => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $month_names[ $_[0]->month ] } ],
 
  94     next_month_long     => [ $period_start_date->clone->truncate(to => 'month')->add(     months => 1), sub { $month_names[ $_[0]->month ] } ],
 
  96     current_year        => [ $period_start_date->clone->truncate(to => 'year'),                         sub { $_[0]->year } ],
 
  97     previous_year       => [ $period_start_date->clone->truncate(to => 'year')->subtract(years => 1),   sub { $_[0]->year } ],
 
  98     next_year           => [ $period_start_date->clone->truncate(to => 'year')->add(     years => 1),   sub { $_[0]->year } ],
 
 100     period_start_date   => [ $period_start_date->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
 
 101     period_end_date     => [ $period_end_date,                                   sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
 
 111   my $str    = $object->$sub;
 
 113   $str =~ s{ <\% ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? \%>}{
 
 114     my ($key, $format) = ($1, $3);
 
 115     if (!$vars->{$key}) {
 
 119       DateTime::Format::Strptime->new(
 
 122         time_zone   => 'local',
 
 123       )->format_datetime($vars->{$key}->[0]);
 
 126       $vars->{$1}->[1]->($vars->{$1}->[0]);
 
 133 sub _create_periodic_invoice {
 
 136   my $period_start_date = shift;
 
 138   my $time_period_vars  = _generate_time_period_variables($config, $period_start_date);
 
 140   my $invdate           = DateTime->today_local;
 
 142   my $order   = $config->order;
 
 144   if (!$self->{db_obj}->db->do_transaction(sub {
 
 145     1;                          # make Emacs happy
 
 147     $invoice = SL::DB::Invoice->new_from($order);
 
 149     my $intnotes  = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
 
 150     $intnotes    .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
 
 152     $invoice->assign_attributes(deliverydate => $period_start_date,
 
 153                                 intnotes     => $intnotes,
 
 154                                 employee     => $order->employee, # new_from sets employee to import user
 
 157     map { _replace_vars($invoice, $time_period_vars, $_) } qw(notes intnotes transaction_description);
 
 159     foreach my $item (@{ $invoice->items }) {
 
 160       map { _replace_vars($item, $time_period_vars, $_) } qw(description longdescription);
 
 163     $invoice->post(ar_id => $config->ar_chart_id) || die;
 
 165     # like $form->add_shipto, but we don't need to check for a manual exception,
 
 166     # because we can already assume this (otherwise no shipto_id from order)
 
 167     if ($order->shipto_id) {
 
 169       my $shipto_oe = SL::DB::Manager::Shipto->find_by(shipto_id => $order->shipto_id);
 
 170       my $shipto_ar = $shipto_oe->clone_and_reset;
 
 172       $shipto_ar->module('AR');            # alter module OE -> AR
 
 173       $shipto_ar->trans_id($invoice->id);  # alter trans_id -> new id from invoice
 
 177     $order->link_to_record($invoice);
 
 179     SL::DB::PeriodicInvoice->new(config_id         => $config->id,
 
 180                                  ar_id             => $invoice->id,
 
 181                                  period_start_date => $period_start_date)
 
 184     # die $invoice->transaction_description;
 
 186     $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
 
 193 sub _calculate_dates {
 
 195   return $config->calculate_invoice_dates(end_date => DateTime->today_local);
 
 199   my ($posted_invoices, $printed_invoices) = @_;
 
 201   my %config = %::lx_office_conf;
 
 203   return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
 
 205   my $user  = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
 
 206   my $email = $user ? $user->get_config_value('email') : undef;
 
 208   return unless $email;
 
 210   my $template = Template->new({ 'INTERPOLATE' => 0,
 
 216   return unless $template;
 
 218   my $email_template = $config{periodic_invoices}->{email_template};
 
 219   my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
 
 220   my %params         = ( POSTED_INVOICES  => $posted_invoices,
 
 221                          PRINTED_INVOICES => $printed_invoices );
 
 224   $template->process($filename, \%params, \$output);
 
 226   my $mail              = Mailer->new;
 
 227   $mail->{from}         = $config{periodic_invoices}->{email_from};
 
 228   $mail->{to}           = $email;
 
 229   $mail->{subject}      = $config{periodic_invoices}->{email_subject};
 
 230   $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
 
 231   $mail->{message}      = $output;
 
 237   my ($invoice, $config) = @_;
 
 239   return unless $config->print && $config->printer_id && $config->printer->printer_command;
 
 241   my $form = Form->new;
 
 242   $invoice->flatten_to_form($form, format_amounts => 1);
 
 244   $form->{printer_code} = $config->printer->template_code;
 
 245   $form->{copies}       = $config->copies;
 
 246   $form->{formname}     = $form->{type};
 
 247   $form->{format}       = 'pdf';
 
 248   $form->{media}        = 'printer';
 
 249   $form->{OUT}          = $config->printer->printer_command;
 
 250   $form->{OUT_MODE}     = '|-';
 
 252   $form->prepare_for_printing;
 
 254   $form->throw_on_error(sub {
 
 256       $form->parse_template(\%::myconfig);
 
 258     } || die $EVAL_ERROR->getMessage;
 
 272 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
 
 277 Iterate over all periodic invoice configurations, extend them if
 
 278 applicable, calculate the dates for which invoices have to be posted
 
 279 and post those invoices by converting the order into an invoice for
 
 288 Strings like month names are hardcoded to German in this file.
 
 294 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>