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