Revision 840ae408
Von Bernd Bleßmann vor fast 4 Jahren hinzugefügt
SL/BackgroundJob/ConvertTimeRecordings.pm | ||
---|---|---|
use parent qw(SL::BackgroundJob::Base);
|
||
|
||
use SL::DB::DeliveryOrder;
|
||
use SL::DB::Part;
|
||
use SL::DB::Project;
|
||
use SL::DB::TimeRecording;
|
||
|
||
use SL::Locale::String qw(t8);
|
||
|
||
use Carp;
|
||
use DateTime;
|
||
use List::Util qw(any);
|
||
use Try::Tiny;
|
||
|
||
sub create_job {
|
||
$_[0]->create_standard_job('7 3 1 * *'); # every first day of month at 03:07
|
||
}
|
||
use Rose::Object::MakeMethods::Generic (
|
||
'scalar' => [ qw(data) ],
|
||
'scalar --get_set_init' => [ qw(rounding link_project) ],
|
||
);
|
||
|
||
... | ... | |
sub run {
|
||
my ($self, $db_obj) = @_;
|
||
|
||
my $data;
|
||
$data = $db_obj->data_as_hash if $db_obj;
|
||
$self->data($db_obj->data_as_hash) if $db_obj;
|
||
|
||
$self->{$_} = [] for qw(job_errors);
|
||
|
||
# check user input param names
|
||
foreach my $param (keys %{ $data }) {
|
||
foreach my $param (keys %{ $self->data }) {
|
||
die "Not a valid parameter: $param" unless exists $valid_params{$param};
|
||
}
|
||
|
||
... | ... | |
my $to_date;
|
||
# handle errors with a catch handler
|
||
try {
|
||
$from_date = DateTime->from_kivitendo($data->{from_date}) if $data->{from_date};
|
||
$to_date = DateTime->from_kivitendo($data->{to_date}) if $data->{to_date};
|
||
$from_date = DateTime->from_kivitendo($self->data->{from_date}) if $self->data->{from_date};
|
||
$to_date = DateTime->from_kivitendo($self->data->{to_date}) if $self->data->{to_date};
|
||
} catch {
|
||
die "Cannot convert date from string $data->{from_date} $data->{to_date}\n Details :\n $_"; # not $@
|
||
die "Cannot convert date from string $self->data->{from_date} $self->data->{to_date}\n Details :\n $_"; # not $@
|
||
};
|
||
$from_date ||= DateTime->new( day => 1, month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1);
|
||
$to_date ||= DateTime->last_day_of_month(month => DateTime->today_local->month, year => DateTime->today_local->year)->subtract(months => 1);
|
||
... | ... | |
$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)
|
||
|
||
my %customer_where;
|
||
%customer_where = ('customer.customernumber' => $data->{customernumbers}) if 'ARRAY' eq ref $data->{customernumbers};
|
||
%customer_where = ('customer.customernumber' => $self->data->{customernumbers}) if 'ARRAY' eq ref $self->data->{customernumbers};
|
||
|
||
my $time_recordings = SL::DB::Manager::TimeRecording->get_all(where => [end_time => { ge_lt => [ $from_date, $to_date ]},
|
||
or => [booked => 0, booked => undef],
|
||
... | ... | |
# no time recordings at all ? -> better exit here before iterating a empty hash
|
||
# return undef or message unless ref $time_recordings->[0] eq SL::DB::Manager::TimeRecording;
|
||
|
||
my @donumbers;
|
||
|
||
if ($self->data->{link_project}) {
|
||
my %time_recordings_by_order_id;
|
||
my %orders_by_order_id;
|
||
foreach my $tr (@$time_recordings) {
|
||
my $order = $self->get_order_for_time_recording($tr);
|
||
next if !$order;
|
||
push @{ $time_recordings_by_order_id{$order->id} }, $tr;
|
||
$orders_by_order_id{$order->id} ||= $order;
|
||
}
|
||
@donumbers = $self->convert_with_linking(\%time_recordings_by_order_id, \%orders_by_order_id);
|
||
|
||
} else {
|
||
@donumbers = $self->convert_without_linking($time_recordings);
|
||
}
|
||
|
||
my $msg = t8('Number of delivery orders created:');
|
||
$msg .= ' ';
|
||
$msg .= scalar @donumbers;
|
||
$msg .= ' (';
|
||
$msg .= join ', ', @donumbers;
|
||
$msg .= ').';
|
||
# die if errors exists
|
||
if (@{ $self->{job_errors} }) {
|
||
$msg .= ' ' . t8('The following errors occurred:');
|
||
$msg .= ' ';
|
||
$msg .= join "\n", @{ $self->{job_errors} };
|
||
die $msg . "\n";
|
||
}
|
||
return $msg;
|
||
}
|
||
|
||
# inits
|
||
|
||
sub init_rounding {
|
||
1
|
||
}
|
||
|
||
sub init_link_project {
|
||
0
|
||
}
|
||
|
||
# helper
|
||
sub convert_without_linking {
|
||
my ($self, $time_recordings) = @_;
|
||
|
||
my %time_recordings_by_customer_id;
|
||
push @{ $time_recordings_by_customer_id{$_->customer_id} }, $_ for @$time_recordings;
|
||
|
||
my %convert_params = map { $_ => $data->{$_} } qw(rounding link_project part_id project_id);
|
||
my %convert_params = map { $_ => $self->data->{$_} } qw(rounding link_project project_id);
|
||
$convert_params{default_part_id} = $self->data->{part_id};
|
||
|
||
my @donumbers;
|
||
foreach my $customer_id (keys %time_recordings_by_customer_id) {
|
||
... | ... | |
}
|
||
}
|
||
|
||
my $msg = t8('Number of delivery orders created:');
|
||
$msg .= ' ';
|
||
$msg .= scalar @donumbers;
|
||
$msg .= ' (';
|
||
$msg .= join ', ', @donumbers;
|
||
$msg .= ').';
|
||
# die if errors exists
|
||
if (@{ $self->{job_errors} }) {
|
||
$msg .= ' ' . t8('The following errors occurred:');
|
||
$msg .= ' ';
|
||
$msg .= join "\n", @{ $self->{job_errors} };
|
||
die $msg . "\n";
|
||
}
|
||
return $msg;
|
||
return @donumbers;
|
||
}
|
||
|
||
# inits
|
||
sub convert_with_linking {
|
||
my ($self, $time_recordings_by_order_id, $orders_by_order_id) = @_;
|
||
|
||
sub init_rounding {
|
||
1
|
||
my %convert_params = map { $_ => $self->data->{$_} } qw(rounding link_project project_id);
|
||
$convert_params{default_part_id} = $self->data->{part_id};
|
||
|
||
my @donumbers;
|
||
foreach my $related_order_id (keys %$time_recordings_by_order_id) {
|
||
my $related_order = $orders_by_order_id->{$related_order_id};
|
||
my $do;
|
||
if (!eval {
|
||
$do = SL::DB::DeliveryOrder->new_from_time_recordings($time_recordings_by_order_id->{$related_order_id}, related_order => $related_order, %convert_params);
|
||
1;
|
||
}) {
|
||
$::lxdebug->message(LXDebug->WARN(),
|
||
"ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
|
||
push @{ $self->{job_errors} }, "ConvertTimeRecordings: creating delivery order failed ($@) for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}};
|
||
}
|
||
|
||
if ($do) {
|
||
if (!SL::DB->client->with_transaction(sub {
|
||
$do->save;
|
||
$_->update_attributes(booked => 1) for @{$time_recordings_by_order_id->{$related_order_id}};
|
||
|
||
# Todo: reduce qty on related order
|
||
1;
|
||
})) {
|
||
$::lxdebug->message(LXDebug->WARN(),
|
||
"ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}});
|
||
push @{ $self->{job_errors} }, "ConvertTimeRecordings: saving delivery order failed for time recording ids " . join ', ', map { $_->id } @{$time_recordings_by_order_id->{$related_order_id}};
|
||
} else {
|
||
push @donumbers, $do->donumber;
|
||
}
|
||
}
|
||
}
|
||
|
||
return @donumbers;
|
||
}
|
||
|
||
sub init_link_project {
|
||
0
|
||
sub get_order_for_time_recording {
|
||
my ($self, $tr) = @_;
|
||
|
||
# check project
|
||
my $project_id;
|
||
#$project_id = $self->overide_project_id;
|
||
$project_id = $self->data->{project_id};
|
||
$project_id ||= $tr->project_id;
|
||
#$project_id ||= $self->default_project_id;
|
||
|
||
if (!$project_id) {
|
||
my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : no project id';
|
||
$::lxdebug->message(LXDebug->WARN(), $err_msg);
|
||
push @{ $self->{job_errors} }, $err_msg;
|
||
return;
|
||
}
|
||
|
||
my $project = SL::DB::Project->load_cached($project_id);
|
||
|
||
if (!$project) {
|
||
my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : project not found';
|
||
$::lxdebug->message(LXDebug->WARN(), $err_msg);
|
||
push @{ $self->{job_errors} }, $err_msg;
|
||
return;
|
||
}
|
||
if (!$project->active || !$project->valid) {
|
||
my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : project not active or not valid';
|
||
$::lxdebug->message(LXDebug->WARN(), $err_msg);
|
||
push @{ $self->{job_errors} }, $err_msg;
|
||
return;
|
||
}
|
||
if ($project->customer_id && $project->customer_id != $tr->customer_id) {
|
||
my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : project customer does not match customer of time recording';
|
||
$::lxdebug->message(LXDebug->WARN(), $err_msg);
|
||
push @{ $self->{job_errors} }, $err_msg;
|
||
return;
|
||
}
|
||
|
||
# check part
|
||
my $part_id;
|
||
#$part_id = $self->overide_part_id;
|
||
$part_id ||= $tr->part_id;
|
||
#$part_id ||= $self->default_part_id;
|
||
$part_id ||= $self->data->{part_id};
|
||
|
||
if (!$part_id) {
|
||
my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : no part id';
|
||
$::lxdebug->message(LXDebug->WARN(), $err_msg);
|
||
push @{ $self->{job_errors} }, $err_msg;
|
||
return;
|
||
}
|
||
my $part = SL::DB::Part->load_cached($part_id);
|
||
if (!$part->unit_obj->is_time_based) {
|
||
my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : part unit is not time based';
|
||
$::lxdebug->message(LXDebug->WARN(), $err_msg);
|
||
push @{ $self->{job_errors} }, $err_msg;
|
||
return;
|
||
}
|
||
|
||
my $orders = SL::DB::Manager::Order->get_all(where => [customer_id => $tr->customer_id,
|
||
or => [quotation => undef, quotation => 0],
|
||
globalproject_id => $project_id, ]);
|
||
my @matching_orders;
|
||
foreach my $order (@$orders) {
|
||
if (any { $_->parts_id == $part_id } @{ $order->items_sorted }) {
|
||
push @matching_orders, $order;
|
||
}
|
||
}
|
||
|
||
if (1 != scalar @matching_orders) {
|
||
my $err_msg = 'ConvertTimeRecordings: searching related order failed for time recording id ' . $tr->id . ' : no or more than one orders do match';
|
||
$::lxdebug->message(LXDebug->WARN(), $err_msg);
|
||
push @{ $self->{job_errors} }, $err_msg;
|
||
return;
|
||
}
|
||
|
||
return $matching_orders[0];
|
||
}
|
||
|
||
1;
|
SL/DB/DeliveryOrder.pm | ||
---|---|---|
croak("Unsupported object type in sources") if any { ref($_) ne 'SL::DB::TimeRecording' } @$sources;
|
||
croak("Cannot create delivery order from source records of different customers") if any { $_->customer_id != $sources->[0]->customer_id } @$sources;
|
||
|
||
my %args = (
|
||
is_sales => 1,
|
||
delivered => 0,
|
||
customer_id => $sources->[0]->customer_id,
|
||
taxzone_id => $sources->[0]->customer->taxzone_id,
|
||
currency_id => $sources->[0]->customer->currency_id,
|
||
employee_id => SL::DB::Manager::Employee->current->id,
|
||
salesman_id => SL::DB::Manager::Employee->current->id,
|
||
items => [],
|
||
);
|
||
my $delivery_order = $class->new(%args);
|
||
$delivery_order->assign_attributes(%{ $params{attributes} }) if $params{attributes};
|
||
|
||
# - one item per part (article)
|
||
# - qty is sum of duration
|
||
# - description goes to item longdescription
|
||
... | ... | |
$entries->{$part_id}->{$date}->{date_obj} = $source->start_time; # for sorting
|
||
}
|
||
|
||
my @items;
|
||
|
||
my $h_unit = SL::DB::Manager::Unit->find_h_unit;
|
||
|
||
my @keys = sort { $part_by_part_id{$a}->partnumber cmp $part_by_part_id{$b}->partnumber } keys %$entries;
|
||
... | ... | |
longdescription => $longdescription,
|
||
);
|
||
|
||
$delivery_order->add_items($item);
|
||
push @items, $item;
|
||
}
|
||
|
||
my $delivery_order;
|
||
|
||
if ($params{related_order}) {
|
||
$delivery_order = SL::DB::DeliveryOrder->new_from($params{related_order}, items => \@items, %params);
|
||
|
||
} else {
|
||
my %args = (
|
||
is_sales => 1,
|
||
delivered => 0,
|
||
customer_id => $sources->[0]->customer_id,
|
||
taxzone_id => $sources->[0]->customer->taxzone_id,
|
||
currency_id => $sources->[0]->customer->currency_id,
|
||
employee_id => SL::DB::Manager::Employee->current->id,
|
||
salesman_id => SL::DB::Manager::Employee->current->id,
|
||
items => \@items,
|
||
);
|
||
$delivery_order = $class->new(%args);
|
||
$delivery_order->assign_attributes(%{ $params{attributes} }) if $params{attributes};
|
||
}
|
||
|
||
return $delivery_order;
|
Auch abrufbar als: Unified diff
Zeiterfassung: Parameter f. Konvertierung mit link_project/related order