From: Bernd Bleßmann Date: Tue, 17 Nov 2020 16:25:03 +0000 (+0100) Subject: Zeiterfassung: Controller X-Git-Tag: kivitendo-mebil_0.1-0~9^2~340 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=8915e7750d73593e86979e9a6a9016016a70c808;p=kivitendo-erp.git Zeiterfassung: Controller --- diff --git a/SL/Controller/TimeRecording.pm b/SL/Controller/TimeRecording.pm new file mode 100644 index 000000000..95de51388 --- /dev/null +++ b/SL/Controller/TimeRecording.pm @@ -0,0 +1,259 @@ +package SL::Controller::TimeRecording; + +use strict; +use parent qw(SL::Controller::Base); + +use DateTime; +use English qw(-no_match_vars); +use POSIX qw(strftime); + +use SL::Controller::Helper::GetModels; +use SL::Controller::Helper::ReportGenerator; +use SL::DB::Customer; +use SL::DB::Employee; +use SL::DB::TimeRecording; +use SL::Locale::String qw(t8); +use SL::ReportGenerator; + +use Rose::Object::MakeMethods::Generic +( +# scalar => [ qw() ], + 'scalar --get_set_init' => [ qw(time_recording models all_time_recording_types all_employees) ], +); + + +# safety +#__PACKAGE__->run_before('check_auth'); + +# +# actions +# + +my %sort_columns = ( + start_time => t8('Start'), + end_time => t8('End'), + customer => t8('Customer'), + type => t8('Type'), + project => t8('Project'), + description => t8('Description'), + staff_member => t8('Mitarbeiter'), + duration => t8('Duration'), +); + +sub action_list { + my ($self, %params) = @_; + + $self->setup_list_action_bar; + $self->make_filter_summary; + $self->prepare_report; + + $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get); +} + +sub action_edit { + my ($self) = @_; + + $::request->{layout}->use_javascript("${_}.js") for qw(kivi.TimeRecording ckeditor/ckeditor ckeditor/adapters/jquery kivi.Validator); + + if ($self->time_recording->start_time) { + $self->{start_date} = $self->time_recording->start_time->to_kivitendo; + $self->{start_time} = $self->time_recording->start_time->to_kivitendo_time; + } + if ($self->time_recording->end_time) { + $self->{end_date} = $self->time_recording->end_time->to_kivitendo; + $self->{end_time} = $self->time_recording->end_time->to_kivitendo_time; + } + + $self->setup_edit_action_bar; + + $self->render('time_recording/form', + title => t8('Time Recording'), + ); +} + +sub action_save { + my ($self) = @_; + + my @errors = $self->time_recording->validate; + if (@errors) { + $::form->error(t8('Saving the time recording entry failed: #1', join '
', @errors)); + return; + } + + if ( !eval { $self->time_recording->save; 1; } ) { + $::form->error(t8('Saving the time recording entry failed: #1', $EVAL_ERROR)); + return; + } + + $self->redirect_to(safe_callback()); +} + +sub action_delete { + my ($self) = @_; + + $self->time_recording->delete; + + $self->redirect_to(safe_callback()); +} + +sub init_time_recording { + my $time_recording = ($::form->{id}) ? SL::DB::TimeRecording->new(id => $::form->{id})->load + : SL::DB::TimeRecording->new(start_time => DateTime->now_local); + + my %attributes = %{ $::form->{time_recording} || {} }; + + foreach my $type (qw(start end)) { + if ($::form->{$type . '_date'}) { + my $date = DateTime->from_kivitendo($::form->{$type . '_date'}); + $attributes{$type . '_time'} = $date->clone; + if ($::form->{$type . '_time'}) { + my ($hour, $min) = split ':', $::form->{$type . '_time'}; + $attributes{$type . '_time'}->set_hour($hour) if $hour; + $attributes{$type . '_time'}->set_minute($min) if $min; + } + } + } + + $attributes{staff_member_id} = $attributes{employee_id} = SL::DB::Manager::Employee->current->id; + + $time_recording->assign_attributes(%attributes); + + return $time_recording; +} + +sub init_models { + SL::Controller::Helper::GetModels->new( + controller => $_[0], + sorted => \%sort_columns, + disable_plugin => 'paginated', + with_objects => [ 'customer', 'type', 'project', 'staff_member', 'employee' ], + ); +} + +sub init_all_time_recording_types { + SL::DB::Manager::TimeRecordingType->get_all_sorted(query => [obsolete => 0]); +} + +sub init_all_employees { + SL::DB::Manager::Employee->get_all_sorted; +} + +sub prepare_report { + my ($self) = @_; + + my $report = SL::ReportGenerator->new(\%::myconfig, $::form); + $self->{report} = $report; + + my @columns = qw(start_time end_time customer type project description staff_member duration); + + my %column_defs = ( + start_time => { text => t8('Start'), sub => sub { $_[0]->start_time_as_timestamp }, + obj_link => sub { $self->url_for(action => 'edit', 'id' => $_[0]->id, callback => $self->models->get_callback) } }, + end_time => { text => t8('End'), sub => sub { $_[0]->end_time_as_timestamp }, + obj_link => sub { $self->url_for(action => 'edit', 'id' => $_[0]->id, callback => $self->models->get_callback) } }, + customer => { text => t8('Customer'), sub => sub { $_[0]->customer->displayable_name } }, + type => { text => t8('Type'), sub => sub { $_[0]->type && $_[0]->type->abbreviation } }, + project => { text => t8('Project'), sub => sub { $_[0]->project && $_[0]->project->displayable_name } }, + description => { text => t8('Description'), sub => sub { $_[0]->description_as_stripped_html }, + raw_data => sub { $_[0]->description_as_restricted_html }, # raw_data only used for html(?) + obj_link => sub { $self->url_for(action => 'edit', 'id' => $_[0]->id, callback => $self->models->get_callback) } }, + staff_member => { text => t8('Mitarbeiter'), sub => sub { $_[0]->staff_member->safe_name } }, + duration => { text => t8('Duration'), sub => sub { $_[0]->duration_as_duration_string }, + align => 'right'}, + ); + + $report->set_options( + controller_class => 'TimeRecording', + std_column_visibility => 1, + output_format => 'HTML', + title => t8('Time Recordings'), + allow_pdf_export => 1, + allow_csv_export => 1, + ); + + $report->set_columns(%column_defs); + $report->set_column_order(@columns); + $report->set_export_options(qw(list filter)); + $report->set_options_from_form; + + $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i; + #$self->models->add_additional_url_params(); + $self->models->finalize; + $self->models->set_report_generator_sort_options(report => $report, sortable_columns => [keys %sort_columns]); + + $report->set_options( + raw_top_info_text => $self->render('time_recording/report_top', { output => 0 }), + raw_bottom_info_text => $self->render('time_recording/report_bottom', { output => 0 }, models => $self->models), + attachment_basename => t8('time_recordings') . strftime('_%Y%m%d', localtime time), + ); +} + +sub make_filter_summary { + my ($self) = @_; + + my $filter = $::form->{filter} || {}; + my @filter_strings; + + my $staff_member = $filter->{staff_member_id} ? SL::DB::Employee->new(id => $filter->{staff_member_id})->load->safe_name : ''; + + my @filters = ( + [ $filter->{"start_time:date::ge"}, t8('From Start') ], + [ $filter->{"start_time:date::le"}, t8('To Start') ], + [ $filter->{"customer"}->{"name:substr::ilike"}, t8('Customer') ], + [ $filter->{"customer"}->{"customernumber:substr::ilike"}, t8('Customer Number') ], + [ $staff_member, t8('Mitarbeiter') ], + ); + + for (@filters) { + push @filter_strings, "$_->[1]: $_->[0]" if $_->[0]; + } + + $self->{filter_summary} = join ', ', @filter_strings; +} + +sub setup_list_action_bar { + my ($self) = @_; + + for my $bar ($::request->layout->get('actionbar')) { + $bar->add( + action => [ + t8('Update'), + submit => [ '#filter_form', { action => 'TimeRecording/list' } ], + accesskey => 'enter', + ], + action => [ + t8('Add'), + link => $self->url_for(action => 'edit', callback => $self->models->get_callback), + ], + ); + } +} + +sub setup_edit_action_bar { + my ($self) = @_; + + for my $bar ($::request->layout->get('actionbar')) { + $bar->add( + action => [ + t8('Save'), + submit => [ '#form', { action => 'TimeRecording/save' } ], + checks => [ 'kivi.validate_form' ], + ], + action => [ + t8('Delete'), + submit => [ '#form', { action => 'TimeRecording/delete' } ], + only_if => $self->time_recording->id, + ], + action => [ + t8('Cancel'), + link => $self->url_for(safe_callback()), + ], + ); + } +} + +sub safe_callback { + $::form->{callback} || (action => 'list') +} + +1; diff --git a/SL/DB/Manager/TimeRecording.pm b/SL/DB/Manager/TimeRecording.pm index f0c7f429e..e1e9b84e1 100644 --- a/SL/DB/Manager/TimeRecording.pm +++ b/SL/DB/Manager/TimeRecording.pm @@ -7,8 +7,20 @@ use strict; use parent qw(SL::DB::Helper::Manager); +use SL::DB::Helper::Sorted; + sub object_class { 'SL::DB::TimeRecording' } __PACKAGE__->make_manager_methods; + +sub _sort_spec { + return ( default => [ 'start_time', 1 ], + columns => { SIMPLE => 'ALL' , + customer => [ 'lower(customer.name)', ], + } + ); +} + + 1; diff --git a/SL/DB/TimeRecording.pm b/SL/DB/TimeRecording.pm index 341e572b2..970fa8738 100644 --- a/SL/DB/TimeRecording.pm +++ b/SL/DB/TimeRecording.pm @@ -5,9 +5,128 @@ package SL::DB::TimeRecording; use strict; +use SL::Locale::String qw(t8); + +use SL::DB::Helper::AttrDuration; +use SL::DB::Helper::AttrHTML; + use SL::DB::MetaSetup::TimeRecording; use SL::DB::Manager::TimeRecording; __PACKAGE__->meta->initialize; +__PACKAGE__->attr_duration_minutes(qw(duration)); + +__PACKAGE__->attr_html('description'); + +__PACKAGE__->before_save('_before_save_check_valid'); + +sub _before_save_check_valid { + my ($self) = @_; + + my @errors = $self->validate; + return (scalar @errors == 0); +} + +sub validate { + my ($self) = @_; + + my @errors; + + push @errors, t8('Start time must not be empty.') if !$self->start_time; + push @errors, t8('Customer must not be empty.') if !$self->customer_id; + push @errors, t8('Staff member must not be empty.') if !$self->staff_member_id; + push @errors, t8('Employee must not be empty.') if !$self->employee_id; + push @errors, t8('Description must not be empty.') if !$self->description; + push @errors, t8('Start time must be earlier than end time.') if $self->is_time_in_wrong_order; + + my $conflict = $self->is_time_overlapping; + push @errors, t8('Entry overlaps with "#1".', $conflict->displayable_times) if $conflict; + + return @errors; +} + +sub is_time_overlapping { + my ($self) = @_; + + # Do not allow overlapping time periods. + # Start time can be equal to another end time + # (an end time can be equal to another start time) + + # We cannot check if no staff member is given. + return if !$self->staff_member_id; + + # If no start time and no end time are given, there is no overlapping. + return if !($self->start_time || $self->end_time); + + my $conflicting; + + # Start time or end time can be undefined. + if (!$self->start_time) { + $conflicting = SL::DB::Manager::TimeRecording->get_all(where => [ and => [ '!id' => $self->id, + staff_member_id => $self->staff_member_id, + start_time => {lt => $self->end_time}, + end_time => {ge => $self->end_time} ] ], + sort_by => 'start_time DESC', + limit => 1); + } elsif (!$self->end_time) { + $conflicting = SL::DB::Manager::TimeRecording->get_all(where => [ and => [ '!id' => $self->id, + staff_member_id => $self->staff_member_id, + or => [ and => [start_time => {le => $self->start_time}, + end_time => {gt => $self->start_time} ], + start_time => $self->start_time, + ], + ], + ], + sort_by => 'start_time DESC', + limit => 1); + } else { + $conflicting = SL::DB::Manager::TimeRecording->get_all(where => [ and => [ '!id' => $self->id, + staff_member_id => $self->staff_member_id, + or => [ and => [ start_time => {lt => $self->end_time}, + end_time => {gt => $self->start_time} ] , + or => [ start_time => $self->start_time, + end_time => $self->end_time, ], + ] + ] + ], + sort_by => 'start_time DESC', + limit => 1); + } + + return $conflicting->[0] if @$conflicting; + return; +} + +sub is_time_in_wrong_order { + my ($self) = @_; + + if ($self->start_time && $self->end_time + && $self->start_time >= $self->end_time) { + return 1; + } + + return; +} + +sub displayable_times { + my ($self) = @_; + + # placeholder + my $ph = $::locale->format_date_object(DateTime->new(year => 1111, month => 11, day => 11, hour => 11, minute => 11), precision => 'minute'); + $ph =~ s{1}{-}g; + + return ($self->start_time_as_timestamp||$ph) . ' - ' . ($self->end_time_as_timestamp||$ph); +} + +sub duration { + my ($self) = @_; + + if ($self->start_time && $self->end_time) { + return ($self->end_time->subtract_datetime_absolute($self->start_time))->seconds/60.0; + } else { + return; + } +} + 1; diff --git a/SL/Dev/ALL.pm b/SL/Dev/ALL.pm index b702b0f5b..8bf30a551 100644 --- a/SL/Dev/ALL.pm +++ b/SL/Dev/ALL.pm @@ -9,10 +9,11 @@ use SL::Dev::Inventory; use SL::Dev::Record; use SL::Dev::Payment; use SL::Dev::Shop; +use SL::Dev::TimeRecording; sub import { no strict "refs"; - for (qw(Part CustomerVendor Inventory Record Payment Shop)) { + for (qw(Part CustomerVendor Inventory Record Payment Shop TimeRecording)) { Exporter::export_to_level("SL::Dev::$_", 1, @_); } } diff --git a/SL/Dev/TimeRecording.pm b/SL/Dev/TimeRecording.pm new file mode 100644 index 000000000..825472f81 --- /dev/null +++ b/SL/Dev/TimeRecording.pm @@ -0,0 +1,41 @@ +package SL::Dev::TimeRecording; + +use strict; +use base qw(Exporter); +our @EXPORT_OK = qw(new_time_recording); +our %EXPORT_TAGS = (ALL => \@EXPORT_OK); + +use DateTime; + +use SL::DB::TimeRecording; + +use SL::DB::Employee; +use SL::Dev::CustomerVendor qw(new_customer); + + +sub new_time_recording { + my (%params) = @_; + + my $customer = delete $params{customer} // new_customer(name => 'Testcustomer')->save; + die "illegal customer" unless defined $customer && ref($customer) eq 'SL::DB::Customer'; + + my $employee = $params{employee} // SL::DB::Manager::Employee->current; + my $staff_member = $params{staff_member} // $employee; + + my $now = DateTime->now_local; + + my $time_recording = SL::DB::TimeRecording->new( + start_time => $now, + end_time => $now->add(hours => 1), + customer => $customer, + description => '

this and that

', + staff_member => $staff_member, + employee => $employee, + %params, + ); + + return $time_recording; +} + + +1; diff --git a/js/kivi.TimeRecording.js b/js/kivi.TimeRecording.js new file mode 100644 index 000000000..cf5014905 --- /dev/null +++ b/js/kivi.TimeRecording.js @@ -0,0 +1,22 @@ +namespace('kivi.TimeRecording', function(ns) { + 'use strict'; + + ns.set_end_date = function() { + if ($('#start_date').val() !== '' && $('#end_date').val() === '') { + var kivi_start_date = kivi.format_date(kivi.parse_date($('#start_date').val())); + $('#end_date').val(kivi_start_date); + } + }; + + ns.set_current_date_time = function(what) { + if (what !== 'start' && what !== 'end') return; + + var $date = $('#' + what + '_date'); + var $time = $('#' + what + '_time'); + var date = new Date(); + + $date.val(kivi.format_date(date)); + $time.val(kivi.format_time(date)); + }; + +}); diff --git a/menus/user/10-time-recording.yaml b/menus/user/10-time-recording.yaml index 204d8a9a0..0e42a4f36 100644 --- a/menus/user/10-time-recording.yaml +++ b/menus/user/10-time-recording.yaml @@ -6,3 +6,15 @@ params: action: SimpleSystemSetting/list type: time_recording_type +- parent: productivity + id: productivity_time_recording + name: Time Recording + order: 350 + params: + action: TimeRecording/edit +- parent: productivity_reports + id: productivity_reports_time_recording + name: Time Recording + order: 300 + params: + action: TimeRecording/list diff --git a/t/db/time_recordig.t b/t/db/time_recordig.t new file mode 100644 index 000000000..33b036484 --- /dev/null +++ b/t/db/time_recordig.t @@ -0,0 +1,582 @@ +use Test::More tests => 40; + +use strict; + +use lib 't'; +use utf8; + +use Support::TestSetup; +use Test::Exception; +use DateTime; + +use_ok 'SL::DB::TimeRecording'; + +use SL::Dev::ALL qw(:ALL); + +Support::TestSetup::login(); + +my @time_recordings; +my ($s1, $e1, $s2, $e2); + +sub clear_up { + foreach (qw(TimeRecording Customer)) { + "SL::DB::Manager::${_}"->delete_all(all => 1); + } + SL::DB::Manager::Employee->delete_all(where => [ '!login' => 'unittests' ]); +}; + +######################################## + +$s1 = DateTime->now_local; +$e1 = $s1->clone; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1); + +ok( $time_recordings[0]->is_time_in_wrong_order, 'same start and end detected' ); +ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping if only one time recording entry in db' ); + +### +$time_recordings[0]->end_time(undef); +ok( !$time_recordings[0]->is_time_in_wrong_order, 'order ok if no end' ); + +######################################## +# ------------s1-----e1----- +# --s2---e2----------------- +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 11, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2)->save; + +ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping: completely before 1' ); +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: completely before 2' ); + + +# -------s1-----e1---------- +# --s2---e2----------------- +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2)->save; + +ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping: before 1' ); +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: before 2' ); + +# ---s1-----e1-------------- +# ---------------s2---e2---- +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 13, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2)->save; + +ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping: completely after 1' ); +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: completely after 2' ); + +# ---s1-----e1-------------- +# ----------s2---e2--------- +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2)->save; + +ok( !$time_recordings[0]->is_time_overlapping, 'not overlapping: after 1' ); +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: after 2' ); + +# -------s1-----e1---------- +# ---s2-----e2-------------- +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 9, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: start before, end inbetween' ); + +# -------s1-----e1---------- +# -----------s2-----e2------ +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: start inbetween, end after' ); + +# ---s1---------e1---------- +# ------s2---e2------------- +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: completely inbetween' ); + + +# ------s1---e1------------- +# ---s2---------e2---------- +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: completely oudside' ); + + +# ---s1---e1---------------- +# ---s2---------e2---------- +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: same start, end outside' ); + +# ---s1------e1------------- +# ------s2---e2------------- +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: start after, same end' ); + +# ---s1------e1------------- +# ------s2------------------ +# e2 undef +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: start inbetween, no end' ); + +# ---s1------e1------------- +# ---s2--------------------- +# e2 undef +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: same start, no end' ); + +# -------s1------e1--------- +# ---s2--------------------- +# e2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: start before, no end' ); + +# -------s1------e1--------- +# -------------------s2----- +# e2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 16, minute => 0); +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: start after, no end' ); + +# -------s1------e1--------- +# ---------------s2--------- +# e2 undef +# -> does not overlap + +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: same start as other end, no end' ); + +# -------s1------e1--------- +# -----------e2------------- +# s2 undef +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); +$s2 = undef; +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: no start, end inbetween' ); + +# -------s1------e1--------- +# ---------------e2--------- +# s2 undef +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); +$s2 = undef; +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: no start, same end' ); + +# -------s1------e1--------- +# --e2---------------------- +# s2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); +$s2 = undef; +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no start, end before' ); + +# -------s1------e1--------- +# -------------------e2----- +# s2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0); +$s2 = undef; +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 17, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no start, end after' ); + +# -------s1------e1--------- +# -------e2----------------- +# s2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0); +$s2 = undef; +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no start, same end as other start' ); + +# ----s1-------------------- +# ----s2-----e2------------- +# e1 undef +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: no end in db, same start' ); + +# --------s1---------------- +# ----s2-----e2------------- +# e1 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, enclosing' ); + +# ---s1--------------------- +# ---------s2-----e2-------- +# e1 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = undef; +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, completely after' ); + +# ---------s1--------------- +# -------------------------- +# e1, s2, e2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = undef; +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, no times in object' ); + +# ---------s1--------------- +# -----s2------------------- +# e1, e2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, start before, no end in object' ); + +# ---------s1--------------- +# -------------s2----------- +# e1, e2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, start after, no end in object' ); + +# ---------s1--------------- +# ---------s2--------------- +# e1, e2 undef +# -> overlaps +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e2 = undef; + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping: no end in db, same start' ); + +# ---------s1--------------- +# ---e2--------------------- +# e1, s2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = undef; +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, no start in object, end before' ); + +# ---------s1--------------- +# ---------------e2--------- +# e1, s2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = undef; +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, no start in object, end after' ); + +# ---------s1--------------- +# ---------e2--------------- +# e1, s2 undef +# -> does not overlap +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); +$e1 = undef; +$s2 = undef; +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 12, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping: no end in db, no start in object, same end' ); + +######################################## +# not overlapping if different staff_member +$s1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 10, minute => 0); +$e1 = DateTime->new(year => 2020, month => 11, day => 15, hour => 15, minute => 0); +$s2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 11, minute => 0); +$e2 = DateTime->new(year => 2020, month => 11, day => 15, hour => 14, minute => 0); + +clear_up; + +@time_recordings = (); +push @time_recordings, new_time_recording(start_time => $s1, end_time => $e1)->save; +push @time_recordings, new_time_recording(start_time => $s2, end_time => $e2); + +ok( $time_recordings[1]->is_time_overlapping, 'overlapping if same staff member' ); +$time_recordings[1]->update_attributes(staff_member => SL::DB::Employee->new( + 'login' => 'testuser', + 'name' => 'Test User', + )->save); +ok( !$time_recordings[1]->is_time_overlapping, 'not overlapping if different staff member' ); + +clear_up; + +1; + + +# set emacs to perl mode +# Local Variables: +# mode: perl +# End: diff --git a/templates/webpages/time_recording/_filter.html b/templates/webpages/time_recording/_filter.html new file mode 100644 index 000000000..c94f00d88 --- /dev/null +++ b/templates/webpages/time_recording/_filter.html @@ -0,0 +1,48 @@ +[%- USE T8 %] +[%- USE L %] +[%- USE LxERP %] +[%- USE HTML %] +
+
+[% 'Show Filter' | $T8 %] + [% SELF.filter_summary | html %] +
+ + +
diff --git a/templates/webpages/time_recording/form.html b/templates/webpages/time_recording/form.html new file mode 100644 index 000000000..6d46067b6 --- /dev/null +++ b/templates/webpages/time_recording/form.html @@ -0,0 +1,41 @@ +[% USE L %] +[% USE P %] +[% USE T8 %] +[% USE LxERP %] + +

[% title %]

+ +[%- INCLUDE 'common/flash.html' %] + +
+ [% P.hidden_tag('id', SELF.time_recording.id) %] + [% L.hidden_tag('callback', FORM.callback) %] + + + + + + + + + + + + + + + + + + +
[% 'Start' | T8 %][% 'End' | T8 %][% 'Customer' | T8 %][% 'Type' | T8 %][% 'Project' | T8 %][% 'Description' | T8 %]
+ [% P.date_tag('start_date', SELF.start_date, "data-validate"="required", "data-title"=LxERP.t8('Start date'), onchange='kivi.TimeRecording.set_end_date()') %]
+ [% P.input_tag('start_time', SELF.start_time, type="time", "data-validate"="required", "data-title"=LxERP.t8('Start time')) %] + [% P.button_tag('kivi.TimeRecording.set_current_date_time("start")', LxERP.t8('now')) %] +
+ [% P.date_tag('end_date', SELF.end_date) %]
+ [% P.input_tag('end_time', SELF.end_time, type="time") %] + [% P.button_tag('kivi.TimeRecording.set_current_date_time("end")', LxERP.t8('now')) %] +
[% P.customer_vendor.picker('time_recording.customer_id', SELF.time_recording.customer_id, type='customer', style='width: 300px', "data-validate"="required", "data-title"=LxERP.t8('Customer')) %][% P.select_tag('time_recording.type_id', SELF.all_time_recording_types, default=SELF.time_recording.type.id, with_empty=1, title_key='abbreviation') %][% P.project.picker('time_recording.project_id', SELF.time_recording.project_id, style='width: 300px') %][% L.textarea_tag('time_recording.description', SELF.time_recording.description, wrap="soft", style="width: 350px; height: 150px", class="texteditor", "data-validate"="required", "data-title"=LxERP.t8('Description')) %]
+ +
diff --git a/templates/webpages/time_recording/report_bottom.html b/templates/webpages/time_recording/report_bottom.html new file mode 100644 index 000000000..a27818a5e --- /dev/null +++ b/templates/webpages/time_recording/report_bottom.html @@ -0,0 +1,9 @@ +[% USE HTML%] +[%- USE T8 %] +[%- USE L %][%- USE LxERP -%] + [% L.paginate_controls(models=SELF.models) %] + + [%- FOREACH item = HIDDEN %] + + [%- END %] + diff --git a/templates/webpages/time_recording/report_top.html b/templates/webpages/time_recording/report_top.html new file mode 100644 index 000000000..9541d808f --- /dev/null +++ b/templates/webpages/time_recording/report_top.html @@ -0,0 +1,2 @@ +[%- PROCESS 'time_recording/_filter.html' filter=SELF.models.filtered.laundered %] +