diff --git a/app/controllers/accounting_posts_controller.rb b/app/controllers/accounting_posts_controller.rb index cf11ec7c3..88a3015c8 100644 --- a/app/controllers/accounting_posts_controller.rb +++ b/app/controllers/accounting_posts_controller.rb @@ -14,7 +14,7 @@ class AccountingPostsController < CrudController self.permitted_attrs = [:closed, :offered_hours, :offered_rate, :offered_total, :remaining_hours, :portfolio_item_id, :service_id, :billable, :description_required, :ticket_required, :from_to_times_required, - :meal_compensation, + :meal_compensation, :billing_reminder_active, { work_item_attributes: %i[name shortname description] }] helper_method :order diff --git a/app/controllers/worktimes_controller.rb b/app/controllers/worktimes_controller.rb index c50710734..67c41d41a 100644 --- a/app/controllers/worktimes_controller.rb +++ b/app/controllers/worktimes_controller.rb @@ -13,6 +13,8 @@ class WorktimesController < CrudController before_save :check_has_accounting_post, :check_worktimes_committed after_save :check_overlapping, :check_employment + before_action :set_unbilled_worktimes_popup + before_render_index :set_statistics before_render_new :create_default_worktime before_render_form :set_existing @@ -177,6 +179,20 @@ def set_statistics @remaining_vacations = @user.statistics.current_remaining_vacations end + def set_unbilled_worktimes_popup + return if Time.zone.today.day < 10 # only show from 10. day of the month onwards to give time to publish / control times + + accounting_posts = @user.managed_orders + .collect(&:accounting_posts) + .flatten + .filter { |ap| ap.billing_reminder_active == true && ap.unbilled_billable_times_exist_in_past_month? } + + return if accounting_posts.blank? + + reminder_text = 'Vorsicht, in einem oder meheren deiner Aufträge gibt es noch unverrechnete Leistungen vom Vormonat! Überprüfe bitte das Verrechnungs-Controlling.' + flash[:warning] = reminder_text + end + # returns the employee's id from the params or the logged in user def employee_id if record_other? diff --git a/app/jobs/not_billed_times_reminder_job.rb b/app/jobs/not_billed_times_reminder_job.rb new file mode 100644 index 000000000..1bfa3ba67 --- /dev/null +++ b/app/jobs/not_billed_times_reminder_job.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +class NotBilledTimesReminderJob < CronJob + self.cron_expression = '0 5 10 * *' + + def perform + Employee.active_employed_last_month.each do |employee| + accounting_posts = employee.managed_orders + .collect(&:accounting_posts) + .flatten + .filter { |ap| ap.billing_reminder_active == true } + EmployeeMailer.not_billed_times_reminder_mail(employee).deliver_now if accounting_posts.any?(&:unbilled_billable_times_exist_in_past_month?) + end + end +end diff --git a/app/mailers/employee_mailer.rb b/app/mailers/employee_mailer.rb index a58d0832e..f98bfd0f6 100644 --- a/app/mailers/employee_mailer.rb +++ b/app/mailers/employee_mailer.rb @@ -17,4 +17,13 @@ def worktime_commit_reminder_mail(employee) subject: 'PuzzleTime Zeiten freigeben' ) end + + def not_billed_times_reminder_mail(employee) + @employee = employee + + mail( + to: "#{employee.firstname} #{employee.lastname} <#{employee.email}>", + subject: 'Erinnerung: Noch nicht verrechnete PuzzleTime Zeiten' + ) + end end diff --git a/app/models/accounting_post.rb b/app/models/accounting_post.rb index aff2f9037..80a485922 100644 --- a/app/models/accounting_post.rb +++ b/app/models/accounting_post.rb @@ -102,6 +102,14 @@ def propagate_closed! work_item.propagate_closed!(order.status.closed? || closed?) end + def unbilled_billable_times_exist_in_past_month? + work_item.worktimes + .in_period(Period.parse('-1m')) + .where(billable: true) + .where(invoice_id: nil) + .present? + end + private def derive_offered_fields diff --git a/app/views/accounting_posts/_form.html.haml b/app/views/accounting_posts/_form.html.haml index f0db1aa5f..06edebc01 100644 --- a/app/views/accounting_posts/_form.html.haml +++ b/app/views/accounting_posts/_form.html.haml @@ -44,3 +44,4 @@ .col-md-2 .help-block.col-md-5 Die Einstellungen zur Beschreibung, Ticket oder Von-Bis-Zeiten können nicht geändert werden, da bereits Leistungen ohne diese Angaben erfasst wurden. + = f.labeled_input_field :billing_reminder_active, html_options = {caption: 'Erinnerung bei unverrechneten Leistungen senden', title: "Am 10. Tag des Monats wird eine Mail an den/die Auftragsverantwortliche:n gesendet, wenn im Vormonat verrechenbare Leistungen auf diese Position gebucht wurden, die keiner Rechnung zugewiesen sind", data: {toggle: :tooltip}} diff --git a/app/views/employee_mailer/not_billed_times_reminder_mail.html.haml b/app/views/employee_mailer/not_billed_times_reminder_mail.html.haml new file mode 100644 index 000000000..aec2bd0b0 --- /dev/null +++ b/app/views/employee_mailer/not_billed_times_reminder_mail.html.haml @@ -0,0 +1,16 @@ +%h1.h3{:style => "box-sizing: border-box; margin: 0.67em 0; font-family: inherit; font-weight: 400; line-height: 1.1; color: inherit; margin-top: 20px; font-size: 24px; margin-bottom: 20px;"} + Hallo + = @employee.firstname +%div{:style => "box-sizing: border-box;"} + .lead + Beim einem der Aufträge, bei welchen du Auftragsverantwortliche:r bist, wurden im vergangenen Monat verrechenbare Leistungen gebucht, welche noch keiner Rechnung zugeteilt wurden. + %br + Bitte überprüfe die Leistungen im + = link_to 'Verrechnungs-Controlling', root_url + %br + .lead + Liebe Grüsse + %br + Dein PuzzleTime + %br + Möchtest du zu einer Buchungsposition künftig keine Erinnerungsmail mehr erhalten, deaktiviere in den Einstellungen der Position die Checkbox "Erinnerung bei unverrechneten Leistungen senden". \ No newline at end of file diff --git a/app/views/employee_mailer/not_billed_times_reminder_mail.text.erb b/app/views/employee_mailer/not_billed_times_reminder_mail.text.erb new file mode 100644 index 000000000..0ef931735 --- /dev/null +++ b/app/views/employee_mailer/not_billed_times_reminder_mail.text.erb @@ -0,0 +1,10 @@ +Hallo <%= @employee.firstname %> + +Beim einem der Aufträge, bei welchen du Auftragsverantwortliche:r bist, wurden im vergangenen Monat verrechenbare Leistungen gebucht, welche noch keiner Rechnung zugeteilt wurden. + +Bitte überprüfe die Leistungen im 'Verrechnungs-Controlling' im PuzzleTime. + +Möchtest du zu einer Buchungsposition künftig keine Erinnerungsmail mehr erhalten, deaktiviere in den Einstellungen der Position die Checkbox "Erinnerung bei unverrechneten Leistungen senden". + +Liebe Grüsse +Dein PuzzleTime \ No newline at end of file diff --git a/db/migrate/20250501075712_add_billing_reminder_active_to_accounting_posts.rb b/db/migrate/20250501075712_add_billing_reminder_active_to_accounting_posts.rb new file mode 100644 index 000000000..d164c8789 --- /dev/null +++ b/db/migrate/20250501075712_add_billing_reminder_active_to_accounting_posts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddBillingReminderActiveToAccountingPosts < ActiveRecord::Migration[7.1] + def change + add_column :accounting_posts, :billing_reminder_active, :boolean, default: true, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 1bd87b11b..9ed7f64f0 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: 2025_02_19_080518) do +ActiveRecord::Schema[7.1].define(version: 2025_05_01_075712) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -35,6 +35,7 @@ t.boolean "closed", default: false, null: false t.integer "service_id" t.boolean "meal_compensation", default: false, null: false + t.boolean "billing_reminder_active", default: true, null: false t.index ["portfolio_item_id"], name: "index_accounting_posts_on_portfolio_item_id" t.index ["service_id"], name: "index_accounting_posts_on_service_id" t.index ["work_item_id"], name: "index_accounting_posts_on_work_item_id" diff --git a/test/mailers/employee_mailer_test.rb b/test/mailers/employee_mailer_test.rb new file mode 100644 index 000000000..e3a48932a --- /dev/null +++ b/test/mailers/employee_mailer_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'test_helper' + +class EmployeeMailerTest < ActionMailer::TestCase + attr_reader :order, :accounting_post1, :accounting_post2 + + setup do + @order = Fabricate(:order, + responsible: employees(:next_year_pablo)) + @accounting_post1 = Fabricate(:accounting_post, + work_item: Fabricate(:work_item, parent_id: order.work_item_id), + offered_hours: 100, + offered_rate: 100, + billing_reminder_active: true) + @accounting_post2 = Fabricate(:accounting_post, + work_item: Fabricate(:work_item, parent_id: order.work_item_id), + offered_hours: 100, + offered_rate: 0, + billing_reminder_active: false) + end + + test 'sends a reminder for an order responsible with active employment' do + order_responsible = employees(:next_year_pablo) + Fabricate(:ordertime, work_item: accounting_post1.work_item, employee: employees(:long_time_john), hours: 5, billable: true, work_date: Period.parse('-1m').end_date) + + assert_emails 1 do + NotBilledTimesReminderJob.new.perform + end + mail = ActionMailer::Base.deliveries.last + + assert_equal [order_responsible.email], mail.to + end + + test 'setting `billing_reminder_active: false` deactivates mails for an accounting_post' do + Fabricate(:ordertime, work_item: accounting_post2.work_item, employee: employees(:long_time_john), hours: 7, billable: true, work_date: Period.parse('-1m').end_date) + + assert_emails 0 do + NotBilledTimesReminderJob.new.perform + end + end +end diff --git a/test/mailers/previews/employee_mailer_preview.rb b/test/mailers/previews/employee_mailer_preview.rb index afbe648eb..7df6c2c4d 100644 --- a/test/mailers/previews/employee_mailer_preview.rb +++ b/test/mailers/previews/employee_mailer_preview.rb @@ -18,4 +18,9 @@ def worktime_commit_reminder_mail employee = Employee.new(email: 'user@example.com', firstname: 'Peter', lastname: 'Puzzler') EmployeeMailer.worktime_commit_reminder_mail(employee) end + + def not_billed_times_reminder_mail + employee = Employee.new(email: 'user@example.com', firstname: 'Peter', lastname: 'Puzzler') + EmployeeMailer.not_billed_times_reminder_mail(employee) + end end