CTR: POD um neue Parameter erweitert
[kivitendo-erp.git] / SL / BackgroundJob / ConvertTimeRecordings.pm
1 package SL::BackgroundJob::ConvertTimeRecordings;
2
3 use strict;
4
5 use parent qw(SL::BackgroundJob::Base);
6
7 use SL::DB::DeliveryOrder;
8 use SL::DB::TimeRecording;
9
10 use SL::Locale::String qw(t8);
11
12 use DateTime;
13 use Try::Tiny;
14
15 sub create_job {
16   $_[0]->create_standard_job('7 3 1 * *'); # every first day of month at 03:07
17 }
18
19
20 #
21 # If job does not throw an error,
22 # success in background_job_histories is 'success'.
23 # It is 'failure' otherwise.
24 #
25 # Return value goes to result in background_job_histories.
26 #
27 sub run {
28   my ($self, $db_obj) = @_;
29
30   my $data;
31   $data = $db_obj->data_as_hash if $db_obj;
32
33   $self->{$_} = [] for qw(job_errors);
34   # from/to date from data. Defaults to begining and end of last month.
35   my $from_date;
36   my $to_date;
37   # handle errors with a catch handler
38   try {
39     $from_date   = DateTime->from_kivitendo($data->{from_date}) if $data->{from_date};
40     $to_date     = DateTime->from_kivitendo($data->{to_date})   if $data->{to_date};
41   } catch {
42     die "Cannot convert date from string $data->{from_date} $data->{to_date}\n Details :\n $_"; # not $@
43   };
44   $from_date ||= DateTime->new( day => 1,    month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1);
45   $to_date   ||= DateTime->last_day_of_month(month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1);
46
47   $to_date->add(days => 1); # to get all from the to_date, because of the time part (15.12.2020 23.59 > 15.12.2020)
48
49   my %customer_where;
50   %customer_where = ('customer.customernumber' => $data->{customernumbers}) if 'ARRAY' eq ref $data->{customernumbers};
51
52   my $time_recordings = SL::DB::Manager::TimeRecording->get_all(where        => [end_time => { ge_lt => [ $from_date, $to_date ]},
53                                                                                  or => [booked => 0, booked => undef],
54                                                                                  %customer_where],
55                                                                 with_objects => ['customer']);
56   my %time_recordings_by_customer_id;
57   push @{ $time_recordings_by_customer_id{$_->customer_id} }, $_ for @$time_recordings;
58
59   my @donumbers;
60   foreach my $customer_id (keys %time_recordings_by_customer_id) {
61     my $do;
62     if (!eval {
63       $do = SL::DB::DeliveryOrder->new_from_time_recordings($time_recordings_by_customer_id{$customer_id});
64       1;
65     }) {
66       $::lxdebug->message(LXDebug->WARN(),
67                           "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
68       push @{ $self->{job_errors} }, "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}};
69
70     }
71
72     if ($do) {
73       if (!SL::DB->client->with_transaction(sub {
74         $do->save;
75         $_->update_attributes(booked => 1) for @{$time_recordings_by_customer_id{$customer_id}};
76         1;
77       })) {
78         $::lxdebug->message(LXDebug->WARN(),
79                             "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
80         push @{ $self->{job_errors} }, "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}};
81       } else {
82         push @donumbers, $do->donumber;
83       }
84     }
85   }
86
87   my $msg  = t8('Number of delivery orders created:');
88   $msg    .= ' ';
89   $msg    .= scalar @donumbers;
90   $msg    .= ' (';
91   $msg    .= join ', ', @donumbers;
92   $msg    .= ').';
93   # die if errors exists
94   if (@{ $self->{job_errors} }) {
95     $msg  .= ' ' . t8('The following errors occurred:');
96     $msg  .= join "\n", @{ $self->{job_errors} };
97     die $msg;
98   }
99   return $msg;
100 }
101
102 1;
103
104 # possible data
105 # from_date: 01.12.2020
106 # to_date: 15.12.2020
107 # customernumbers: [1,2,3]
108 __END__
109
110 =pod
111
112 =encoding utf8
113
114 =head1 NAME
115
116 SL::BackgroundJob::ConvertTimeRecordings - Convert time recording
117 entries into delivery orders
118
119 =head1 SYNOPSIS
120
121 Get all time recording entries for the given period and customer numbers
122 and create delivery ordes out of that (using
123 C<SL::DB::DeliveryOrder-E<gt>new_from_time_recordings>).
124
125 =head1 CONFIGURATION
126
127 Some data can be provided to configure this backgroung job.
128 If there is user data and it cannot be validated the background job
129 returns a error messages.
130
131 =over 4
132
133 =item C<from_date>
134
135 The date from which on time recordings should be collected. It defaults
136 to the first day of the previous month.
137
138 Example (format depends on your settings):
139
140 from_date: 01.12.2020
141
142 =item C<to_date>
143
144 The date till which time recordings should be collected. It defaults
145 to the last day of the previous month.
146
147 Example (format depends on your settings):
148
149 to_date: 15.12.2020
150
151 =item C<customernumbers>
152
153 An array with the customer numbers for which time recordings should
154 be collected. If not given, time recordings for customers are
155 collected. This is the default.
156
157 Example (format depends on your settings):
158
159 customernumbers: [c1,22332,334343]
160
161 =item C<part_id>
162
163 The part id of a time based service which should be used to
164 book the times. If not set the clients config defaults is used.
165
166 =item C<rounding>
167
168 If set the 0 no rounding of the times will be done otherwise
169 the times will be rounded up to th full quarters of an hour,
170 ie. 0.25h 0.5h 0.75h 1.25h ...
171 Defaults to rounding true (1).
172
173 =item C<link_project>
174
175 If set the job tries to find a previous Order with the current
176 customer and project number and tries to do as much automatic
177 workflow processing as the UI.
178 Defaults to off. If set to true (1) the job will fail if there
179 is no Sales Orders which qualifies as a predecessor.
180 Conditions for a predeccesor:
181
182  * Global project_id must match time_recording.project_id OR data.project_id
183  * Customer name must match time_recording.customer_id OR data.customernumbers
184  * The sales order must have at least one or more time related services
185  * The Project needs to be valid and active
186
187 The job doesn't care if the Sales Order is already delivered or closed.
188 If the sales order is overdelivered some organisational stuff needs to be done.
189 The sales order may also already be closed, ie the amount is fully billed, but
190 the services are not yet fully delivered (simple case: 'Payment in advance').
191
192 Hint: take a look or extend the job CloseProjectsBelongingToClosedSalesOrder for
193 further automatisation of your organisational needs.
194
195
196 =item C<project_id>
197
198 Use this project_id instead of the project_id in the time recordings.
199
200 =back
201
202 =head1 AUTHOR
203
204 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
205
206 =cut