--- /dev/null
+package SL::BackgroundJob::CreatePeriodicInvoices;
+
+use strict;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use Config::Std;
+use English qw(-no_match_vars);
+
+use SL::DB::AuthUser;
+use SL::DB::Order;
+use SL::DB::Invoice;
+use SL::DB::PeriodicInvoice;
+use SL::DB::PeriodicInvoicesConfig;
+use SL::Mailer;
+
+sub create_job {
+ $_[0]->create_standard_job('0 3 1 * *'); # first day of month at 3:00 am
+}
+
+sub run {
+ my $self = shift;
+ $self->{db_obj} = shift;
+
+ my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(where => [ active => 1 ]);
+
+ foreach my $config (@{ $configs }) {
+ my $new_end_date = $config->handle_automatic_extension;
+ _log_msg("Periodic invoice configuration ID " . $config->id . " extended through " . $new_end_date->strftime('%d.%m.%Y') . "\n") if $new_end_date;
+ }
+
+ my (@new_invoices, @invoices_to_print);
+
+ _log_msg("Number of configs: " . scalar(@{ $configs}));
+
+ foreach my $config (@{ $configs }) {
+ # A configuration can be set to inactive by
+ # $config->handle_automatic_extension. Therefore the check in
+ # ...->get_all() does not suffice.
+ _log_msg("Config " . $config->id . " active " . $config->active);
+ next unless $config->active;
+
+ my @dates = _calculate_dates($config);
+
+ _log_msg("Dates: " . join(' ', map { $_->to_lxoffice } @dates));
+
+ foreach my $date (@dates) {
+ my $invoice = $self->_create_periodic_invoice($config, $date);
+ next unless $invoice;
+
+ _log_msg("Invoice " . $invoice->invnumber . " posted for config ID " . $config->id . ", period start date " . $::locale->format_date(\%::myconfig, $date) . "\n");
+ push @new_invoices, $invoice;
+ push @invoices_to_print, $invoice if $config->print;
+
+ # last;
+ }
+ }
+
+ map { _print_invoice($_) } @invoices_to_print;
+
+ _send_email(\@new_invoices, \@invoices_to_print) if @new_invoices;
+
+ return 1;
+}
+
+sub _log_msg {
+ # my $message = join('', @_);
+ # $message .= "\n" unless $message =~ m/\n$/;
+ # $::lxdebug->message(0, $message);
+}
+
+sub _generate_time_period_variables {
+ my $config = shift;
+ my $period_start_date = shift;
+ my $period_end_date = $period_start_date->clone->truncate(to => 'month')->add(months => $config->get_period_length)->subtract(days => 1);
+
+ my @month_names = ('',
+ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
+ 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember');
+
+ my $vars = { current_quarter => $period_start_date->quarter,
+ previous_quarter => $period_start_date->clone->subtract(months => 3)->quarter,
+ next_quarter => $period_start_date->clone->add( months => 3)->quarter,
+
+ current_month => $period_start_date->month,
+ previous_month => $period_start_date->clone->subtract(months => 1)->month,
+ next_month => $period_start_date->clone->add( months => 1)->month,
+
+ current_year => $period_start_date->year,
+ previous_year => $period_start_date->year - 1,
+ next_year => $period_start_date->year + 1,
+
+ period_start_date => $::locale->format_date(\%::myconfig, $period_start_date),
+ period_end_date => $::locale->format_date(\%::myconfig, $period_end_date),
+ };
+
+ map { $vars->{"${_}_month_long"} = $month_names[ $vars->{"${_}_month"} ] } qw(current previous next);
+
+ return $vars;
+}
+
+sub _replace_vars {
+ my $object = shift;
+ my $vars = shift;
+ my $sub = shift;
+ my $str = $object->$sub;
+
+ my ($key, $value);
+ $str =~ s|<\%${key}\%>|$value|g while ($key, $value) = each %{ $vars };
+ $object->$sub($str);
+}
+
+sub _create_periodic_invoice {
+ my $self = shift;
+ my $config = shift;
+ my $period_start_date = shift;
+
+ my $time_period_vars = _generate_time_period_variables($config, $period_start_date);
+
+ my $invdate = DateTime->today_local;
+
+ my $order = $config->order;
+ my $invoice;
+ if (!$self->{db_obj}->db->do_transaction(sub {
+ 1; # make Emacs happy
+
+ $invoice = SL::DB::Invoice->new_from($order);
+
+ my $intnotes = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
+ $intnotes .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
+
+ $invoice->assign_attributes(deliverydate => $period_start_date,
+ intnotes => $intnotes,
+ );
+
+ map { _replace_vars($invoice, $time_period_vars, $_) } qw(notes intnotes transaction_description);
+
+ foreach my $item (@{ $invoice->items }) {
+ map { _replace_vars($item, $time_period_vars, $_) } qw(description longdescription);
+ }
+
+ $invoice->post(ar_id => $config->ar_chart_id) || die;
+
+ $order->link_to_record($invoice);
+
+ SL::DB::PeriodicInvoice->new(config_id => $config->id,
+ ar_id => $invoice->id,
+ period_start_date => $period_start_date)
+ ->save;
+
+ # die $invoice->transaction_description;
+ })) {
+ $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
+ return undef;
+ }
+
+ return $invoice;
+}
+
+sub _calculate_dates {
+ my $config = shift;
+
+ my $cur_date = $config->start_date;
+ my $start_date = $config->get_previous_invoice_date || DateTime->new(year => 1970, month => 1, day => 1);
+ my $end_date = $config->end_date || DateTime->new(year => 2100, month => 1, day => 1);
+ my $tomorrow = DateTime->today_local->add(days => 1);
+ my $period_len = $config->get_period_length;
+
+ $end_date = $tomorrow if $end_date > $tomorrow;
+
+ my @dates;
+
+ while (1) {
+ last if $cur_date >= $end_date;
+
+ push @dates, $cur_date->clone if $cur_date > $start_date;
+
+ $cur_date->add(months => $period_len);
+ }
+
+ return @dates;
+}
+
+sub _send_email {
+ my ($posted_invoices, $printed_invoices) = @_;
+
+ read_config 'config/periodic_invoices.conf' => my %config;
+
+ return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
+
+ my $user = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
+ my $email = $user ? $user->get_config_value('email') : undef;
+
+ return unless $email;
+
+ my $template = Template->new({ 'INTERPOLATE' => 0,
+ 'EVAL_PERL' => 0,
+ 'ABSOLUTE' => 1,
+ 'CACHE_SIZE' => 0,
+ });
+
+ return unless $template;
+
+ my $email_template = $config{periodic_invoices}->{email_template};
+ my $filename = $email_template || ( ($user->get_config_value('templates') || "templates/webpages") . "/periodic_invoices_email.txt" );
+ my %params = ( POSTED_INVOICES => $posted_invoices,
+ PRINTED_INVOICES => $printed_invoices );
+
+ my $output;
+ $template->process($filename, \%params, \$output);
+
+ my $mail = Mailer->new;
+ $mail->{from} = $config{periodic_invoices}->{email_from};
+ $mail->{to} = $email;
+ $mail->{subject} = $config{periodic_invoices}->{email_subject};
+ $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
+ $mail->{message} = $output;
+
+ $mail->send;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
+invoices for orders
+
+=head1 SYNOPSIS
+
+Iterate over all periodic invoice configurations, extend them if
+applicable, calculate the dates for which invoices have to be posted
+and post those invoices by converting the order into an invoice for
+each date.
+
+=head1 TOTO
+
+=over 4
+
+=item *
+
+Strings like month names are hardcoded to German in this file.
+
+=item *
+
+Implement printing the invoices if requested.
+
+=back
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
use strict;
+use Readonly;
+
use SL::DB::MetaSetup::PeriodicInvoicesConfig;
__PACKAGE__->meta->add_relationships(
# Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
__PACKAGE__->meta->make_manager_class;
+Readonly our @PERIODICITIES => qw(m q f b y);
+Readonly our %PERIOD_LENGTHS => ( m => 1, q => 3, f => 4, b => 6, y => 12 );
+
+sub get_period_length {
+ my $self = shift;
+ return $PERIOD_LENGTHS{ $self->periodicity } || 1;
+}
+
+sub _log_msg {
+ $::lxdebug->message(LXDebug->DEBUG1(), join('', @_));
+}
+
+sub handle_automatic_extension {
+ my $self = shift;
+
+ _log_msg("HAE for " . $self->id . "\n");
+ # Don't extend configs that have been terminated. There's nothing to
+ # extend if there's no end date.
+ return if $self->terminated || !$self->end_date;
+
+ my $today = DateTime->now_local;
+ my $end_date = $self->end_date;
+
+ _log_msg("today $today end_date $end_date\n");
+
+ # The end date has not been reached yet, therefore no extension is
+ # needed.
+ return if $today <= $end_date;
+
+ # The end date has been reached. If no automatic extension has been
+ # set then terminate the config and return.
+ if (!$self->extend_automatically_by) {
+ _log_msg("setting inactive\n");
+ $self->active(0);
+ $self->save;
+ return;
+ }
+
+ # Add the automatic extension period to the new end date as long as
+ # the new end date is in the past. Then save it and get out.
+ $end_date->add(months => $self->extend_automatically_by) while $today > $end_date;
+ _log_msg("new end date $end_date\n");
+
+ $self->end_date($end_date);
+ $self->save;
+
+ return $end_date;
+}
+
+sub get_previous_invoice_date {
+ my $self = shift;
+
+ my $query = <<SQL;
+ SELECT MAX(ar.transdate)
+ FROM periodic_invoices
+ LEFT JOIN ar ON (ar.id = periodic_invoices.ar_id)
+ WHERE periodic_invoices.config_id = ?
+SQL
+
+ my ($max_transdate) = $self->dbh->selectrow_array($query, undef, $self->id);
+
+ return undef unless $max_transdate;
+ return ref $max_transdate ? $max_transdate : $self->db->parse_date($max_transdate);
+}
+
1;