Verified Commit 18cc9697 authored by Markus Koller's avatar Markus Koller 🦊
Browse files

Implement new workflow for billing expenses

parent d1d6d484
Pipeline #17277 passed with stage
in 52 minutes and 46 seconds
......@@ -22,7 +22,11 @@ Style/LineEndConcatenation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/NumericPredicate:
Enabled: false
Layout/IndentArray:
EnforcedStyle: consistent
Layout/MultilineOperationIndentation:
EnforcedStyle: indented
Layout/MultilineMethodCallIndentation:
......@@ -40,6 +44,7 @@ Metrics/ClassLength:
Exclude:
- 'test/**/*'
Metrics/MethodLength:
Max: 15
Exclude:
- 'test/**/*'
- 'db/seeds.rb'
......@@ -63,7 +68,7 @@ Style/ClassAndModuleChildren:
Enabled: false
Style/FormatStringToken:
EnforcedStyle: template
Enabled: false
Style/SymbolArray:
EnforcedStyle: brackets
......
......@@ -2,7 +2,7 @@ $(() => $(document).on('turbolinks:render, turbolinks:load', () => {
truncateModal();
dateTimePicker();
conditionalField();
mailingsSelectAll();
tableRowSelectable();
volunteerForm();
groupOfferForm();
}));
......@@ -19,4 +19,5 @@
//= require bootstrap-datepicker/locales/bootstrap-datepicker.de.js
//= require cocoon
//= require selectize
//
//= require_tree .
function mailingsSelectAll() {
$('.reminder-select .click-selecting').on('click', event => {
event.stopPropagation();
const row = $(event.target).closest('tr');
const selectBox = $(`#reminder_mailing_reminder_mailing_volunteers_attributes_${row.data().index}_picked`);
row.toggleClass('mailing-selected', !selectBox.is(':checked'));
selectBox.prop('checked', !selectBox.is(':checked'));
});
const selectedBoxes = $('input.boolean[id$="_picked"]');
selectedBoxes.on('change', ({target, preventDefault}) => {
preventDefault();
target = $(target);
target.prop('checked', !target.is(':checked'));
const row = $(target).closest('tr');
row.toggleClass('mailing-selected', target.is(':checked'));
});
$('.select-all-mailings input[type="checkbox"]').on('change', ({target}) => {
const mailingRows = $('.reminder-select tbody tr');
mailingRows.toggleClass('mailing-selected', target.checked);
selectedBoxes.prop('checked', target.checked);
});
}
function tableRowSelectable() {
var rows = $('tr.table-row-selectable');
rows.on('click', event => {
var row = $(event.currentTarget);
var checkbox = row.find(':checkbox:first');
var selected = checkbox.is(':checked');
if (checkbox.is(':disabled')) {
return;
}
if ($(event.target).is(checkbox)) {
selected = !selected;
} else {
checkbox.prop({ checked: !selected });
}
row.toggleClass('success', !selected);
});
// open links in new tabs, don't select when clicking on them
var links = rows.find('a');
links.attr({ target: '_blank' });
links.on('click', event => {
event.stopPropagation();
});
// toggle all rows
$('.table-row-select-all').on('click', function() {
if ($(this).is(':checked')) {
rows.find(':checkbox:not(:checked)').click();
} else {
rows.find(':checkbox:checked').click();
}
});
// restore state on page load
rows.find(':checkbox:checked:not(:disabled)').each(function() {
$(this).closest('tr').addClass('success');
});
}
.table {
.limit-width {
width: 5%;
}
}
.table-scrollable {
overflow-y: auto;
}
.assignments.find-client .table-scrollable {
max-height: 300px;
}
.billing-expenses.new .table-scrollable {
max-height: 500px;
}
.list-responses-table {
.limit-width {
width: 5%;
}
}
......@@ -3,27 +3,6 @@
min-height: 30vh;
}
.select-all-mailings {
padding-left: 7px;
input {
margin: 0 5px 0 0;
}
label {
font-weight: 500;
margin: 0;
}
}
.mailing-selected {
td {
background-color: $brand-success-lighter;
border-bottom: 1px solid $gray-lighter;
color: $gray-base;
}
}
.mailing-body-preview {
background-color: $gray-lighter;
border: solid 1px $gray-lighter;
......
class BillingExpensesController < ApplicationController
before_action :set_billing_expense, only: [:show, :destroy]
before_action :set_volunteer
before_action :set_volunteer, only: [:index]
def index
authorize BillingExpense
@billing_expenses = BillingExpense.where(volunteer: @volunteer)
@q = BillingExpense.ransack(params[:q])
@q.sorts = ['created_at desc'] if @q.sorts.empty?
@billing_expenses = @q.result.page(params[:page])
@billing_expenses = @billing_expenses.where(volunteer_id: @volunteer.id) if @volunteer
end
def show
......@@ -18,21 +23,26 @@ class BillingExpensesController < ApplicationController
end
def new
@billing_expense = BillingExpense.new(volunteer: @volunteer)
@billing_expense = BillingExpense.new
authorize @billing_expense
@q = Volunteer.with_billable_hours.ransack(params[:q])
@volunteers = @q.result
@selected_volunteers = params[:selected_volunteers].presence || []
end
def create
@billing_expense = BillingExpense.new(@volunteer.slice(:bank, :iban))
@billing_expense.hours = @volunteer.hours.billable
@billing_expense.volunteer = @volunteer
@billing_expense.user = current_user
authorize @billing_expense
if @billing_expense.save
redirect_to volunteer_billing_expenses_url, make_notice
else
redirect_to volunteer_billing_expenses_url, notice: t('already_computed')
end
authorize BillingExpense, :create?
selected_volunteers = params[:selected_volunteers]
volunteers = Volunteer.need_refunds.where(id: selected_volunteers)
BillingExpense.create_for!(volunteers, current_user)
redirect_to billing_expenses_url,
notice: 'Spesenformulare wurden erfolgreich erstellt.'
rescue ActiveRecord::RecordInvalid => error
redirect_to new_billing_expense_url(selected_volunteers: selected_volunteers),
notice: error.message
end
def destroy
......@@ -53,7 +63,10 @@ class BillingExpensesController < ApplicationController
def pdf_file_name
'Spesenauszahlung-' +
[@volunteer.contact.full_name, @volunteer.hours.maximum(:meeting_date)].join('-').parameterize
[
@billing_expense.volunteer.contact.full_name,
@billing_expense.volunteer.hours.maximum(:meeting_date)
].join('-').parameterize
end
def billing_expense_params
......
class HoursController < ApplicationController
before_action :set_hour, only: [:show, :edit, :update, :destroy, :create_redirect, :mark_as_done]
before_action :set_hour, only: [:show, :edit, :update, :destroy, :create_redirect]
before_action :set_volunteer
def index
......@@ -43,16 +43,8 @@ class HoursController < ApplicationController
redirect_to @volunteer, make_notice
end
def mark_as_done
redirect_path = list_responses_hours_path(params.to_unsafe_hash.slice(:q))
if @hour.update(reviewer: current_user)
redirect_to(redirect_path, notice: 'Stunden als angeschaut markiert.')
else
redirect_to(redirect_path, notice: 'Fehler: Angeschaut markieren fehlgeschlagen.')
end
end
private
def simple_form_params
@simple_form_for_params = [
[@volunteer, @hour.hourable, @hour],
......
class ListResponsesController < ApplicationController
before_action { set_default_filter(author_volunteer: 'true', reviewer_id_null: 'true') }
def feedbacks
authorize :list_response
@q = Feedback.created_asc.author_volunteer(params[:q]).ransack(params[:q])
......@@ -12,11 +14,4 @@ class ListResponsesController < ApplicationController
@q.sorts = ['updated_at asc'] if @q.sorts.empty?
@trial_feedbacks = @q.result.paginate(page: params[:page])
end
def hours
authorize :list_response
@q = Hour.created_asc.ransack(params[:q])
@q.sorts = ['updated_at asc'] if @q.sorts.empty?
@hours = @q.result.paginate(page: params[:page])
end
end
......@@ -112,13 +112,13 @@ module ApplicationHelper
end
end
def default_list_response_query
{ author_volunteer: 'true', reviewer_id_null: 'true', s: 'updated_at asc' }
def section_nav_button(text, url)
style = current_page?(url) ? 'btn-section-active' : 'btn-default'
link_to text, url, class: "btn btn-sm #{style}"
end
def section_nav_button(actions_name, text, url)
link_to_unless(action_name == actions_name, text, url, class: 'btn btn-default btn-sm') do
link_to(text, url, class: 'btn btn-sm btn-section-active')
end
def select_all_rows
check_box_tag 'table-row-select-all', '', false, class: 'table-row-select-all',
title: 'Alle auswählen'
end
end
module FormatHelper
def format_currency(amount)
number_to_currency amount, unit: 'Fr.', format: '%u %n', separator: '.', delimiter: "'"
end
def format_hours(hours)
hours = hours.to_i if (hours % 1).zero?
pluralize hours, 'Stunde', 'Stunden'
end
def format_hours_period(hours)
dates = hours.map(&:meeting_date)
"#{I18n.l dates.min} - #{I18n.l dates.max}"
end
end
......@@ -6,28 +6,55 @@ class BillingExpense < ApplicationRecord
before_validation :compute_amount, unless: :import_mode
belongs_to :volunteer, -> { with_deleted }, inverse_of: 'billing_expenses'
belongs_to :user, -> { with_deleted }, inverse_of: 'billing_expenses'
belongs_to :volunteer, -> { with_deleted }, inverse_of: :billing_expenses
belongs_to :user, -> { with_deleted }, inverse_of: :billing_expenses
has_many :hours, dependent: :nullify
default_scope { order(created_at: :desc) }
AMOUNT = [50, 100, 150].freeze
validates :iban, presence: true
validates :amount, inclusion: { in: AMOUNT }, unless: :import_mode
def self.amount_for(hours)
if hours > 50
150
elsif hours > 25
100
elsif hours > 0
50
else
0
end
end
def self.create_for!(volunteers, creator)
transaction do
volunteers.find_each do |volunteer|
hours = volunteer.hours.billable
hours.find_each do |hour|
hour.update!(reviewer: creator)
end
create!(
volunteer: volunteer,
user: creator,
hours: hours,
bank: volunteer.bank,
iban: volunteer.iban
)
end
end
end
private
def compute_amount
hour_count = id ? hours.total_hours : volunteer.hours.billable.total_hours
if hour_count > 50
self.amount = 150
elsif hour_count > 25
self.amount = 100
elsif hour_count >= 1
self.amount = 50
else
self.amount = 0
end
return if hours.blank?
# FIXME: we can't use total_hours here because of some weirdness
# with ActiveRecord::AssociationRelation
self.amount = self.class.amount_for(hours.sum(&:hours))
end
end
......@@ -3,7 +3,7 @@ module FullBankDetails
included do
def full_bank_details
[bank, iban].reject(&:blank?).join(' ')
[bank, iban].reject(&:blank?).join(', ')
end
end
end
......@@ -12,7 +12,7 @@ class Hour < ApplicationRecord
belongs_to :billing_expense, -> { with_deleted }, optional: true, inverse_of: 'hours'
belongs_to :certificate, optional: true
validates :hours, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :hours, presence: true, numericality: { greater_than: 0 }
validates :meeting_date, presence: true
validates :hourable, presence: true
......@@ -22,10 +22,6 @@ class Hour < ApplicationRecord
where('created_at > ?', submitted_at) if submitted_at
}
scope :need_refund, lambda {
joins(:volunteer).where('volunteers.waive = FALSE')
}
scope :assignment, (-> { where(hourable_type: 'Assignment') })
scope :group_offer, (-> { where(hourable_type: 'GroupOffer') })
scope :from_assignments, lambda { |assignment_ids|
......
......@@ -183,6 +183,17 @@ class Volunteer < ApplicationRecord
volunteers.where("NOT EXISTS (#{assignments.to_sql})")
}
scope :need_refunds, (-> { where(waive: false) })
scope :with_billable_hours, lambda {
need_refunds
.joins(:contact)
.joins(:hours).merge(Hour.billable)
.select('volunteers.*, SUM(hours.hours) AS total_hours')
.group(:id, 'contacts.full_name')
.order("(CASE WHEN COALESCE(iban, '') = '' THEN 2 ELSE 1 END), contacts.full_name")
}
def verify_and_update_state
update(active: active?, activeness_might_end: relevant_period_end_max)
end
......
class ListResponsePolicy < ApplicationPolicy
alias_method :feedbacks?, :superadmin?
alias_method :hours?, :superadmin?
alias_method :trial_feedbacks?, :superadmin?
alias_method :feedbacks?, :superadmin?
alias_method :trial_feedbacks?, :superadmin?
end
......@@ -20,7 +20,7 @@ nav.navbar.section-navigation
li= render 'age_request_select'
li= render 'language_skills_language_select'
#find-clients-table.table-responsive
.table-responsive.table-scrollable
table.table.table-striped
thead
tr
......
h1= @volunteer.contact.full_name
h1= t_title(:index, BillingExpense)
- unless @volunteer
= render 'reminder_mailings/section_navigation'
h1
'Spesenformulare
- if @volunteer
= "für #{@volunteer}"
.row
.col-xs-12
- if @volunteer
=> button_link navigation_glyph(:back), @volunteer
- else
=> button_link 'Spesenformulare erstellen', new_billing_expense_path
= bootstrap_paginate(@billing_expenses)
.table-responsive
table.table
thead
tr
th.hidden-print Aktionen
th= t_model(Volunteer)
th= t_attr(:address)
th= sort_link @q, :volunteer_contact_last_name, t_model(Volunteer)
th= t_attr(:bank_details, Volunteer)
th= t_attr(:waive, Volunteer)
th= t_attr(:hours, Hour)
th= t_attr(:amount, BillingExpense)
th= t_attr(:created_at)
th= t_attr(:created_by)
th= t_attr(:hours)
th= sort_link @q, :amount
th Periode
th= sort_link @q, :user_profile_contact_last_name, t_attr(:created_by)
th= sort_link @q, :created_at
tbody
- @volunteer.billing_expenses.each do |record|
- @billing_expenses.each do |record|
tr
td.index-action-cell.hidden-print
= button_link navigation_glyph(:show), volunteer_billing_expense_path(@volunteer, record),
= button_link navigation_glyph(:show), billing_expense_path(record),
title: 'Anzeigen'
= button_link navigation_glyph(:download), volunteer_billing_expense_path(@volunteer, record, format: :pdf),
= button_link navigation_glyph(:download), billing_expense_path(record, format: :pdf),
title: 'Herunterladen'
= button_link navigation_glyph(:delete), volunteer_billing_expense_path(@volunteer, record),
= button_link navigation_glyph(:delete), billing_expense_path(record),
confirm_deleting(record, 'btn btn-default').merge(title: 'Löschen')
td= link_to @volunteer.contact.full_name, volunteer_path(@volunteer)
td= @volunteer.contact.full_address
td= link_to record.volunteer.contact.full_name, volunteer_path(record.volunteer)
td= record.full_bank_details
td= t(@volunteer.waive)
td= record.hours.total_hours
td= record.amount
td= link_to format_hours(record.hours.total_hours), volunteer_hours_path(record.volunteer)
td= format_currency record.amount
td= format_hours_period record.hours
td= link_to record.user, profile_link(record.user)
td= l(record.created_at.to_date)
td= record.user.profile.contact.full_name
.row
.col-xs-12
= simple_form_for [@volunteer, BillingExpense.new] do |f|
= f.hidden_field :volunteer_id, value: @volunteer.id
= f.button :submit
.row
.col-xs-12
= button_link navigation_glyph(:back), @volunteer
= bootstrap_paginate(@billing_expenses)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment