Zeiterfassung: Datum/Dauer statt Start/Ende wählbar (Benutzereinstellung)
authorBernd Bleßmann <bernd@kivitendo-premium.de>
Wed, 28 Apr 2021 12:33:19 +0000 (14:33 +0200)
committerBernd Bleßmann <bernd@kivitendo-premium.de>
Wed, 5 May 2021 15:25:03 +0000 (17:25 +0200)
SL/AM.pm
SL/Controller/TimeRecording.pm
SL/DB/TimeRecording.pm
SL/Helper/UserPreferences/TimeRecording.pm [new file with mode: 0644]
bin/mozilla/am.pl
locale/de/all
locale/en/all
templates/webpages/am/config.html
templates/webpages/time_recording/form.html

index b7c6003..57e6b44 100644 (file)
--- a/SL/AM.pm
+++ b/SL/AM.pm
@@ -54,6 +54,7 @@ use SL::DB;
 use SL::GenericTranslations;
 use SL::Helper::UserPreferences::PositionsScrollbar;
 use SL::Helper::UserPreferences::PartPickerSearch;
+use SL::Helper::UserPreferences::TimeRecording;
 use SL::Helper::UserPreferences::UpdatePositions;
 
 use strict;
@@ -546,6 +547,10 @@ sub positions_show_update_button {
   SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
 }
 
+sub time_recording_use_duration {
+  SL::Helper::UserPreferences::TimeRecording->new()->get_use_duration();
+}
+
 sub save_preferences {
   $main::lxdebug->enter_sub();
 
@@ -583,6 +588,9 @@ sub save_preferences {
   if (exists $form->{positions_show_update_button}) {
     SL::Helper::UserPreferences::UpdatePositions->new()->store_show_update_button($form->{positions_show_update_button})
   }
+  if (exists $form->{time_recording_use_duration}) {
+    SL::Helper::UserPreferences::TimeRecording->new()->store_use_duration($form->{time_recording_use_duration})
+  }
 
   $main::lxdebug->leave_sub();
 
index 8215122..ed1d9b9 100644 (file)
@@ -14,13 +14,15 @@ use SL::DB::Employee;
 use SL::DB::Part;
 use SL::DB::TimeRecording;
 use SL::DB::TimeRecordingArticle;
+use SL::Helper::Flash qw(flash);
+use SL::Helper::UserPreferences::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_employees all_time_recording_articles can_view_all can_edit_all) ],
+ 'scalar --get_set_init' => [ qw(time_recording models all_employees all_time_recording_articles can_view_all can_edit_all use_duration) ],
 );
 
 
@@ -65,6 +67,12 @@ sub action_edit {
 
   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.TimeRecording ckeditor/ckeditor ckeditor/adapters/jquery kivi.Validator);
 
+  if ($self->use_duration) {
+    flash('warning', t8('This entry is using start and end time. This information will be overwritten on saving.')) if !$self->time_recording->is_duration_used;
+  } else {
+    flash('warning', t8('This entry is using date and duration. This information will be overwritten on saving.'))  if $self->time_recording->is_duration_used;
+  }
+
   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;
@@ -84,6 +92,11 @@ sub action_edit {
 sub action_save {
   my ($self) = @_;
 
+  if ($self->use_duration) {
+    $self->time_recording->start_date(undef);
+    $self->time_recording->end_date(undef);
+  }
+
   my @errors = $self->time_recording->validate;
   if (@errors) {
     $::form->error(t8('Saving the time recording entry failed: #1', join '<br>', @errors));
@@ -107,25 +120,30 @@ sub action_delete {
 }
 
 sub init_time_recording {
+  my ($self) = @_;
+
   my $is_new         = !$::form->{id};
-  my $time_recording = $is_new ? SL::DB::TimeRecording->new(start_time => DateTime->now_local)
-                               : SL::DB::TimeRecording->new(id => $::form->{id})->load;
+  my $time_recording = !$is_new            ? SL::DB::TimeRecording->new(id => $::form->{id})->load
+                     : $self->use_duration ? SL::DB::TimeRecording->new(date => DateTime->today_local)
+                     :                       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;
+  if (!$self->use_duration) {
+    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;
+        }
       }
     }
   }
 
-  # do not overwright staff member if you do not have the right
+  # do not overwrite staff member if you do not have the right
   delete $attributes{staff_member_id} if !$_[0]->can_edit_all;
   $attributes{staff_member_id} = SL::DB::Manager::Employee->current->id if $is_new;
 
@@ -178,6 +196,10 @@ sub init_all_time_recording_articles {
   return $res;
 }
 
+sub init_use_duration {
+  return SL::Helper::UserPreferences::TimeRecording->new()->get_use_duration();
+}
+
 sub check_auth {
   $::auth->assert('time_recording');
 }
@@ -255,8 +277,8 @@ sub make_filter_summary {
   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->{"date:date::ge"},                              t8('From Date')      ],
+    [ $filter->{"date:date::le"},                              t8('To Date')        ],
     [ $filter->{"customer"}->{"name:substr::ilike"},           t8('Customer')        ],
     [ $filter->{"customer"}->{"customernumber:substr::ilike"}, t8('Customer Number') ],
     [ $staff_member,                                           t8('Mitarbeiter')     ],
index 62143b3..9b1a2e3 100644 (file)
@@ -33,7 +33,6 @@ sub validate {
 
   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;
@@ -109,6 +108,10 @@ sub is_time_in_wrong_order {
   return;
 }
 
+sub is_duration_used {
+  return !$_[0]->start_time;
+}
+
 sub displayable_times {
   my ($self) = @_;
 
diff --git a/SL/Helper/UserPreferences/TimeRecording.pm b/SL/Helper/UserPreferences/TimeRecording.pm
new file mode 100644 (file)
index 0000000..7686a7c
--- /dev/null
@@ -0,0 +1,69 @@
+package SL::Helper::UserPreferences::TimeRecording;
+
+use strict;
+use parent qw(Rose::Object);
+
+use Carp;
+use List::MoreUtils qw(none);
+
+use SL::Helper::UserPreferences;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(user_prefs) ],
+);
+
+sub get_use_duration {
+  !!$_[0]->user_prefs->get('use_duration');
+}
+
+sub store_use_duration {
+  $_[0]->user_prefs->store('use_duration', $_[1]);
+}
+
+sub init_user_prefs {
+  SL::Helper::UserPreferences->new(
+    namespace => $_[0]->namespace,
+  )
+}
+
+# read only stuff
+sub namespace     { 'TimeRecording' }
+sub version       { 1 }
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Helper::UserPreferences::TimeRecording - preferences intended
+to store user settings for using the time recording functionality.
+
+=head1 SYNOPSIS
+
+  use SL::Helper::UserPreferences::TimeRecording;
+  my $prefs = SL::Helper::UserPreferences::TimeRecording->new();
+
+  $prefs->store_use_duration(1);
+  my $value = $prefs->get_use_duration;
+
+=head1 DESCRIPTION
+
+This module manages storing the user's choise for settings for
+the time recording controller.
+For now it can be choosen if an entry is done by entering start and
+end time or a date and a duration.
+
+=head1 BUGS
+
+None yet :)
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut
index d041561..0b35fc7 100644 (file)
@@ -664,6 +664,7 @@ sub config {
   $form->{purchase_search_makemodel}        = AM->purchase_search_makemodel();
   $form->{sales_search_customer_partnumber} = AM->sales_search_customer_partnumber();
   $form->{positions_show_update_button}     = AM->positions_show_update_button();
+  $form->{time_recording_use_duration}      = AM->time_recording_use_duration();
 
   $myconfig{show_form_details} = 1 unless (defined($myconfig{show_form_details}));
   $form->{CAN_CHANGE_PASSWORD} = $main::auth->can_change_password();
index 8b28550..f88cbf5 100755 (executable)
@@ -1542,7 +1542,6 @@ $self->{texts} = {
   'Fristsetzung'                => 'Fristsetzung',
   'From'                        => 'Von',
   'From Date'                   => 'Von',
-  'From Start'                  => 'Ab Start',
   'From bin'                    => 'Ausgelagert',
   'From shop "#1" :  #2 '       => 'Shop #1 : #2',
   'From shop #1 :  #2 shoporders have been fetched.' => 'Es wurden #2 Bestellungen von #1 geholt.',
@@ -3137,7 +3136,6 @@ $self->{texts} = {
   'Start the correction assistant' => 'Korrekturassistenten starten',
   'Start time'                  => 'Startzeit',
   'Start time must be earlier than end time.' => 'Startzeit muss vor der Endzeit liegen.',
-  'Start time must not be empty.' => 'Startzeit darf nicht leer sein.',
   'Startdate method'            => 'Methode zur Ermittlung des Startdatums',
   'Startdate_coa'               => 'Gültig ab',
   'Starting Balance'            => 'Eröffnungsbilanzwerte',
@@ -3694,6 +3692,8 @@ $self->{texts} = {
   'This discount is only valid in purchase documents' => 'Dieser Rabatt ist nur in Einkaufsdokumenten gültig',
   'This discount is only valid in records with customer or vendor' => 'Dieser Rabatt ist nur in Dokumenten mit Kunde oder Lieferant gültig',
   'This discount is only valid in sales documents' => 'Dieser Rabatt ist nur in Verkaufsdokumenten gültig',
+  'This entry is using date and duration. This information will be overwritten on saving.' => 'Dieser Eintrag verwendet Datum und Dauer. Diese Information wird beim Speichern überschrieben.',
+  'This entry is using start and end time. This information will be overwritten on saving.' => 'Dieser Eintrag verwendet Start- und End-Zeit. Diese Information wird beim Speichern überschrieben.',
   'This export will include all records in the given time range and all supplicant information from checked entities. You will receive a single zip file. Please extract this file onto the data medium requested by your auditor.' => 'Dieser Export umfasst alle Belege im gewählten Zeitrahmen und die dazugehörgen Informationen aus den gewählten Blöcken. Sie erhalten eine einzelne Zip-Datei. Bitte entpacken Sie diese auf das Medium das Ihr Steuerprüfer wünscht.',
   'This feature especially prevents mistakes by mixing up prior tax and sales tax.' => 'Dieses Feature vermeidet insbesondere Verwechslungen von Umsatz- und Vorsteuer.',
   'This field must not be empty.' => 'Dieses Feld darf nicht leer sein.',
@@ -3770,7 +3770,6 @@ $self->{texts} = {
   'To (email)'                  => 'An',
   'To (time)'                   => 'Bis',
   'To Date'                     => 'Bis',
-  'To Start'                    => 'Bis Start',
   'To continue please change the taxkey 0 to another value.' => 'Um fortzufahren, ändern Sie bitte den Steuerschlüssel 0 auf einen anderen Wert.',
   'To import'                   => 'Zu importieren',
   'To upload images: Please create shoppart first' => 'Um Bilder hochzuladen bitte Shopartikel zuerst anlegen',
@@ -3945,6 +3944,7 @@ $self->{texts} = {
   'Use a text field to enter (new) contact titles if enabled. Otherwise, only a drop down box is offered.' => 'Textfeld zusätzlich zur Eingabe (neuer) Titel von Ansprechpersonen verwenden. Sonst wird nur eine Auswahlliste angezeigt.',
   'Use a text field to enter (new) greetings if enabled. Otherwise, only a drop down box is offered.' => 'Textfeld zusätzlich zur Eingabe (neuer) Anreden verwenden. Sonst wird nur eine Auswahlliste angezeigt.',
   'Use as new'                  => 'Als neu verwenden',
+  'Use date and duration for time recordings' => 'Datum und Dauer für Zeiterfassung verwenden',
   'Use default booking group because setting is \'all\'' => 'Standardbuchungsgruppe wird verwendet',
   'Use default booking group because wanted is missing' => 'Fehlende Buchungsgruppe, deshalb Standardbuchungsgruppe',
   'Use default warehouse for assembly transfer' => 'Zum Fertigen Standardlager des Bestandteils verwenden',
@@ -4269,6 +4269,7 @@ $self->{texts} = {
   'http'                        => 'http',
   'https'                       => 'https',
   'imported'                    => 'Importiert',
+  'in minutes'                  => 'in Minuten',
   'inactive'                    => 'inaktiv',
   'income'                      => 'Einnahmen-Überschuß-Rechnung',
   'internal error (see details)' => 'Interner Fehler (siehe Details)!',
index 4921c29..1f5bfea 100644 (file)
@@ -1542,7 +1542,6 @@ $self->{texts} = {
   'Fristsetzung'                => '',
   'From'                        => '',
   'From Date'                   => '',
-  'From Start'                  => '',
   'From bin'                    => '',
   'From shop "#1" :  #2 '       => '',
   'From shop #1 :  #2 shoporders have been fetched.' => '',
@@ -3136,7 +3135,6 @@ $self->{texts} = {
   'Start the correction assistant' => '',
   'Start time'                  => '',
   'Start time must be earlier than end time.' => '',
-  'Start time must not be empty.' => '',
   'Startdate method'            => '',
   'Startdate_coa'               => '',
   'Starting Balance'            => '',
@@ -3692,6 +3690,8 @@ $self->{texts} = {
   'This discount is only valid in purchase documents' => '',
   'This discount is only valid in records with customer or vendor' => '',
   'This discount is only valid in sales documents' => '',
+  'This entry is using date and duration. This information will be overwritten on saving.' => '',
+  'This entry is using start and end time. This information will be overwritten on saving.' => '',
   'This export will include all records in the given time range and all supplicant information from checked entities. You will receive a single zip file. Please extract this file onto the data medium requested by your auditor.' => '',
   'This feature especially prevents mistakes by mixing up prior tax and sales tax.' => '',
   'This field must not be empty.' => '',
@@ -3768,7 +3768,6 @@ $self->{texts} = {
   'To (email)'                  => '',
   'To (time)'                   => '',
   'To Date'                     => '',
-  'To Start'                    => '',
   'To continue please change the taxkey 0 to another value.' => '',
   'To import'                   => '',
   'To upload images: Please create shoppart first' => '',
@@ -3943,6 +3942,7 @@ $self->{texts} = {
   'Use a text field to enter (new) contact titles if enabled. Otherwise, only a drop down box is offered.' => '',
   'Use a text field to enter (new) greetings if enabled. Otherwise, only a drop down box is offered.' => '',
   'Use as new'                  => '',
+  'Use date and duration for time recordings' => '',
   'Use default booking group because setting is \'all\'' => '',
   'Use default booking group because wanted is missing' => '',
   'Use default warehouse for assembly transfer' => '',
@@ -4267,6 +4267,7 @@ $self->{texts} = {
   'http'                        => '',
   'https'                       => '',
   'imported'                    => '',
+  'in minutes'                  => '',
   'inactive'                    => '',
   'income'                      => 'GUV and BWA',
   'internal error (see details)' => '',
index 6e65c4d..6901cba 100644 (file)
         </td>
       </tr>
 
+     <tr>
+      <th align="right">[% 'Use date and duration for time recordings' | $T8 %]</th>
+      <td>
+        [% L.yes_no_tag('time_recording_use_duration', time_recording_use_duration) %]
+      </td>
+     </tr>
+
     </table>
    </div>
 
index 32207b5..99e9c0f 100644 (file)
   <table>
     <thead class="listheading">
       <tr>
+       [%- IF SELF.use_duration %]
+        <th>[% 'Date'     | $T8 %]</th>
+        <th>[% 'Duration' | $T8 %] ([% 'in minutes' | $T8 %])</th>
+       [%- ELSE %]
         <th>[% 'Start' | $T8 %]</th>
-        <th>[% 'End' | $T8 %]</th>
+        <th>[% 'End'   | $T8 %]</th>
+       [%- END %]
         <th>[% 'Customer' | $T8 %]</th>
         <th>[% 'Article' | $T8 %]</th>
         <th>[% 'Project' | $T8 %]</th>
     </thead>
     <tbody valign="top">
       <tr>
+       [%- IF SELF.use_duration %]
+        <td>
+          [% P.date_tag('time_recording.date_as_date', SELF.time_recording.date_as_date, "data-validate"="required", "data-title"=LxERP.t8('Date')) %]<br>
+        </td>
+        <td>
+          [% P.input_tag('time_recording.duration', SELF.time_recording.duration, size=15) %]
+        </td>
+       [%- ELSE %]
         <td>
           [% P.date_tag('start_date',  SELF.start_date, "data-validate"="required", "data-title"=LxERP.t8('Start date'), onchange='kivi.TimeRecording.set_end_date()') %]<br>
           [% P.input_tag('start_time', SELF.start_time, type="time", "data-validate"="required", "data-title"=LxERP.t8('Start time')) %]
@@ -36,6 +49,7 @@
           [% P.input_tag('end_time', SELF.end_time, type="time") %]
           [% P.button_tag('kivi.TimeRecording.set_current_date_time("end")', LxERP.t8('now')) %]
         </td>
+       [%- END %]
         <td>[% 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')) %]</td>
         <td>[% P.select_tag('time_recording.part_id', SELF.all_time_recording_articles, default=SELF.time_recording.part_id, with_empty=1, value_key='id', title_key='description') %]</td>
         <td>[% P.project.picker('time_recording.project_id', SELF.time_recording.project_id, style='width: 300px') %]</td>