Skip to content

Commit c564b25

Browse files
committed
Fix not invoiced order mail (64086)
1 parent f278655 commit c564b25

File tree

10 files changed

+109
-90
lines changed

10 files changed

+109
-90
lines changed
Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
# Copyright (c) 2006-2022, Puzzle ITC GmbH. This file is part of
3+
# Copyright (c) 2006-2025, Puzzle ITC GmbH. This file is part of
44
# PuzzleTime and licensed under the Affero General Public License version 3
55
# or later. See the COPYING file at the top-level directory or at
66
# https://github.com/puzzle/puzzletime.
@@ -9,12 +9,23 @@ class NotBilledTimesReminderJob < CronJob
99
self.cron_expression = '0 5 10 * *'
1010

1111
def perform
12-
Employee.active_employed_last_month.each do |employee|
13-
accounting_posts = employee.managed_orders
14-
.collect(&:accounting_posts)
15-
.flatten
16-
.filter { |ap| ap.billing_reminder_active == true }
17-
EmployeeMailer.not_billed_times_reminder_mail(employee).deliver_now if accounting_posts.any?(&:unbilled_billable_times_exist_in_past_month?)
12+
responsible_employees_with_not_billed_times_last_month.each do |employee_data|
13+
EmployeeMailer.not_billed_times_reminder_mail(employee_data).deliver_now
1814
end
1915
end
16+
17+
def responsible_employees_with_not_billed_times_last_month
18+
Employee.joins(:employments)
19+
.joins(:worktimes)
20+
.joins('INNER JOIN work_items ON work_items.id = worktimes.work_item_id')
21+
.joins('INNER JOIN accounting_posts ON accounting_posts.work_item_id = work_items.id')
22+
.joins('INNER JOIN orders ON orders.work_item_id = ANY (work_items.path_ids)')
23+
.joins('INNER JOIN employees as responsibles ON responsibles.id = orders.responsible_id')
24+
.where(accounting_posts: { billing_reminder_active: true })
25+
.where(Period.parse('-1m').where_condition('worktimes.work_date'))
26+
.merge(Employment.active.during(Period.previous_month))
27+
.where(worktimes: { billable: true, invoice_id: nil })
28+
.select('responsibles.*, orders.id as order_id, work_items.path_names as client')
29+
.distinct
30+
end
2031
end

app/mailers/employee_mailer.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ def worktime_commit_reminder_mail(employee)
1818
)
1919
end
2020

21-
def not_billed_times_reminder_mail(employee)
22-
@employee = employee
21+
def not_billed_times_reminder_mail(employee_data)
22+
@employee_data = employee_data
2323

2424
mail(
25-
to: "#{employee.firstname} #{employee.lastname} <#{employee.email}>",
26-
subject: 'Erinnerung: Noch nicht verrechnete PuzzleTime Zeiten'
25+
to: "#{employee_data.firstname} #{employee_data.lastname} <#{employee_data.email}>",
26+
subject: "PTime: #{employee_data.client} - nicht verrechnete Leistungen im letzten Monat gefunden"
2727
)
2828
end
2929
end

app/models/accounting_post.rb

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of
3+
# Copyright (c) 2006-2025, Puzzle ITC GmbH. This file is part of
44
# PuzzleTime and licensed under the Affero General Public License version 3
55
# or later. See the COPYING file at the top-level directory or at
66
# https://github.com/puzzle/puzzletime.
@@ -9,19 +9,20 @@
99
#
1010
# Table name: accounting_posts
1111
#
12-
# id :integer not null, primary key
13-
# work_item_id :integer not null
14-
# portfolio_item_id :integer
15-
# offered_hours :float
16-
# offered_rate :decimal(12, 2)
17-
# offered_total :decimal(12, 2)
18-
# remaining_hours :integer
19-
# billable :boolean default(TRUE), not null
20-
# description_required :boolean default(FALSE), not null
21-
# ticket_required :boolean default(FALSE), not null
22-
# closed :boolean default(FALSE), not null
23-
# from_to_times_required :boolean default(FALSE), not null
24-
# service_id :integer
12+
# id :integer not null, primary key
13+
# work_item_id :integer not null
14+
# portfolio_item_id :integer
15+
# offered_hours :float
16+
# offered_rate :decimal(12, 2)
17+
# offered_total :decimal(12, 2)
18+
# remaining_hours :integer
19+
# billable :boolean default(TRUE), not null
20+
# description_required :boolean default(FALSE), not null
21+
# ticket_required :boolean default(FALSE), not null
22+
# closed :boolean default(FALSE), not null
23+
# from_to_times_required :boolean default(FALSE), not null
24+
# service_id :integer
25+
# billing_reminder_active :boolean default(TRUE), not null
2526
#
2627

2728
class AccountingPost < ApplicationRecord
@@ -100,14 +101,6 @@ def propagate_closed!
100101
work_item.propagate_closed!(order.status.closed? || closed?)
101102
end
102103

103-
def unbilled_billable_times_exist_in_past_month?
104-
work_item.worktimes
105-
.in_period(Period.parse('-1m'))
106-
.where(billable: true)
107-
.where(invoice_id: nil)
108-
.present?
109-
end
110-
111104
private
112105

113106
def derive_offered_fields
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
%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;"}
22
Hallo
3-
= @employee.firstname
3+
= @employee_data.firstname
44
%div{:style => "box-sizing: border-box;"}
55
.lead
6-
Beim einem der Aufträge, bei welchen du Auftragsverantwortliche:r bist, wurden im vergangenen Monat verrechenbare Leistungen gebucht, welche noch keiner Rechnung zugeteilt wurden.
6+
Beim Auftrag
7+
= @employee_data.client
8+
wurden im letzten Monat verrechenbare Leistungen gebucht, welche noch keiner Rechnung zugeteilt wurden.
79
%br
8-
Bitte überprüfe die Leistungen im
9-
= link_to 'Verrechnungs-Controlling', root_url
10+
Bitte
11+
= link_to 'überprüfe die Leistungen', order_order_services_url(order_id: @employee_data.order_id, invoice_id: '[leer]')
1012
%br
1113
.lead
1214
Liebe Grüsse
1315
%br
1416
Dein PuzzleTime
1517
%br
16-
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".
18+
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".
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
Hallo <%= @employee.firstname %>
1+
Hallo <%= @employee_data.firstname %>
22

3-
Beim einem der Aufträge, bei welchen du Auftragsverantwortliche:r bist, wurden im vergangenen Monat verrechenbare Leistungen gebucht, welche noch keiner Rechnung zugeteilt wurden.
3+
Beim Auftrag <%= @employee_data.client %> wurden im letzten Monat verrechenbare Leistungen gebucht, welche noch keiner Rechnung zugeteilt wurden.
44

5-
Bitte überprüfe die Leistungen im 'Verrechnungs-Controlling' im PuzzleTime.
5+
Bitte <%= link_to 'überprüfe die Leistungen', order_order_services_url(order_id: @employee_data.order_id, invoice_id: '[leer]') %>
66

77
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".
88

99
Liebe Grüsse
10-
Dein PuzzleTime
10+
Dein PuzzleTime

test/fabricators/accounting_post_fabricator.rb

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of
3+
# Copyright (c) 2006-2025, Puzzle ITC GmbH. This file is part of
44
# PuzzleTime and licensed under the Affero General Public License version 3
55
# or later. See the COPYING file at the top-level directory or at
66
# https://github.com/puzzle/puzzletime.
@@ -9,19 +9,20 @@
99
#
1010
# Table name: accounting_posts
1111
#
12-
# id :integer not null, primary key
13-
# work_item_id :integer not null
14-
# portfolio_item_id :integer
15-
# offered_hours :float
16-
# offered_rate :decimal(12, 2)
17-
# offered_total :decimal(12, 2)
18-
# remaining_hours :integer
19-
# billable :boolean default(TRUE), not null
20-
# description_required :boolean default(FALSE), not null
21-
# ticket_required :boolean default(FALSE), not null
22-
# from_to_times_required :boolean default(FALSE), not null
23-
# closed :boolean default(FALSE), not null
24-
# service_id :integer
12+
# id :integer not null, primary key
13+
# work_item_id :integer not null
14+
# portfolio_item_id :integer
15+
# offered_hours :float
16+
# offered_rate :decimal(12, 2)
17+
# offered_total :decimal(12, 2)
18+
# remaining_hours :integer
19+
# billable :boolean default(TRUE), not null
20+
# description_required :boolean default(FALSE), not null
21+
# ticket_required :boolean default(FALSE), not null
22+
# from_to_times_required :boolean default(FALSE), not null
23+
# closed :boolean default(FALSE), not null
24+
# service_id :integer
25+
# billing_reminder_active :boolean default(TRUE), not null
2526
#
2627

2728
Fabricator(:accounting_post) do

test/fixtures/accounting_posts.yml

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of
1+
# Copyright (c) 2006-2025, Puzzle ITC GmbH. This file is part of
22
# PuzzleTime and licensed under the Affero General Public License version 3
33
# or later. See the COPYING file at the top-level directory or at
44
# https://github.com/puzzle/puzzletime.
55

6-
76
# == Schema Information
87
#
98
# Table name: accounting_posts
109
#
11-
# id :integer not null, primary key
12-
# work_item_id :integer not null
13-
# portfolio_item_id :integer
14-
# offered_hours :float
15-
# offered_rate :decimal(12, 2)
16-
# offered_total :decimal(12, 2)
17-
# remaining_hours :integer
18-
# billable :boolean default(TRUE), not null
19-
# description_required :boolean default(FALSE), not null
20-
# ticket_required :boolean default(FALSE), not null
21-
# closed :boolean default(FALSE), not null
22-
# from_to_times_required :boolean default(FALSE), not null
23-
# service_id :integer
10+
# id :integer not null, primary key
11+
# work_item_id :integer not null
12+
# portfolio_item_id :integer
13+
# offered_hours :float
14+
# offered_rate :decimal(12, 2)
15+
# offered_total :decimal(12, 2)
16+
# remaining_hours :integer
17+
# billable :boolean default(TRUE), not null
18+
# description_required :boolean default(FALSE), not null
19+
# ticket_required :boolean default(FALSE), not null
20+
# closed :boolean default(FALSE), not null
21+
# from_to_times_required :boolean default(FALSE), not null
22+
# service_id :integer
23+
# billing_reminder_active :boolean default(TRUE), not null
2424
#
2525

2626
---
@@ -58,4 +58,3 @@ webauftritt:
5858
offered_hours: 1000
5959
offered_rate: 140
6060
offered_total: 140000
61-
...

test/mailers/employee_mailer_test.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,24 @@ class EmployeeMailerTest < ActionMailer::TestCase
2424
order_responsible = employees(:next_year_pablo)
2525
Fabricate(:ordertime, work_item: accounting_post1.work_item, employee: employees(:long_time_john), hours: 5, billable: true, work_date: Period.parse('-1m').end_date)
2626

27-
assert_emails 1 do
27+
emails = capture_emails do
2828
NotBilledTimesReminderJob.new.perform
2929
end
30-
mail = ActionMailer::Base.deliveries.last
30+
31+
assert_equal 1, emails.count
32+
mail = emails.first
33+
body = mail.text_part.body.raw_source.gsub(/\s+/, ' ')
34+
client = accounting_post1.path_names.gsub(/\s+/, ' ')
3135

3236
assert_equal [order_responsible.email], mail.to
37+
assert_equal "PTime: #{accounting_post1.path_names} - nicht verrechnete Leistungen im letzten Monat gefunden", mail.subject
38+
39+
assert_match(/Hallo #{order_responsible.firstname}/, body)
40+
41+
assert_match(/Beim Auftrag #{client} wurden im letzten Monat verrechenbare Leistungen gebucht, welche noch keiner Rechnung zugeteilt wurden./, body)
42+
assert_match(%r{orders/#{order.id}/order_services\?invoice_id=%5Bleer%5D}, body)
43+
assert_match(/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"/, body)
44+
assert_match(/Liebe Grüsse Dein PuzzleTime/, body)
3345
end
3446

3547
test 'setting `billing_reminder_active: false` deactivates mails for an accounting_post' do

test/mailers/previews/employee_mailer_preview.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def worktime_commit_reminder_mail
2020
end
2121

2222
def not_billed_times_reminder_mail
23-
employee = Employee.new(email: '[email protected]', firstname: 'Peter', lastname: 'Puzzler')
24-
EmployeeMailer.not_billed_times_reminder_mail(employee)
23+
employee_data = { email: '[email protected]', firstname: 'Peter', lastname: 'Puzzler', client: 'TOP-FAV', order_id: 1 }
24+
EmployeeMailer.not_billed_times_reminder_mail(employee_data)
2525
end
2626
end

test/models/accounting_post_test.rb

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of
3+
# Copyright (c) 2006-2025, Puzzle ITC GmbH. This file is part of
44
# PuzzleTime and licensed under the Affero General Public License version 3
55
# or later. See the COPYING file at the top-level directory or at
66
# https://github.com/puzzle/puzzletime.
@@ -9,19 +9,20 @@
99
#
1010
# Table name: accounting_posts
1111
#
12-
# id :integer not null, primary key
13-
# work_item_id :integer not null
14-
# portfolio_item_id :integer
15-
# offered_hours :float
16-
# offered_rate :decimal(12, 2)
17-
# offered_total :decimal(12, 2)
18-
# remaining_hours :integer
19-
# billable :boolean default(TRUE), not null
20-
# description_required :boolean default(FALSE), not null
21-
# ticket_required :boolean default(FALSE), not null
22-
# closed :boolean default(FALSE), not null
23-
# from_to_times_required :boolean default(FALSE), not null
24-
# service_id :integer
12+
# id :integer not null, primary key
13+
# work_item_id :integer not null
14+
# portfolio_item_id :integer
15+
# offered_hours :float
16+
# offered_rate :decimal(12, 2)
17+
# offered_total :decimal(12, 2)
18+
# remaining_hours :integer
19+
# billable :boolean default(TRUE), not null
20+
# description_required :boolean default(FALSE), not null
21+
# ticket_required :boolean default(FALSE), not null
22+
# closed :boolean default(FALSE), not null
23+
# from_to_times_required :boolean default(FALSE), not null
24+
# service_id :integer
25+
# billing_reminder_active :boolean default(TRUE), not null
2526
#
2627

2728
require 'test_helper'

0 commit comments

Comments
 (0)