From bc67990d626b0abc48b101c09aa41c0320dd68fa Mon Sep 17 00:00:00 2001 From: Philipp Date: Sun, 1 Dec 2024 15:55:17 +0100 Subject: [PATCH] Support multiday absencs for part time employees --- Gemfile | 1 + Gemfile.lock | 6 +++ app/controllers/absences_controller.rb | 4 ++ app/models/absence.rb | 22 ++++++++- app/services/absences_service.rb | 11 +++++ app/views/absences/index.html.erb | 2 +- ...241201132737_add_hours_for_all_absences.rb | 10 +++++ db/schema.rb | 2 +- spec/controllers/absences_controller_spec.rb | 45 +++++++++++++++++++ spec/features/hours_spec.rb | 4 +- spec/models/absences_spec.rb | 24 ++-------- .../activities/report/absences_cube_spec.rb | 2 + .../models/hours/calendar_day_factory_spec.rb | 2 +- spec/models/hours/calendar_day_spec.rb | 23 +--------- 14 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 app/services/absences_service.rb create mode 100644 db/migrate/20241201132737_add_hours_for_all_absences.rb diff --git a/Gemfile b/Gemfile index a6bbb1d0..90a7b790 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,7 @@ group :development do end group :development, :test do + gem 'pry' gem 'awesome_print' gem 'factory_bot_rails' gem 'i18n-tasks' diff --git a/Gemfile.lock b/Gemfile.lock index fc918b44..4fb63f4c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -111,6 +111,7 @@ GEM chunky_png (1.4.0) classy_cancan (0.0.3) cancancan + coderay (1.1.3) coffee-rails (5.0.0) coffee-script (>= 2.2.0) railties (>= 5.2.0) @@ -210,6 +211,7 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + method_source (1.1.0) mini_mime (1.1.5) mini_portile2 (2.8.7) minitest (5.23.1) @@ -246,6 +248,9 @@ GEM ast (~> 2.4.1) racc pg (1.5.6) + pry (0.15.0) + coderay (~> 1.1) + method_source (~> 1.0) psych (5.1.2) stringio public_suffix (5.0.5) @@ -465,6 +470,7 @@ DEPENDENCIES money-rails paper_trail pg + pry puma rails (~> 7.0) rails-controller-testing diff --git a/app/controllers/absences_controller.rb b/app/controllers/absences_controller.rb index 0c0a54b1..708ccfbb 100644 --- a/app/controllers/absences_controller.rb +++ b/app/controllers/absences_controller.rb @@ -19,6 +19,10 @@ def new end def create + if @absence.hours.nil? + @absence.hours = AbsencesService.new.calculate_default_hours(@absence) + end + @absence.save respond_with @absence, location: absences_path end diff --git a/app/models/absence.rb b/app/models/absence.rb index 9afcd051..04753f69 100644 --- a/app/models/absence.rb +++ b/app/models/absence.rb @@ -4,7 +4,7 @@ class Absence < ActiveRecord::Base belongs_to :employee validates :employee, :reason, :from_date, :to_date, presence: true - validates :hours, absence: true, unless: :one_day_absence? + validates :hours, presence: true validate :to_after_from enumerize :reason, in: { holidays: 0, doctor: 1, funeral: 2, disease: 3, @@ -17,8 +17,26 @@ class Absence < ActiveRecord::Base def absent_target_hours(from: from_date, to: to_date) return hours if one_day_absence? + # we can return the full hours if the requested range covers the full absence + if from <= from_date && to >= to_date + return hours + end + + # if the requested range does not cover the full range of the absence, it's getting compicated... + start_date = [from_date, from].max + end_date = [to, to_date].min + + # calculate the absent target hours for the full absence + absent_target_hours_of_full_absence = TargetHours.hours_between(from: from_date, to: to_date) + + # and the absent target hours in the requested range + absent_target_hours_within_requested_range = TargetHours.hours_between(from: start_date, to: end_date) + + # calculate a hours per target hours ratio + absent_hours_per_target_hours = absent_target_hours_of_full_absence / hours - TargetHours.hours_between(from: [from_date, from].max, to: [to, to_date].min) + # and multiply this by the absent target hours with in the range + absent_hours_per_target_hours * absent_target_hours_within_requested_range end def to_after_from diff --git a/app/services/absences_service.rb b/app/services/absences_service.rb new file mode 100644 index 00000000..30ecb3e8 --- /dev/null +++ b/app/services/absences_service.rb @@ -0,0 +1,11 @@ +class AbsencesService + def calculate_default_hours(absence) + if absence.from_date.present? && absence.to_date.present? + if absence.from_date.monday? && absence.to_date.friday? + TargetHours.hours_between_for_employee(from: absence.from_date, to: absence.to_date, employee: absence.employee) + else + TargetHours.hours_between(from: absence.from_date, to: absence.to_date) + end + end + end +end diff --git a/app/views/absences/index.html.erb b/app/views/absences/index.html.erb index 695477f6..af4d9972 100644 --- a/app/views/absences/index.html.erb +++ b/app/views/absences/index.html.erb @@ -19,7 +19,7 @@ <%= absence.employee.name %> <%= present(absence) { |a| a.date } %> - <%= absence.absent_target_hours %> + <%= absence.hours %> <%= absence.reason.text %> <%= absence.text %> <%= link_to_edit absence %> diff --git a/db/migrate/20241201132737_add_hours_for_all_absences.rb b/db/migrate/20241201132737_add_hours_for_all_absences.rb new file mode 100644 index 00000000..c9b82ff8 --- /dev/null +++ b/db/migrate/20241201132737_add_hours_for_all_absences.rb @@ -0,0 +1,10 @@ +class AddHoursForAllAbsences < ActiveRecord::Migration[7.1] + def change + Absence.where(hours: nil).each do |absence| + absence.update! hours: TargetHours.hours_between( + from: absence.from_date, + to: absence.to_date + ) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e2c2a3bf..08bcf358 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_06_24_060025) do +ActiveRecord::Schema[7.1].define(version: 2024_12_01_132737) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/controllers/absences_controller_spec.rb b/spec/controllers/absences_controller_spec.rb index 52533cc1..0151dd81 100644 --- a/spec/controllers/absences_controller_spec.rb +++ b/spec/controllers/absences_controller_spec.rb @@ -41,6 +41,51 @@ post :create, params: { absence: attributes } end.to change(Absence, :count).by(1) end + + # these tests should be in the serivce specs, but I've no time left now... + context 'for part time employee' do + before do + employee.update! workload: 50 + end + + context 'for a full week' do + let(:attributes) do + { + employee_id: employee.id, + from_date: '2024-12-02', + to_date: '2024-12-06', + reason: :holidays + } + end + + it 'creates a new absence which takes workload into account' do + expect do + post :create, params: { absence: attributes } + end.to change(Absence, :count).by(1) + + expect(Absence.last.hours).to eq(20) + end + end + + context 'for a part of the week' do + let(:attributes) do + { + employee_id: employee.id, + from_date: '2024-12-02', + to_date: '2024-12-03', + reason: :holidays + } + end + + it 'creates a new absence which does not take workload into account' do + expect do + post :create, params: { absence: attributes } + end.to change(Absence, :count).by(1) + + expect(Absence.last.hours).to eq(16) + end + end + end end describe 'GET #edit' do diff --git a/spec/features/hours_spec.rb b/spec/features/hours_spec.rb index a0c60c7d..26a65e30 100644 --- a/spec/features/hours_spec.rb +++ b/spec/features/hours_spec.rb @@ -20,7 +20,7 @@ create :target_hours, date: '2017-04-17', hours: 4 create :absence, :default_associations, from_date: '2017-04-17', to_date: '2017-04-21', - hours: nil + hours: 36 create :absence, :default_associations, from_date: '2017-04-6', to_date: '2017-04-6', hours: 4 @@ -30,7 +30,7 @@ create :activity, :default_associations, date: 1.month.ago, hours: 10 create :absence, :default_associations, from_date: '2017-03-27', to_date: '2017-03-28', - hours: nil + hours: 16 create :absence, :default_associations, from_date: '2017-03-29', to_date: '2017-03-29', hours: 8 diff --git a/spec/models/absences_spec.rb b/spec/models/absences_spec.rb index ae94e9d5..a843ffcc 100644 --- a/spec/models/absences_spec.rb +++ b/spec/models/absences_spec.rb @@ -4,13 +4,7 @@ describe 'validations' do context 'with absence spanning mutliple days' do it 'is invalid when to-date after from-date' do - absence = build :absence, from_date: 2.days.from_now, to_date: 1.day.ago - - expect(absence.valid?).to be false - end - - it 'is invalid when hours not nil' do - absence = build :absence, from_date: 2.day.ago, to_date: 1.day.ago, hours: 3 + absence = build :absence, hours: 8, from_date: 2.days.from_now, to_date: 1.day.ago expect(absence.valid?).to be false end @@ -44,22 +38,12 @@ context 'absence spanning multiple days' do let(:absence) do - build :absence, from_date: '2016-12-12', to_date: '2016-12-13' + build :absence, hours: 16, from_date: '2016-12-12', to_date: '2016-12-13' end it 'returns correct absent target hours' do expect(subject).to eq 16 end - - context 'overlapping target hours' do - before do - create :target_hours, date: '2016-12-12', hours: 4 - end - - it 'considers target_hours' do - expect(subject).to eq 12 - end - end end describe 'limiting date range' do @@ -70,7 +54,7 @@ let(:range) { { from: Date.parse('2016-12-01'), to: Date.parse('2016-12-05') } } let(:absence) do - build :absence, from_date: '2016-11-30', to_date: '2016-12-06' + build :absence, hours: 40, from_date: '2016-11-30', to_date: '2016-12-06' end it 'only returns value between limited date range' do @@ -91,7 +75,7 @@ subject { described_class.between(**range) } let!(:absence) do - create :absence, hours: nil, from_date: '2016-11-30', to_date: '2016-12-06' + create :absence, hours: 40, from_date: '2016-11-30', to_date: '2016-12-06' end context 'with not covering range' do diff --git a/spec/models/activities/report/absences_cube_spec.rb b/spec/models/activities/report/absences_cube_spec.rb index 96cda999..b73f04b5 100644 --- a/spec/models/activities/report/absences_cube_spec.rb +++ b/spec/models/activities/report/absences_cube_spec.rb @@ -16,6 +16,7 @@ build(:absence, employee:, from_date: (date - 1.day), to_date: date, + hours: 16, reason: :holidays), build(:absence, employee: other_employee, @@ -46,6 +47,7 @@ build(:absence, employee:, from_date: (date.beginning_of_month - 2.day), to_date: (date.beginning_of_month + 2.day), + hours: 32, reason: :doctor), ] end diff --git a/spec/models/hours/calendar_day_factory_spec.rb b/spec/models/hours/calendar_day_factory_spec.rb index fc03524a..b5fe0f3f 100644 --- a/spec/models/hours/calendar_day_factory_spec.rb +++ b/spec/models/hours/calendar_day_factory_spec.rb @@ -46,7 +46,7 @@ context 'with spanning absence' do let!(:absence) do - create :absence, employee:, from_date: '2016-12-01', to_date: '2016-12-02', hours: nil + create :absence, employee:, from_date: '2016-12-01', to_date: '2016-12-02', hours: 16 end it 'assigns absence to relevant days' do diff --git a/spec/models/hours/calendar_day_spec.rb b/spec/models/hours/calendar_day_spec.rb index b73ea03f..01c787a1 100644 --- a/spec/models/hours/calendar_day_spec.rb +++ b/spec/models/hours/calendar_day_spec.rb @@ -115,7 +115,7 @@ [ create(:absence, :default_associations, from_date: date, to_date: date + 1.day, - hours: nil), + hours: 16), ] end @@ -123,27 +123,6 @@ expect(absence_event.date).to eq date expect(absence_event.hours).to eq 8 end - - context 'and target hours' do - before do - create :target_hours, date:, hours: 6 - end - - it 'considers target hours' do - expect(absence_event.date).to eq date - expect(absence_event.hours).to eq 6 - end - end - - context 'if target hours are zero' do - before do - create :target_hours, date:, hours: 0 - end - - it 'does not create an event' do - expect(absence_event).to eq nil - end - end end context 'without absences' do