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   _print_invoice(@{ $_ }) for @invoices_to_print;
 
  63   _send_email(\@new_invoices, [ map { $_->[0] } @invoices_to_print ]) if @new_invoices;
 
  69   my $message  = join('', 'SL::BackgroundJob::CreatePeriodicInvoices: ', @_);
 
  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_billing_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]) } ],
 
 109   my $sub      = $params{attribute};
 
 110   my $str      = $params{object}->$sub // '';
 
 111   my $sub_fmt  = lc($params{attribute_format} // 'text');
 
 113   my ($start_tag, $end_tag) = $sub_fmt eq 'html' ? ('<%', '%>') : ('<%', '%>');
 
 115   $str =~ s{ ${start_tag} ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? ${end_tag} }{
 
 116     my ($key, $format) = ($1, $3);
 
 117     $key               = $::locale->unquote_special_chars('html', $key) if $sub_fmt eq 'html';
 
 120     if (!$params{vars}->{$key}) {
 
 124       $format    = $::locale->unquote_special_chars('html', $format) if $sub_fmt eq 'html';
 
 126       $new_value = DateTime::Format::Strptime->new(
 
 129         time_zone   => 'local',
 
 130       )->format_datetime($params{vars}->{$key}->[0]);
 
 133       $new_value = $params{vars}->{$1}->[1]->($params{vars}->{$1}->[0]);
 
 136     $new_value = $::locale->quote_special_chars('html', $new_value) if $sub_fmt eq 'html';
 
 142   $params{object}->$sub($str);
 
 145 sub _adjust_sellprices_for_period_lengths {
 
 148   my $billing_len     = $params{config}->get_billing_period_length;
 
 149   my $order_value_len = $params{config}->get_order_value_period_length;
 
 151   return if $billing_len == $order_value_len;
 
 153   my $is_last_invoice_in_cycle = $params{config}->is_last_bill_date_in_order_value_cycle(date => $params{period_start_date});
 
 155   _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");
 
 157   if ($order_value_len < $billing_len) {
 
 158     my $num_orders_per_invoice = $billing_len / $order_value_len;
 
 160     $_->sellprice($_->sellprice * $num_orders_per_invoice) for @{ $params{invoice}->items };
 
 165   my $num_invoices_in_cycle = $order_value_len / $billing_len;
 
 167   foreach my $item (@{ $params{invoice}->items }) {
 
 168     my $sellprice_one_invoice = $::form->round_amount($item->sellprice * $billing_len / $order_value_len, 2);
 
 170     if ($is_last_invoice_in_cycle) {
 
 171       $item->sellprice($item->sellprice - ($num_invoices_in_cycle - 1) * $sellprice_one_invoice);
 
 174       $item->sellprice($sellprice_one_invoice);
 
 179 sub _create_periodic_invoice {
 
 180   $main::lxdebug->enter_sub();
 
 184   my $period_start_date = shift;
 
 186   my $time_period_vars  = _generate_time_period_variables($config, $period_start_date);
 
 188   my $invdate           = DateTime->today_local;
 
 190   my $order   = $config->order;
 
 192   if (!$self->{db_obj}->db->do_transaction(sub {
 
 193     1;                          # make Emacs happy
 
 195     $invoice = SL::DB::Invoice->new_from($order);
 
 197     my $intnotes  = $invoice->intnotes ? $invoice->intnotes . "\n\n" : '';
 
 198     $intnotes    .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung";
 
 200     $invoice->assign_attributes(deliverydate => $period_start_date,
 
 201                                 intnotes     => $intnotes,
 
 202                                 employee     => $order->employee, # new_from sets employee to import user
 
 203                                 direct_debit => $config->direct_debit,
 
 206     _replace_vars(object => $invoice, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'notes' ? 'html' : 'text')) for qw(notes intnotes transaction_description);
 
 208     foreach my $item (@{ $invoice->items }) {
 
 209       _replace_vars(object => $item, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'longdescription' ? 'html' : 'text')) for qw(description longdescription);
 
 212     _adjust_sellprices_for_period_lengths(invoice => $invoice, config => $config, period_start_date => $period_start_date);
 
 214     $invoice->post(ar_id => $config->ar_chart_id) || die;
 
 216     # like $form->add_shipto, but we don't need to check for a manual exception,
 
 217     # because we can already assume this (otherwise no shipto_id from order)
 
 218     if ($order->shipto_id) {
 
 220       my $shipto_oe = SL::DB::Manager::Shipto->find_by(shipto_id => $order->shipto_id);
 
 221       my $shipto_ar = $shipto_oe->clone_and_reset;
 
 223       $shipto_ar->module('AR');            # alter module OE -> AR
 
 224       $shipto_ar->trans_id($invoice->id);  # alter trans_id -> new id from invoice
 
 228     $order->link_to_record($invoice);
 
 230     foreach my $item (@{ $invoice->items }) {
 
 231       foreach (qw(orderitems)) {    # expand if needed (delivery_order_items)
 
 232           if ($item->{"converted_from_${_}_id"}) {
 
 233             die unless $item->{id};
 
 234             RecordLinks->create_links('mode'       => 'ids',
 
 236                                       'from_ids'   => $item->{"converted_from_${_}_id"},
 
 237                                       'to_table'   => 'invoice',
 
 238                                       'to_id'      => $item->{id},
 
 240             delete $item->{"converted_from_${_}_id"};
 
 245     SL::DB::PeriodicInvoice->new(config_id         => $config->id,
 
 246                                  ar_id             => $invoice->id,
 
 247                                  period_start_date => $period_start_date)
 
 250     _log_msg("_create_invoice created for period start date $period_start_date id " . $invoice->id . " number " . $invoice->invnumber . " netamount " . $invoice->netamount . " amount " . $invoice->amount);
 
 252     # die $invoice->transaction_description;
 
 254     $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
 
 257   $main::lxdebug->leave_sub();
 
 261 sub _calculate_dates {
 
 263   return $config->calculate_invoice_dates(end_date => DateTime->today_local);
 
 267   my ($posted_invoices, $printed_invoices) = @_;
 
 269   my %config = %::lx_office_conf;
 
 271   return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
 
 273   my $user  = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
 
 274   my $email = $user ? $user->get_config_value('email') : undef;
 
 276   return unless $email;
 
 278   my $template = Template->new({ 'INTERPOLATE' => 0,
 
 284   return unless $template;
 
 286   my $email_template = $config{periodic_invoices}->{email_template};
 
 287   my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
 
 288   my %params         = ( POSTED_INVOICES  => $posted_invoices,
 
 289                          PRINTED_INVOICES => $printed_invoices );
 
 292   $template->process($filename, \%params, \$output);
 
 294   my $mail              = Mailer->new;
 
 295   $mail->{from}         = $config{periodic_invoices}->{email_from};
 
 296   $mail->{to}           = $email;
 
 297   $mail->{subject}      = $config{periodic_invoices}->{email_subject};
 
 298   $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
 
 299   $mail->{message}      = $output;
 
 305   my ($invoice, $config) = @_;
 
 307   return unless $config->print && $config->printer_id && $config->printer->printer_command;
 
 309   my $form = Form->new;
 
 310   $invoice->flatten_to_form($form, format_amounts => 1);
 
 312   $form->{printer_code} = $config->printer->template_code;
 
 313   $form->{copies}       = $config->copies;
 
 314   $form->{formname}     = $form->{type};
 
 315   $form->{format}       = 'pdf';
 
 316   $form->{media}        = 'printer';
 
 317   $form->{OUT}          = $config->printer->printer_command;
 
 318   $form->{OUT_MODE}     = '|-';
 
 320   $form->{TEMPLATE_DRIVER_OPTIONS} = {
 
 321     variable_content_types => {
 
 322       longdescription => 'html',
 
 328   $form->prepare_for_printing;
 
 330   $form->throw_on_error(sub {
 
 332       $form->parse_template(\%::myconfig);
 
 334     } || die $EVAL_ERROR->getMessage;
 
 348 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
 
 353 Iterate over all periodic invoice configurations, extend them if
 
 354 applicable, calculate the dates for which invoices have to be posted
 
 355 and post those invoices by converting the order into an invoice for
 
 364 Strings like month names are hardcoded to German in this file.
 
 370 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>