Projekt

Allgemein

Profil

Herunterladen (14 KB) Statistiken
| Zweig: | Markierung: | Revision:
250fa402 Moritz Bunkus
package SL::DB::PeriodicInvoicesConfig;

use strict;

use SL::DB::MetaSetup::PeriodicInvoicesConfig;
dac5f3de Tamino Steinert
use SL::DB::Manager::PeriodicInvoicesConfig;
250fa402 Moritz Bunkus
fe13d7f1 Tamino Steinert
use Params::Validate qw(:all);
fde528b6 Moritz Bunkus
use List::Util qw(max min);
3e714a7f Tamino Steinert
use Rose::DB::Object::Helpers qw(clone);
fde528b6 Moritz Bunkus
fe13d7f1 Tamino Steinert
use SL::Helper::DateTime;

2d7e4203 Sven Schöling
__PACKAGE__->meta->initialize;

efeb61e0 Jan Büren
our %PERIOD_LENGTHS = ( o => 0, m => 1, q => 3, b => 6, y => 12 );
82ff5451 Moritz Bunkus
our %ORDER_VALUE_PERIOD_LENGTHS = ( %PERIOD_LENGTHS, 2 => 24, 3 => 36, 4 => 48, 5 => 60 );
our @PERIODICITIES = keys %PERIOD_LENGTHS;
our @ORDER_VALUE_PERIODICITIES = keys %ORDER_VALUE_PERIOD_LENGTHS;
47d35d06 Moritz Bunkus
3e714a7f Tamino Steinert
sub get_open_orders_for_period {
my $self = shift;

my %params = validate(@_, {
start_date => {
callbacks => { is_date => \&_is_date, },
default => $self->start_date,
},
end_date => {
callbacks => { is_date => \&_is_date, },
default => DateTime->today_local,
},
});
return [] unless $self->active;

my @start_dates = $self->calculate_invoice_dates(%params);
return [] unless scalar @start_dates;

my $orig_order = $self->order;

my $next_period_start_date = $self->get_next_period_start_date;

my @orders;
foreach my $period_start_date (@start_dates) {
my $new_order = clone($orig_order);
$new_order->reqdate($period_start_date);
$new_order->tax_point(
add_months(
d24e5589 Tamino Steinert
$period_start_date, $self->get_billing_period_length || $self->get_order_value_period_length || 1
3e714a7f Tamino Steinert
)->add(days => -1)
);
my @items;
for my $item ($orig_order->items) {
93ae5d0f Tamino Steinert
if ($item->periodic_invoice_items_config) {
next if $item->periodic_invoice_items_config->periodicity eq 'n';
next if $item->periodic_invoice_items_config->periodicity eq 'o' && (
$item->periodic_invoice_items_config->once_invoice_id
3e714a7f Tamino Steinert
|| $period_start_date != $next_period_start_date
);
93ae5d0f Tamino Steinert
}
3e714a7f Tamino Steinert
my $new_item = clone($item);

$new_item = $self->_adjust_sellprices_for_period(
order_item => $new_item,
period_start_date => $period_start_date,
);

push @items, $new_item;
}
if (scalar @items) { # don't return empty orders
$new_order->items(@items);
$new_order->calculate_prices_and_taxes;
push @orders, $new_order;
}
}
return \@orders;
}

sub _adjust_sellprices_for_period {
my $self = shift;

my %params = validate(@_, {
period_start_date => { callbacks => { is_date => \&_is_date, } },
order_item => { isa => 'SL::DB::OrderItem' },
});
my $item = $params{order_item};

my $config = $self;

my $billing_len = $config->get_billing_period_length;
my $order_value_len = $config->get_order_value_period_length;

return $item if $billing_len == $order_value_len;
return $item if $billing_len == 0;

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

my $multiplier_per_invoice = $billing_len / $order_value_len;
my $sellprice_one_invoice = $::form->round_amount($item->sellprice * $multiplier_per_invoice, 2);
if ($multiplier_per_invoice < 1 && $is_last_invoice_in_cycle) {
# add rounding difference on last cycle
my $num_invoices_in_cycle = $order_value_len / $billing_len;
$item->sellprice($item->sellprice - ($num_invoices_in_cycle - 1) * $sellprice_one_invoice);
} else {
$item->sellprice($sellprice_one_invoice);
}

return $item;
}

fe13d7f1 Tamino Steinert
sub calculate_invoice_dates {
my $self = shift;

my %params = validate(@_, {
start_date => {
callbacks => { is_date => \&_is_date, },
default => $self->start_date,
},
end_date => {
callbacks => { is_date => \&_is_date, },
default => DateTime->today_local,
},
});

my $start_date = DateTime->from_ymd($params{start_date});
my $end_date = DateTime->from_ymd($params{end_date});

if ($self->end_date
&& ($self->terminated || !$self->extend_automatically_by) ) {
$end_date = min($end_date, $self->end_date);
}

my $last_created_on_date = $self->get_previous_billed_period_start_date;

my @start_dates;
my $first_period_start_date = $self->first_billing_date || $self->start_date;
if ($self->periodicity ne 'o') {
my $billing_period_length = $self->get_billing_period_length;
my $months_first_period =
$first_period_start_date->year * 12 + $first_period_start_date->month;

my $month_to_start = $start_date->year * 12 + $start_date->month - $months_first_period;
$month_to_start += 1
if add_months($first_period_start_date, $month_to_start) < $start_date;

my $month_after_last_created = 0;
if ($last_created_on_date) {
$month_after_last_created =
$last_created_on_date->year * 12 + $last_created_on_date->month - $months_first_period;
$month_after_last_created += 1
if add_months($first_period_start_date, $month_after_last_created) <= $last_created_on_date;
}

my $months_from_period_start = max(
$month_to_start,
$month_after_last_created,
0);

my $period_count = int($months_from_period_start / $billing_period_length); # floor
$period_count += $months_from_period_start % $billing_period_length != 0 ? 1 : 0; # ceil

my $next_period_start_date = add_months($first_period_start_date, $period_count * $billing_period_length);
while ($next_period_start_date <= $end_date) {
push @start_dates, $next_period_start_date;
$period_count++;
$next_period_start_date = add_months($first_period_start_date, $period_count * $billing_period_length);
}
} else { # single
push @start_dates, $first_period_start_date
unless $last_created_on_date
|| $first_period_start_date < $start_date
|| $first_period_start_date > $end_date;
}

return @start_dates;
}

3e714a7f Tamino Steinert
sub get_next_period_start_date {
my $self = shift;

my $last_created_on_date = $self->get_previous_billed_period_start_date;

return $self->first_billing_date || $self->start_date unless $last_created_on_date;

my @dates = $self->calculate_invoice_dates(
end_date => add_months($last_created_on_date, $self->get_billing_period_length)
);

return scalar @dates ? $dates[0] : undef;
}

82ff5451 Moritz Bunkus
sub get_billing_period_length {
47d35d06 Moritz Bunkus
my $self = shift;
d24e5589 Tamino Steinert
return $PERIOD_LENGTHS{ $self->periodicity };
47d35d06 Moritz Bunkus
}

82ff5451 Moritz Bunkus
sub get_order_value_period_length {
my $self = shift;
return $self->get_billing_period_length if $self->order_value_periodicity eq 'p';
d24e5589 Tamino Steinert
return $ORDER_VALUE_PERIOD_LENGTHS{ $self->order_value_periodicity };
82ff5451 Moritz Bunkus
}

fe13d7f1 Tamino Steinert
sub add_months {
my ($date, $months) = @_;

my $start_months_of_date = $date->month;
$date = $date->clone();
my $new_date = $date->clone();
$new_date->add(months => $months);
# stay in month: 31.01 + 1 month should be 28.02 or 29.02 (not 03.03. or 02.03)
while (($start_months_of_date + $months) % 12 != $new_date->month % 12) {
$new_date->add(days => -1);
}

# if date was at end of month -> move new date also to end of month
if ($date->is_last_day_of_month()) {
return DateTime->last_day_of_month(year => $new_date->year, month => $new_date->month)
}

return $new_date
};

sub _is_date {
return !!DateTime->from_ymd($_[0]); # can also be a DateTime object
}

47d35d06 Moritz Bunkus
sub _log_msg {
430216b9 Moritz Bunkus
$::lxdebug->message(LXDebug->DEBUG1(), join('', 'SL::DB::PeriodicInvoicesConfig: ', @_));
47d35d06 Moritz Bunkus
}

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.
fe13d7f1 Tamino Steinert
$end_date = add_months($end_date, $self->extend_automatically_by) while $today > $end_date;
47d35d06 Moritz Bunkus
_log_msg("new end date $end_date\n");

$self->end_date($end_date);
$self->save;

return $end_date;
}

f98064e0 Moritz Bunkus
sub get_previous_billed_period_start_date {
47d35d06 Moritz Bunkus
my $self = shift;

my $query = <<SQL;
f98064e0 Moritz Bunkus
SELECT MAX(period_start_date)
47d35d06 Moritz Bunkus
FROM periodic_invoices
f98064e0 Moritz Bunkus
WHERE config_id = ?
47d35d06 Moritz Bunkus
SQL

f98064e0 Moritz Bunkus
my ($date) = $self->dbh->selectrow_array($query, undef, $self->id);
47d35d06 Moritz Bunkus
f98064e0 Moritz Bunkus
return undef unless $date;
return ref $date ? $date : $self->db->parse_date($date);
47d35d06 Moritz Bunkus
}

430216b9 Moritz Bunkus
sub is_last_bill_date_in_order_value_cycle {
fe13d7f1 Tamino Steinert
my $self = shift;

my %params = validate(@_, {
date => { callbacks => { is_date => \&_is_date, } },
});
430216b9 Moritz Bunkus
my $months_billing = $self->get_billing_period_length;
my $months_order_value = $self->get_order_value_period_length;

return 1 if $months_billing >= $months_order_value;

fe13d7f1 Tamino Steinert
my $billing_date = DateTime->from_ymd($params{date});
my $first_date = $self->first_billing_date || $self->start_date;
430216b9 Moritz Bunkus
fe13d7f1 Tamino Steinert
return (12 * ($billing_date->year - $first_date->year) + $billing_date->month + $months_billing) % $months_order_value
== $first_date->month % $months_order_value;
430216b9 Moritz Bunkus
}

efeb61e0 Jan Büren
sub disable_one_time_config {
my $self = shift;

_log_msg("check one time for " . $self->id . "\n");

# A periodicity of one time was set. Deactivate this config now.
if ($self->periodicity eq 'o') {
_log_msg("setting inactive\n");
04479c02 Jan Büren
if (!$self->db->with_transaction(sub {
1; # make Emacs happy
$self->active(0);
$self->order->update_attributes(closed => 1);
$self->save;
1;
})) {
$::lxdebug->message(LXDebug->WARN(), "disalbe_one_time config failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
return undef;
}
efeb61e0 Jan Büren
return $self->order->ordnumber;
}
return undef;
}
250fa402 Moritz Bunkus
1;
82ff5451 Moritz Bunkus
__END__

=pod

=encoding utf8

=head1 NAME

SL::DB::PeriodicInvoicesConfig - DB model for the configuration for periodic invoices

=head1 FUNCTIONS

=over 4

=item C<calculate_invoice_dates %params>

Calculates dates for which invoices will have to be created. Returns a
list of L<DateTime> objects.

This function looks at the configuration settings and at the list of
invoices that have already been created for this configuration. The
date range for which dates are created are controlled by several
values:

=over 2

=item * The properties C<first_billing_date> and C<start_date>
determine the start date.

=item * The properties C<end_date> and C<terminated> determine the end
date.

=item * The optional parameter C<past_dates> determines whether or not
dates for which invoices have already been created will be included in
the list. The default is not to include them.

=item * The optional parameters C<start_date> and C<end_date> override
the start and end dates from the configuration.

=item * If no end date is set or implied via the configuration and no
C<end_date> parameter is given then the function will use 100 years
in the future as the end date.

=back

=item C<get_billing_period_length>

Returns the number of months corresponding to the billing
periodicity. This means that a new invoice has to be created every x
months starting with the value in C<first_billing_date> (or
C<start_date> if C<first_billing_date> is unset).

=item C<get_order_value_period_length>

Returns the number of months the order's value refers to. This looks
at the C<order_value_periodicity>.

Each invoice's value is calculated as C<order value *
billing_period_length / order_value_period_length>.

=item C<get_previous_billed_period_start_date>

Returns the highest date (as an instance of L<DateTime>) for which an
invoice has been created from this configuration.

=item C<handle_automatic_extension>

Configurations which haven't been terminated and which have an end
date set may be eligible for automatic extension by a certain number
of months. This what the function implements.

If the configuration is not eligible or if the C<end_date> hasn't been
reached yet then nothing is done and C<undef> is returned. Otherwise
its behavior is determined by the C<extend_automatically_by> property.

If the property C<extend_automatically_by> is not 0 then the
C<end_date> will be extended by C<extend_automatically_by> months, and
the configuration will be saved. In this case the new end date will be
returned.

Otherwise (if C<extend_automatically_by> is 0) the property C<active>
will be set to 1, and the configuration will be saved. In this case
C<undef> will be returned.

430216b9 Moritz Bunkus
=item C<is_last_billing_date_in_order_value_cycle %params>

Determines whether or not the mandatory parameter C<date>, an instance
of L<DateTime>, is the last billing date within the cycle given by the
order value periodicity. Returns a truish value if this is the case
and a falsish value otherwise.

This check is always true if the billing periodicity is longer than or
equal to the order value periodicity. For example, if you have an
order whose value is given for three months and you bill every six
months and you have twice the order value on each invoice, meaning
each invoice is itself the last invoice for not only one but two order
value cycles.

Otherwise (if the order value periodicity is longer than the billing
periodicity) this function iterates over all eligible dates starting
with C<first_billing_date> (or C<start_date> if C<first_billing_date>
is unset) and adding the order value length with each step. If the
date given by the C<date> parameter plus the billing period length
equals one of those dates then the given date is indeed the date of
the last invoice in that particular order value cycle.

efeb61e0 Jan Büren
=item C<sub disable_one_time_config>

Sets the state of the periodic_invoices_configs to inactive
62533640 Jan Büren
(active => false) and closes the source order (closed => true)
if the periodicity is <Co> (one time).

efeb61e0 Jan Büren
Returns undef if the periodicity is not 'one time' otherwise the
order number of the deactivated periodic order.
a76881ea Bernd Bleßmann
82ff5451 Moritz Bunkus
=back

=head1 BUGS

Nothing here yet.

=head1 AUTHOR

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

=cut