Implementiert #357 Auftrag schliessen, falls einmalig wiederkehrende Rechnung inaktiv
[kivitendo-erp.git] / SL / DB / PeriodicInvoicesConfig.pm
1 package SL::DB::PeriodicInvoicesConfig;
2
3 use strict;
4
5 use SL::DB::MetaSetup::PeriodicInvoicesConfig;
6
7 use List::Util qw(max min);
8
9 __PACKAGE__->meta->initialize;
10
11 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
12 __PACKAGE__->meta->make_manager_class;
13
14 our %PERIOD_LENGTHS             = ( o => 0, m => 1, q => 3, b => 6, y => 12 );
15 our %ORDER_VALUE_PERIOD_LENGTHS = ( %PERIOD_LENGTHS, 2 => 24, 3 => 36, 4 => 48, 5 => 60 );
16 our @PERIODICITIES              = keys %PERIOD_LENGTHS;
17 our @ORDER_VALUE_PERIODICITIES  = keys %ORDER_VALUE_PERIOD_LENGTHS;
18
19 sub get_billing_period_length {
20   my $self = shift;
21   return $PERIOD_LENGTHS{ $self->periodicity } || 1;
22 }
23
24 sub get_order_value_period_length {
25   my $self = shift;
26   return $self->get_billing_period_length if $self->order_value_periodicity eq 'p';
27   return $ORDER_VALUE_PERIOD_LENGTHS{ $self->order_value_periodicity } || 1;
28 }
29
30 sub _log_msg {
31   $::lxdebug->message(LXDebug->DEBUG1(), join('', 'SL::DB::PeriodicInvoicesConfig: ', @_));
32 }
33
34 sub handle_automatic_extension {
35   my $self = shift;
36
37   _log_msg("HAE for " . $self->id . "\n");
38   # Don't extend configs that have been terminated. There's nothing to
39   # extend if there's no end date.
40   return if $self->terminated || !$self->end_date;
41
42   my $today    = DateTime->now_local;
43   my $end_date = $self->end_date;
44
45   _log_msg("today $today end_date $end_date\n");
46
47   # The end date has not been reached yet, therefore no extension is
48   # needed.
49   return if $today <= $end_date;
50
51   # The end date has been reached. If no automatic extension has been
52   # set then terminate the config and return.
53   if (!$self->extend_automatically_by) {
54     _log_msg("setting inactive\n");
55     $self->active(0);
56     $self->save;
57     return;
58   }
59
60   # Add the automatic extension period to the new end date as long as
61   # the new end date is in the past. Then save it and get out.
62   $end_date->add(months => $self->extend_automatically_by) while $today > $end_date;
63   _log_msg("new end date $end_date\n");
64
65   $self->end_date($end_date);
66   $self->save;
67
68   return $end_date;
69 }
70
71 sub get_previous_billed_period_start_date {
72   my $self  = shift;
73
74   my $query = <<SQL;
75     SELECT MAX(period_start_date)
76     FROM periodic_invoices
77     WHERE config_id = ?
78 SQL
79
80   my ($date) = $self->dbh->selectrow_array($query, undef, $self->id);
81
82   return undef unless $date;
83   return ref $date ? $date : $self->db->parse_date($date);
84 }
85
86 sub calculate_invoice_dates {
87   my ($self, %params) = @_;
88
89   my $period_len = $self->get_billing_period_length;
90   my $cur_date   = ($self->first_billing_date || $self->start_date)->clone;
91   my $end_date   = $self->terminated ? $self->end_date : undef;
92   $end_date    //= DateTime->today_local->add(years => 100);
93   my $start_date = $params{past_dates} ? undef                              : $self->get_previous_billed_period_start_date;
94   $start_date    = $start_date         ? $start_date->clone->add(days => 1) : $cur_date->clone;
95
96   $start_date    = max($start_date, $params{start_date}) if $params{start_date};
97   $end_date      = min($end_date,   $params{end_date})   if $params{end_date};
98
99   if ($self->periodicity eq 'o') {
100     return ($cur_date >= $start_date) && ($cur_date <= $end_date) ? ($cur_date) : ();
101   }
102
103   my @dates;
104
105   while ($cur_date <= $end_date) {
106     push @dates, $cur_date->clone if $cur_date >= $start_date;
107
108     $cur_date->add(months => $period_len);
109   }
110
111   return @dates;
112 }
113
114 sub is_last_bill_date_in_order_value_cycle {
115   my ($self, %params)    = @_;
116
117   my $months_billing     = $self->get_billing_period_length;
118   my $months_order_value = $self->get_order_value_period_length;
119
120   return 1 if $months_billing >= $months_order_value;
121
122   my $next_billing_date = $params{date}->clone->add(months => $months_billing);
123   my $date_itr          = max($self->start_date, $self->first_billing_date || $self->start_date)->clone;
124
125   _log_msg("is_last_billing_date_in_order_value_cycle start: id " . $self->id . " date_itr $date_itr start " . $self->start_date);
126
127   $date_itr->add(months => $months_order_value) while $date_itr < $next_billing_date;
128
129   _log_msg("is_last_billing_date_in_order_value_cycle end: refdate $params{date} next_billing_date $next_billing_date date_itr $date_itr months_billing $months_billing months_order_value $months_order_value result "
130            . ($date_itr == $next_billing_date));
131
132   return $date_itr == $next_billing_date;
133 }
134
135 sub disable_one_time_config {
136   my $self = shift;
137
138   _log_msg("check one time for " . $self->id . "\n");
139
140   # A periodicity of one time was set. Deactivate this config now.
141   if ($self->periodicity eq 'o') {
142     _log_msg("setting inactive\n");
143     $self->active(0);
144     $self->order->update_attributes(closed => 1);
145     $self->save;
146     return $self->order->ordnumber;
147   }
148   return undef;
149 }
150 1;
151 __END__
152
153 =pod
154
155 =encoding utf8
156
157 =head1 NAME
158
159 SL::DB::PeriodicInvoicesConfig - DB model for the configuration for periodic invoices
160
161 =head1 FUNCTIONS
162
163 =over 4
164
165 =item C<calculate_invoice_dates %params>
166
167 Calculates dates for which invoices will have to be created. Returns a
168 list of L<DateTime> objects.
169
170 This function looks at the configuration settings and at the list of
171 invoices that have already been created for this configuration. The
172 date range for which dates are created are controlled by several
173 values:
174
175 =over 2
176
177 =item * The properties C<first_billing_date> and C<start_date>
178 determine the start date.
179
180 =item * The properties C<end_date> and C<terminated> determine the end
181 date.
182
183 =item * The optional parameter C<past_dates> determines whether or not
184 dates for which invoices have already been created will be included in
185 the list. The default is not to include them.
186
187 =item * The optional parameters C<start_date> and C<end_date> override
188 the start and end dates from the configuration.
189
190 =item * If no end date is set or implied via the configuration and no
191 C<end_date> parameter is given then the function will use 100 years
192 in the future as the end date.
193
194 =back
195
196 =item C<get_billing_period_length>
197
198 Returns the number of months corresponding to the billing
199 periodicity. This means that a new invoice has to be created every x
200 months starting with the value in C<first_billing_date> (or
201 C<start_date> if C<first_billing_date> is unset).
202
203 =item C<get_order_value_period_length>
204
205 Returns the number of months the order's value refers to. This looks
206 at the C<order_value_periodicity>.
207
208 Each invoice's value is calculated as C<order value *
209 billing_period_length / order_value_period_length>.
210
211 =item C<get_previous_billed_period_start_date>
212
213 Returns the highest date (as an instance of L<DateTime>) for which an
214 invoice has been created from this configuration.
215
216 =item C<handle_automatic_extension>
217
218 Configurations which haven't been terminated and which have an end
219 date set may be eligible for automatic extension by a certain number
220 of months. This what the function implements.
221
222 If the configuration is not eligible or if the C<end_date> hasn't been
223 reached yet then nothing is done and C<undef> is returned. Otherwise
224 its behavior is determined by the C<extend_automatically_by> property.
225
226 If the property C<extend_automatically_by> is not 0 then the
227 C<end_date> will be extended by C<extend_automatically_by> months, and
228 the configuration will be saved. In this case the new end date will be
229 returned.
230
231 Otherwise (if C<extend_automatically_by> is 0) the property C<active>
232 will be set to 1, and the configuration will be saved. In this case
233 C<undef> will be returned.
234
235 =item C<is_last_billing_date_in_order_value_cycle %params>
236
237 Determines whether or not the mandatory parameter C<date>, an instance
238 of L<DateTime>, is the last billing date within the cycle given by the
239 order value periodicity. Returns a truish value if this is the case
240 and a falsish value otherwise.
241
242 This check is always true if the billing periodicity is longer than or
243 equal to the order value periodicity. For example, if you have an
244 order whose value is given for three months and you bill every six
245 months and you have twice the order value on each invoice, meaning
246 each invoice is itself the last invoice for not only one but two order
247 value cycles.
248
249 Otherwise (if the order value periodicity is longer than the billing
250 periodicity) this function iterates over all eligible dates starting
251 with C<first_billing_date> (or C<start_date> if C<first_billing_date>
252 is unset) and adding the order value length with each step. If the
253 date given by the C<date> parameter plus the billing period length
254 equals one of those dates then the given date is indeed the date of
255 the last invoice in that particular order value cycle.
256
257 =item C<sub disable_one_time_config>
258
259 Sets the state of the periodic_invoices_configs to inactive
260 (active => false) and closes the source order (closed => true)
261 if the periodicity is <Co> (one time).
262
263 Returns undef if the periodicity is not 'one time' otherwise the
264 order number of the deactivated periodic order.
265
266 =back
267
268 =head1 BUGS
269
270 Nothing here yet.
271
272 =head1 AUTHOR
273
274 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
275
276 =cut