Projekt

Allgemein

Profil

Herunterladen (13,1 KB) Statistiken
| Zweig: | Markierung: | Revision:
47d35d06 Moritz Bunkus
package SL::BackgroundJob::CreatePeriodicInvoices;

use strict;

use parent qw(SL::BackgroundJob::Base);

use Config::Std;
1ad7929f Moritz Bunkus
use DateTime::Format::Strptime;
47d35d06 Moritz Bunkus
use English qw(-no_match_vars);

use SL::DB::AuthUser;
2e66dde5 Moritz Bunkus
use SL::DB::Default;
47d35d06 Moritz Bunkus
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;

33b0b2ca Moritz Bunkus
my $configs = SL::DB::Manager::PeriodicInvoicesConfig->get_all(query => [ active => 1 ]);
47d35d06 Moritz Bunkus
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;
4791c790 Moritz Bunkus
push @invoices_to_print, [ $invoice, $config ] if $config->print;
47d35d06 Moritz Bunkus
# last;
}
}

5142ae5c Moritz Bunkus
_print_invoice(@{ $_ }) for @invoices_to_print;
47d35d06 Moritz Bunkus
4791c790 Moritz Bunkus
_send_email(\@new_invoices, [ map { $_->[0] } @invoices_to_print ]) if @new_invoices;
47d35d06 Moritz Bunkus
return 1;
}

sub _log_msg {
430216b9 Moritz Bunkus
my $message = join('', 'SL::BackgroundJob::CreatePeriodicInvoices: ', @_);
bffdf9b7 Moritz Bunkus
$message .= "\n" unless $message =~ m/\n$/;
$::lxdebug->message(LXDebug::DEBUG1(), $message);
47d35d06 Moritz Bunkus
}

sub _generate_time_period_variables {
my $config = shift;
my $period_start_date = shift;
82ff5451 Moritz Bunkus
my $period_end_date = $period_start_date->clone->truncate(to => 'month')->add(months => $config->get_billing_period_length)->subtract(days => 1);
47d35d06 Moritz Bunkus
my @month_names = ('',
4eda5c73 Moritz Bunkus
$::locale->text('January'), $::locale->text('February'), $::locale->text('March'), $::locale->text('April'), $::locale->text('May'), $::locale->text('June'),
$::locale->text('July'), $::locale->text('August'), $::locale->text('September'), $::locale->text('October'), $::locale->text('November'), $::locale->text('December'));
47d35d06 Moritz Bunkus
1ad7929f Moritz Bunkus
my $vars = {
current_quarter => [ $period_start_date->clone->truncate(to => 'month'), sub { $_[0]->quarter } ],
previous_quarter => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 3), sub { $_[0]->quarter } ],
next_quarter => [ $period_start_date->clone->truncate(to => 'month')->add( months => 3), sub { $_[0]->quarter } ],
47d35d06 Moritz Bunkus
1ad7929f Moritz Bunkus
current_month => [ $period_start_date->clone->truncate(to => 'month'), sub { $_[0]->month } ],
previous_month => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $_[0]->month } ],
next_month => [ $period_start_date->clone->truncate(to => 'month')->add( months => 1), sub { $_[0]->month } ],
47d35d06 Moritz Bunkus
f171e7ac Moritz Bunkus
current_month_long => [ $period_start_date->clone->truncate(to => 'month'), sub { $month_names[ $_[0]->month ] } ],
previous_month_long => [ $period_start_date->clone->truncate(to => 'month')->subtract(months => 1), sub { $month_names[ $_[0]->month ] } ],
next_month_long => [ $period_start_date->clone->truncate(to => 'month')->add( months => 1), sub { $month_names[ $_[0]->month ] } ],

1ad7929f Moritz Bunkus
current_year => [ $period_start_date->clone->truncate(to => 'year'), sub { $_[0]->year } ],
previous_year => [ $period_start_date->clone->truncate(to => 'year')->subtract(years => 1), sub { $_[0]->year } ],
next_year => [ $period_start_date->clone->truncate(to => 'year')->add( years => 1), sub { $_[0]->year } ],
47d35d06 Moritz Bunkus
1ad7929f Moritz Bunkus
period_start_date => [ $period_start_date->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
bdb802cd Moritz Bunkus
period_end_date => [ $period_end_date, sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
1ad7929f Moritz Bunkus
};
47d35d06 Moritz Bunkus
return $vars;
}

sub _replace_vars {
111861a5 Moritz Bunkus
my (%params) = @_;
my $sub = $params{attribute};
my $str = $params{object}->$sub;
8abdaf40 Moritz Bunkus
my $sub_fmt = lc($params{attribute_format} // 'text');
47d35d06 Moritz Bunkus
8abdaf40 Moritz Bunkus
my ($start_tag, $end_tag) = $sub_fmt eq 'html' ? ('&lt;%', '%&gt;') : ('<%', '%>');

$str =~ s{ ${start_tag} ([a-z0-9_]+) ( \s+ format \s*=\s* (.*?) \s* )? ${end_tag} }{
1ad7929f Moritz Bunkus
my ($key, $format) = ($1, $3);
8abdaf40 Moritz Bunkus
$key = $::locale->unquote_special_chars('html', $key) if $sub_fmt eq 'html';
my $new_value;

111861a5 Moritz Bunkus
if (!$params{vars}->{$key}) {
8abdaf40 Moritz Bunkus
$new_value = '';
1ad7929f Moritz Bunkus
} elsif ($format) {
8abdaf40 Moritz Bunkus
$format = $::locale->unquote_special_chars('html', $format) if $sub_fmt eq 'html';

$new_value = DateTime::Format::Strptime->new(
1ad7929f Moritz Bunkus
pattern => $format,
locale => 'de_DE',
time_zone => 'local',
111861a5 Moritz Bunkus
)->format_datetime($params{vars}->{$key}->[0]);
1ad7929f Moritz Bunkus
} else {
8abdaf40 Moritz Bunkus
$new_value = $params{vars}->{$1}->[1]->($params{vars}->{$1}->[0]);
1ad7929f Moritz Bunkus
}
8abdaf40 Moritz Bunkus
$new_value = $::locale->quote_special_chars('html', $new_value) if $sub_fmt eq 'html';

$new_value;

1ad7929f Moritz Bunkus
}eigx;

111861a5 Moritz Bunkus
$params{object}->$sub($str);
47d35d06 Moritz Bunkus
}

430216b9 Moritz Bunkus
sub _adjust_sellprices_for_period_lengths {
my (%params) = @_;

my $billing_len = $params{config}->get_billing_period_length;
my $order_value_len = $params{config}->get_order_value_period_length;

return if $billing_len == $order_value_len;

my $is_last_invoice_in_cycle = $params{config}->is_last_bill_date_in_order_value_cycle(date => $params{period_start_date});

_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");

if ($order_value_len < $billing_len) {
my $num_orders_per_invoice = $billing_len / $order_value_len;

$_->sellprice($_->sellprice * $num_orders_per_invoice) for @{ $params{invoice}->items };

return;
}

my $num_invoices_in_cycle = $order_value_len / $billing_len;

foreach my $item (@{ $params{invoice}->items }) {
my $sellprice_one_invoice = $::form->round_amount($item->sellprice * $billing_len / $order_value_len, 2);

if ($is_last_invoice_in_cycle) {
$item->sellprice($item->sellprice - ($num_invoices_in_cycle - 1) * $sellprice_one_invoice);

} else {
$item->sellprice($sellprice_one_invoice);
}
}
}

47d35d06 Moritz Bunkus
sub _create_periodic_invoice {
34420ddb Jan Büren
$main::lxdebug->enter_sub();

47d35d06 Moritz Bunkus
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,
560d7292 Sven Schöling
employee => $order->employee, # new_from sets employee to import user
47d35d06 Moritz Bunkus
);

326fa24a Moritz Bunkus
_replace_vars(object => $invoice, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'notes' ? 'html' : 'text')) for qw(notes intnotes transaction_description);
47d35d06 Moritz Bunkus
foreach my $item (@{ $invoice->items }) {
8abdaf40 Moritz Bunkus
_replace_vars(object => $item, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'longdescription' ? 'html' : 'text')) for qw(description longdescription);
47d35d06 Moritz Bunkus
}

430216b9 Moritz Bunkus
_adjust_sellprices_for_period_lengths(invoice => $invoice, config => $config, period_start_date => $period_start_date);

47d35d06 Moritz Bunkus
$invoice->post(ar_id => $config->ar_chart_id) || die;

f46afb13 Jan Büren
# like $form->add_shipto, but we don't need to check for a manual exception,
# because we can already assume this (otherwise no shipto_id from order)
if ($order->shipto_id) {

my $shipto_oe = SL::DB::Manager::Shipto->find_by(shipto_id => $order->shipto_id);
my $shipto_ar = $shipto_oe->clone_and_reset;

$shipto_ar->module('AR'); # alter module OE -> AR
$shipto_ar->trans_id($invoice->id); # alter trans_id -> new id from invoice
$shipto_ar->save;
}

47d35d06 Moritz Bunkus
$order->link_to_record($invoice);

34420ddb Jan Büren
foreach my $item (@{ $invoice->items }) {
foreach (qw(orderitems)) { # expand if needed (delivery_order_items)
if ($item->{"converted_from_${_}_id"}) {
die unless $item->{id};
RecordLinks->create_links('mode' => 'ids',
'from_table' => $_,
'from_ids' => $item->{"converted_from_${_}_id"},
'to_table' => 'invoice',
'to_id' => $item->{id},
) || die;
delete $item->{"converted_from_${_}_id"};
}
}
}

47d35d06 Moritz Bunkus
SL::DB::PeriodicInvoice->new(config_id => $config->id,
ar_id => $invoice->id,
period_start_date => $period_start_date)
->save;

430216b9 Moritz Bunkus
_log_msg("_create_invoice created for period start date $period_start_date id " . $invoice->id . " number " . $invoice->invnumber . " netamount " . $invoice->netamount . " amount " . $invoice->amount);

47d35d06 Moritz Bunkus
# die $invoice->transaction_description;
})) {
$::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
return undef;
}
34420ddb Jan Büren
$main::lxdebug->leave_sub();
47d35d06 Moritz Bunkus
return $invoice;
}

sub _calculate_dates {
fde528b6 Moritz Bunkus
my ($config) = @_;
2a0a0b32 Sven Schöling
return $config->calculate_invoice_dates(end_date => DateTime->today_local);
47d35d06 Moritz Bunkus
}

sub _send_email {
my ($posted_invoices, $printed_invoices) = @_;

67b21d42 Moritz Bunkus
my %config = %::lx_office_conf;
47d35d06 Moritz Bunkus
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};
52fd3269 Sven Schöling
my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
47d35d06 Moritz Bunkus
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;
}

4791c790 Moritz Bunkus
sub _print_invoice {
my ($invoice, $config) = @_;

return unless $config->print && $config->printer_id && $config->printer->printer_command;

my $form = Form->new;
$invoice->flatten_to_form($form, format_amounts => 1);

$form->{printer_code} = $config->printer->template_code;
$form->{copies} = $config->copies;
$form->{formname} = $form->{type};
$form->{format} = 'pdf';
$form->{media} = 'printer';
c89e1237 Bernd Bleßmann
$form->{OUT} = $config->printer->printer_command;
$form->{OUT_MODE} = '|-';
4791c790 Moritz Bunkus
$form->prepare_for_printing;

$form->throw_on_error(sub {
eval {
8cd05ad6 Moritz Bunkus
$form->parse_template(\%::myconfig);
4791c790 Moritz Bunkus
1;
27ffa16a Moritz Bunkus
} || die $EVAL_ERROR->getMessage;
4791c790 Moritz Bunkus
});
}

47d35d06 Moritz Bunkus
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.

=back

=head1 AUTHOR

Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>

=cut