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 = @{ $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->do_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;
 
 258     $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
 
 264     period_start_date => $period_start_date,
 
 266     time_period_vars  => $time_period_vars,
 
 270 sub _calculate_dates {
 
 272   return $config->calculate_invoice_dates(end_date => DateTime->today_local);
 
 275 sub _send_summary_email {
 
 276   my ($self, $posted_invoices, $printed_invoices, $emailed_invoices) = @_;
 
 278   my %config = %::lx_office_conf;
 
 280   return if !$config{periodic_invoices} || !$config{periodic_invoices}->{send_email_to} || !scalar @{ $posted_invoices };
 
 282   my $user  = SL::DB::Manager::AuthUser->find_by(login => $config{periodic_invoices}->{send_email_to});
 
 283   my $email = $user ? $user->get_config_value('email') : undef;
 
 285   return unless $email;
 
 287   my $template = Template->new({ 'INTERPOLATE' => 0,
 
 293   return unless $template;
 
 295   my $email_template = $config{periodic_invoices}->{email_template};
 
 296   my $filename       = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
 
 297   my %params         = ( POSTED_INVOICES  => $posted_invoices,
 
 298                          PRINTED_INVOICES => $printed_invoices,
 
 299                          EMAILED_INVOICES => $emailed_invoices );
 
 302   $template->process($filename, \%params, \$output);
 
 304   my $mail              = Mailer->new;
 
 305   $mail->{from}         = $config{periodic_invoices}->{email_from};
 
 306   $mail->{to}           = $email;
 
 307   $mail->{subject}      = $config{periodic_invoices}->{email_subject};
 
 308   $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
 
 309   $mail->{message}      = $output;
 
 315   my ($self, $data) = @_;
 
 317   my $invoice       = $data->{invoice};
 
 318   my $config        = $data->{config};
 
 320   return unless $config->print && $config->printer_id && $config->printer->printer_command;
 
 322   my $form = Form->new;
 
 323   $invoice->flatten_to_form($form, format_amounts => 1);
 
 325   $form->{printer_code} = $config->printer->template_code;
 
 326   $form->{copies}       = $config->copies;
 
 327   $form->{formname}     = $form->{type};
 
 328   $form->{format}       = 'pdf';
 
 329   $form->{media}        = 'printer';
 
 330   $form->{OUT}          = $config->printer->printer_command;
 
 331   $form->{OUT_MODE}     = '|-';
 
 333   $form->{TEMPLATE_DRIVER_OPTIONS} = {
 
 334     variable_content_types => {
 
 335       longdescription => 'html',
 
 341   $form->prepare_for_printing;
 
 343   $form->throw_on_error(sub {
 
 345       $form->parse_template(\%::myconfig);
 
 348       push @{ $self->{job_errors} }, $EVAL_ERROR->getMessage;
 
 354   my ($self, $data) = @_;
 
 356   $data->{config}->load;
 
 358   return unless $data->{config}->send_email;
 
 365     (split(m{,}, $data->{config}->email_recipient_address),
 
 366      $data->{config}->email_recipient_contact ? ($data->{config}->email_recipient_contact->cp_email) : ());
 
 368   return unless @recipients;
 
 370   my %create_params = (
 
 371     template               => $self->find_template(name => 'invoice'),
 
 372     variables              => Form->new(''),
 
 373     return                 => 'file_name',
 
 374     variable_content_types => {
 
 375       longdescription => 'html',
 
 381   $data->{invoice}->flatten_to_form($create_params{variables}, format_amounts => 1);
 
 382   $create_params{variables}->prepare_for_printing;
 
 387     $pdf_file_name = $self->create_pdf(%create_params);
 
 389     for (qw(email_subject email_body)) {
 
 391         object           => $data->{config},
 
 392         vars             => $data->{time_period_vars},
 
 394         attribute_format => 'text'
 
 398     for my $recipient (@recipients) {
 
 399       my $mail             = Mailer->new;
 
 400       $mail->{from}        = $data->{config}->email_sender || $::lx_office_conf{periodic_invoices}->{email_from};
 
 401       $mail->{to}          = $recipient;
 
 402       $mail->{subject}     = $data->{config}->email_subject;
 
 403       $mail->{message}     = $data->{config}->email_body;
 
 404       $mail->{attachments} = [{
 
 405         filename => $pdf_file_name,
 
 406         name     => sprintf('%s %s.pdf', $::locale->text('Invoice'), $data->{invoice}->invnumber),
 
 409       my $error        = $mail->send;
 
 411       push @{ $self->{job_errors} }, $error if $error;
 
 417     push @{ $self->{job_errors} }, $EVAL_ERROR;
 
 420   unlink $pdf_file_name if $pdf_file_name;
 
 433 SL::BackgroundJob::CleanBackgroundJobHistory - Create periodic
 
 438 Iterate over all periodic invoice configurations, extend them if
 
 439 applicable, calculate the dates for which invoices have to be posted
 
 440 and post those invoices by converting the order into an invoice for
 
 449 Strings like month names are hardcoded to German in this file.
 
 455 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>