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>