Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KST-Teiler Korrektur & KST-Bericht #667

Merged
merged 8 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apir/app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ class Invoice < ApplicationRecord
validates :fixed_price, numericality: { only_integer: true }, if: -> { fixed_price.present? }
validates :fixed_price_vat, numericality: { greater_than_or_equal_to: 0 }, if: -> { fixed_price_vat.present? }

delegate :costgroup_distribution, :costgroup_sums, :missing_costgroup_distribution, :costgroup_dist_incomplete?, to: :cost_group_breakdown

def breakdown
@breakdown ||= CostBreakdown.new(invoice_positions, invoice_discounts, position_groupings, fixed_price, fixed_price_vat || 0.077).calculate
end

def cost_group_breakdown
@cost_group_breakdown ||= CostGroupBreakdownService.new project, beginning..ending
end

def position_groupings
invoice_positions.uniq { |p| p.position_group&.id }.map(&:position_group).select { |g| g }
end
Expand Down
29 changes: 5 additions & 24 deletions apir/app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Project < ApplicationRecord
# rubocop:enable Rails/RedundantPresenceValidationOnBelongsTo

delegate :budget_price, :budget_time, :current_price, :current_time, to: :project_calculator
delegate :costgroup_distribution, :costgroup_sums, :missing_costgroup_distribution, :costgroup_dist_incomplete?, to: :cost_group_breakdown

def listing_name
name + (archived ? " [A]" : "")
Expand All @@ -36,35 +37,15 @@ def project_calculator
@project_calculator ||= ProjectCalculator.new self
end

def cost_group_breakdown
@cost_group_breakdown ||= CostGroupBreakdownService.new self
end

def position_groupings
project_positions.uniq { |p| p.position_group&.id }.map(&:position_group).select { |g| g }
end

def invoice_ids
invoices&.select { |i| i.deleted_at.nil? }&.map(&:id) || []
end

def costgroup_sums
@costgroup_sums ||= project_efforts.group_by(&:costgroup_number).transform_values { |pegs| pegs.sum(&:value) }
end

def costgroups_sum
@costgroups_sum ||= costgroup_sums.values.sum
end

def costgroup_distribution(costgroup_number)
return 0.0.to_f if !costgroup_sums.key?(costgroup_number) || costgroups_sum.nil?

((costgroup_sums[costgroup_number] / costgroups_sum) * 100).to_f
end

def missing_costgroup_distribution
return 0.0.to_f if !costgroup_dist_incomplete? || costgroups_sum.nil?

((costgroup_sums[nil] / costgroups_sum) * 100).to_f
end

def costgroup_dist_incomplete?
costgroup_sums.key?(nil)
end
end
4 changes: 4 additions & 0 deletions apir/app/models/project_effort.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ class ProjectEffort < ApplicationRecord
validates :date, :value, presence: true
validates :value, numericality: { greater_than_or_equal_to: 0 }
validates :date, timeliness: { type: :date }

def price
project_position.price_per_rate * value / project_position.rate_unit.factor
end
end
31 changes: 31 additions & 0 deletions apir/app/services/cost_group_breakdown_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

class CostGroupBreakdownService
def initialize(project, daterange = nil)
@project_efforts = daterange.nil? ? project.project_efforts : project.project_efforts.where(date: daterange)
end

def costgroup_sums
@costgroup_sums ||= @project_efforts.group_by(&:costgroup_number).transform_values { |pegs| pegs.sum(&:price) }
end

def costgroups_sum
@costgroups_sum ||= costgroup_sums.values.sum
end

def costgroup_distribution(costgroup_number)
return 0.0.to_f if !costgroup_sums.key?(costgroup_number) || costgroups_sum.nil?

((costgroup_sums[costgroup_number] / costgroups_sum) * 100).to_f
end

def missing_costgroup_distribution
return 0.0.to_f if !costgroup_dist_incomplete? || costgroups_sum.nil?

((costgroup_sums[nil] / costgroups_sum) * 100).to_f
end

def costgroup_dist_incomplete?
costgroup_sums.key?(nil)
end
end
122 changes: 63 additions & 59 deletions apir/app/services/cost_group_report_service.rb
Original file line number Diff line number Diff line change
@@ -1,88 +1,75 @@
# frozen_string_literal: true

class CostGroupReportService
attr_accessor :daterange, :employees, :projects, :project_efforts, :project_positions, :efforts_by_project, :effort_minutes_weighted, :cost_groups
attr_accessor :daterange, :employees, :projects, :project_efforts, :project_positions, :efforts_by_project, :employee_sums, :cost_group_sums, :cost_groups

def initialize(daterange = Date.today.all_year, employee_group_name: "SWO Angestellte")
self.daterange = daterange
self.project_positions = ProjectPosition.joins(:project, :rate_unit, project: :costgroups, project_efforts: { employee: :employee_group })
.where("rate_units.is_time" => true, "project_efforts.date" => daterange)
.where("employee_groups.name" => employee_group_name)
self.project_efforts = ProjectEffort.joins(project_position: [:project, :rate_unit], employee: :employee_group)
.where("rate_units.is_time" => true, "project_efforts.date" => daterange)
.where("employee_groups.name" => employee_group_name)
# .where(:project => Project.where(vacation_project: 0)) Uncomment if vacation projects should not be considered
self.projects = Project.where(id: project_positions.select("project_id"))
self.employees = Employee.where(id: project_positions.select("employees.id")).order(first_name: :asc, last_name: :asc)
self.cost_groups = Costgroup.where(number: project_positions.select("costgroups.number")).order(number: :asc, name: :asc)
self.efforts_by_project = project_positions.group("employees.id", "costgroups.number", "project_id").sum("project_efforts.value")
self.effort_minutes_weighted = weighted_effort_minutes
self.projects = Project.where(id: project_efforts.select("project_position.project_id"))
self.employees = Employee.where(id: project_efforts.select("employees.id")).order(first_name: :asc, last_name: :asc) + [Employee.new(first_name: "Unbekannt", last_name: "")]
self.cost_groups = Costgroup.where(number: project_efforts.select("costgroup_number")).order(number: :asc, name: :asc) + [Costgroup.new(name: "???", number: 0)]
self.efforts_by_project = project_efforts.group("employees.id", "costgroup_number").sum("value")
self.employee_sums = project_efforts.group("employees.id").sum("value")
self.cost_group_sums = project_efforts.group("costgroup_number").sum("value")
end

def weighted_effort_minutes
effort_minutes = {}
efforts_by_project.each do |(employee_id, cost_group_id, project_id), minutes|
project = projects.find { |project| project.id == project_id }

total_weight = 0.0

project.project_costgroup_distributions.each do |d|
total_weight += d.weight
end
def effort
@effort ||= efforts_by_project.each_with_object({}) do |memo, (ids, minutes)|
employee_id, costgroup_number = *ids

total_weight = 1.0 if total_weight.zero?
employee = employees.find { |employee| employee.id == employee_id } || Employee.new(first_name: "Unbekannt", last_name: "")
employee_values = memo[employee] || {}

weight = project.project_costgroup_distributions.find do |cg|
cg.costgroup_number == cost_group_id
end.weight / total_weight
employee_values[cost_groups.find { |cost_group| cost_group.number == costgroup_number } || Costgroup.new(name: "???", number: 0)] = minutes
memo[employee] = employee_values

effort_minutes[[employee_id, cost_group_id]] ||= 0
effort_minutes[[employee_id, cost_group_id]] += minutes * weight
memo
end
effort_minutes
end

def effort
@effort = {}

effort_minutes_weighted.each do |(employee_id, cost_group_id), minutes|
@effort[employee_id] ||= {}
@effort[employee_id][cost_group_id] = minutes
end
@effort.transform_keys! do |employee_id|
employees.find { |employee| employee.id == employee_id } || Employee.new(first_name: "Unbekannt", last_name: "")
end
@effort.transform_values! do |cost_group_efforts|
cost_group_efforts.transform_keys! do |cost_group_id|
cost_groups.find { |cost_group| cost_group.number == cost_group_id } || Costgroup.new(name: "???", number: 0)
end
cost_group_efforts.transform_values! do |minutes|
(minutes / 60.0).round(2)
end
cost_group_efforts
end
@effort
end

def rows
effort.map do |employee, cost_group_effort|
effort.map do |employee, cost_group_efforts|
row = [employee.name]
row += cost_groups.map { |cost_group| cost_group_effort[cost_group] || 0.0 }
row += employee_cost_groups_hours(cost_group_efforts)
row += [(employee_sums[employee.id] / 60.0).round(2)]
row += employee_cost_groups_percentages(cost_group_efforts, employee)
row
end
end

def row_style
[nil] +
Array.new(cost_groups.size) { nil } +
[nil] +
Array.new(cost_groups.size) { @percent }
end

def header
["Name"] + cost_groups.map { |costgroup| [costgroup.number, costgroup.name, "Stunden"].reject(&:blank?).join(", ") }
["Name"] +
cost_groups.map { |costgroup| [costgroup.number, costgroup.name, "Stunden"].reject(&:blank?).join(", ") } +
["Total"] +
cost_groups.map { |costgroup| [costgroup.number, costgroup.name, "(%)"].reject(&:blank?).join(", ") }
end

def footer
effort_minutes_by_cost_group = {}
effort_minutes_weighted.each do |(_employee_id, cost_group_id), minutes|
effort_minutes_by_cost_group[cost_group_id] ||= 0
effort_minutes_by_cost_group[cost_group_id] += minutes
end
["Total"] +
cost_groups.map do |cost_group|
((cost_group_sums[cost_group.number] || 0.0) / 60.0).round(2)
end +
[((cost_group_sums.values.sum || 0.0) / 60.0).round(2)] +
cost_groups.map { "" }
end

["Total"] + cost_groups.map do |cost_group|
((effort_minutes_by_cost_group[cost_group.id] || 0.0) / 60.0).round 2
end
def define_styles(workbook)
percent(workbook)
end

def percent(workbook)
@percent ||= workbook.styles.add_style(format_code: "0.00%")
end

def table
Expand All @@ -97,4 +84,21 @@ def table
def tty
Rails.logger.debug TTY::Table.new rows: table
end

private

def employee_cost_groups_percentages(cost_group_efforts, employee)
cost_groups.map do |cost_group|
employee_sum = employee_sums[employee.id]
cost_group_effort = cost_group_efforts[cost_group]

next 0.0 if employee_sum.nil? || cost_group_effort.nil?

(cost_group_effort / employee_sum)
end
end

def employee_cost_groups_hours(cost_group_efforts)
cost_groups.map { |cost_group| ((cost_group_efforts[cost_group] || 0.0) / 60.0).round(2) }
end
end
11 changes: 10 additions & 1 deletion apir/app/views/v2/cost_group_reports/index.xlsx.axlsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
# frozen_string_literal: true

wb = xlsx_package.workbook

@report.define_styles(wb) if @report.respond_to?(:define_styles)

wb.add_worksheet(name: "Kostenstellenraport") do |sheet|
sheet.add_row @report.header
@report.rows.map { |row| sheet.add_row row }
@report.rows.each do |row|
if @report.respond_to?(:row_style)
sheet.add_row(row, style: @report.row_style)
else
sheet.add_row row
end
end
sheet.add_row @report.footer
end
6 changes: 3 additions & 3 deletions apir/app/views/v2/invoices/_invoice.json.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ json.costgroup_distributions invoice.invoice_costgroup_distributions.map do |ic|
json.costgroup_number ic.costgroup_number
json.invoice_id ic.invoice_id
json.weight ic.weight
if invoice.project.costgroup_sums.key?(ic.costgroup_number)
json.distribution invoice.project.costgroup_distribution(ic.costgroup_number)
if invoice.costgroup_sums.key?(ic.costgroup_number)
json.distribution invoice.costgroup_distribution(ic.costgroup_number)
else
0.00.to_f
end
end
json.costgroup_uncategorized_distribution invoice.project.missing_costgroup_distribution if invoice.project.costgroup_dist_incomplete?
json.costgroup_uncategorized_distribution invoice.missing_costgroup_distribution if invoice.costgroup_dist_incomplete?

json.project_id invoice.project&.id
json.discounts invoice.invoice_discounts
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ services:
- ./apir:/apir
working_dir: /apir
command: "bash -c 'bundle install && bundle exec rails server -p 8000 -b 0.0.0.0'"
stdin_open: true
tty: true
depends_on:
- mysql
ports:
Expand Down
Loading