1 package SL::BackgroundJob::ConvertTimeRecordings;
5 use parent qw(SL::BackgroundJob::Base);
7 use SL::DB::DeliveryOrder;
8 use SL::DB::TimeRecording;
10 use SL::Locale::String qw(t8);
17 $_[0]->create_standard_job('7 3 1 * *'); # every first day of month at 03:07
19 use Rose::Object::MakeMethods::Generic (
20 'scalar --get_set_init' => [ qw(rounding link_project) ],
23 # valid parameters -> better as class members with rose generic set/get
27 customernumbers => '',
35 # If job does not throw an error,
36 # success in background_job_histories is 'success'.
37 # It is 'failure' otherwise.
39 # Return value goes to result in background_job_histories.
42 my ($self, $db_obj) = @_;
45 $data = $db_obj->data_as_hash if $db_obj;
47 $self->{$_} = [] for qw(job_errors);
49 # check user input param names
50 foreach my $param (keys %{ $data }) {
51 die "Not a valid parameter: $param" unless exists $valid_params{$param};
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)
59 # from/to date from data. Defaults to begining and end of last month.
60 # TODO get/set see above
63 # handle errors with a catch handler
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};
68 die "Cannot convert date from string $data->{from_date} $data->{to_date}\n Details :\n $_"; # not $@
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);
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)
76 %customer_where = ('customer.customernumber' => $data->{customernumbers}) if 'ARRAY' eq ref $data->{customernumbers};
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],
81 with_objects => ['customer']);
82 # no time recordings at all ? -> better exit here before iterating a empty hash
83 # return undef or message unless ref $time_recordings->[0] eq SL::DB::Manager::TimeRecording;
84 my %time_recordings_by_customer_id;
85 # push @{ $time_recordings_by_customer_id{$_->customer_id} }, $_ for @$time_recordings;
86 # loop over all entries and add default or user defined params:
88 for my $source_entry (@$time_recordings) {
89 # set user defaults for processing
90 $source_entry->{$_} = $self->$_ for qw(rounding link_project);
91 foreach (qw(project_id parts_id)) {
92 $source_entry->{$_} = $self->{$_} if length ($self->{$_});
94 push @{ $time_recordings_by_customer_id{$source_entry->customer_id} }, $source_entry;
97 foreach my $customer_id (keys %time_recordings_by_customer_id) {
100 $do = SL::DB::DeliveryOrder->new_from_time_recordings($time_recordings_by_customer_id{$customer_id});
103 $::lxdebug->message(LXDebug->WARN(),
104 "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
105 push @{ $self->{job_errors} }, "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}};
110 if (!SL::DB->client->with_transaction(sub {
112 $_->update_attributes(booked => 1) for @{$time_recordings_by_customer_id{$customer_id}};
115 $::lxdebug->message(LXDebug->WARN(),
116 "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}});
117 push @{ $self->{job_errors} }, "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_customer_id{$customer_id}};
119 push @donumbers, $do->donumber;
124 my $msg = t8('Number of delivery orders created:');
126 $msg .= scalar @donumbers;
128 $msg .= join ', ', @donumbers;
130 # die if errors exists
131 if (@{ $self->{job_errors} }) {
132 $msg .= ' ' . t8('The following errors occurred:');
133 $msg .= join "\n", @{ $self->{job_errors} };
145 sub init_link_project {
152 # from_date: 01.12.2020
153 # to_date: 15.12.2020
154 # customernumbers: [1,2,3]
163 SL::BackgroundJob::ConvertTimeRecordings - Convert time recording
164 entries into delivery orders
168 Get all time recording entries for the given period and customer numbers
169 and create delivery ordes out of that (using
170 C<SL::DB::DeliveryOrder-E<gt>new_from_time_recordings>).
174 Some data can be provided to configure this backgroung job.
175 If there is user data and it cannot be validated the background job
176 returns a error messages.
182 The date from which on time recordings should be collected. It defaults
183 to the first day of the previous month.
185 Example (format depends on your settings):
187 from_date: 01.12.2020
191 The date till which time recordings should be collected. It defaults
192 to the last day of the previous month.
194 Example (format depends on your settings):
198 =item C<customernumbers>
200 An array with the customer numbers for which time recordings should
201 be collected. If not given, time recordings for customers are
202 collected. This is the default.
204 Example (format depends on your settings):
206 customernumbers: [c1,22332,334343]
210 The part id of a time based service which should be used to
211 book the times. If not set the clients config defaults is used.
215 If set the 0 no rounding of the times will be done otherwise
216 the times will be rounded up to th full quarters of an hour,
217 ie. 0.25h 0.5h 0.75h 1.25h ...
218 Defaults to rounding true (1).
220 =item C<link_project>
222 If set the job tries to find a previous Order with the current
223 customer and project number and tries to do as much automatic
224 workflow processing as the UI.
225 Defaults to off. If set to true (1) the job will fail if there
226 is no Sales Orders which qualifies as a predecessor.
227 Conditions for a predeccesor:
229 * Global project_id must match time_recording.project_id OR data.project_id
230 * Customer name must match time_recording.customer_id OR data.customernumbers
231 * The sales order must have at least one or more time related services
232 * The Project needs to be valid and active
234 The job doesn't care if the Sales Order is already delivered or closed.
235 If the sales order is overdelivered some organisational stuff needs to be done.
236 The sales order may also already be closed, ie the amount is fully billed, but
237 the services are not yet fully delivered (simple case: 'Payment in advance').
239 Hint: take a look or extend the job CloseProjectsBelongingToClosedSalesOrder for
240 further automatisation of your organisational needs.
245 Use this project_id instead of the project_id in the time recordings.
251 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>