Update model to support financial transactions #367

This change introduces two new data types to group the financial
transactions. Now every transaction has a "type", which itself belongs
to a "class".
Types should be used add structured information to an transaction, instead
of writing it into the notice textfield. E.g. this could be used to have
different types depending on the source of money (cash vs. bank transfer).
Classes are shown as different columns in the tables and will be uses to
group transactions of specific types. They should be used if not the whole
amount of ordergroup should be used to order food. E.g. if there is a
deposit or membership fee, which is independent of the normal credit.
This will allow us to implement additional features based on classes in
the future. E.g. the sum of transactions in the "membership fee" class
must be positive to allow food orders or show a big warning if it is bellow
a certain value.
This commit is contained in:
Patrick Gansterer 2017-03-04 14:15:18 +01:00
parent dc94e98138
commit e7657b987f
21 changed files with 156 additions and 34 deletions

View File

@ -68,7 +68,8 @@ class Finance::BalancingController < Finance::BaseController
# Balances the Order, Update of the Ordergroup.account_balances # Balances the Order, Update of the Ordergroup.account_balances
def close def close
@order = Order.find(params[:id]) @order = Order.find(params[:id])
@order.close!(@current_user) @type = FinancialTransactionType.find_by_id(params.permit(:type))
@order.close!(@current_user, @type)
redirect_to finance_order_index_url, notice: t('finance.balancing.close.notice') redirect_to finance_order_index_url, notice: t('finance.balancing.close.notice')
rescue => error rescue => error

View File

@ -55,10 +55,11 @@ class Finance::FinancialTransactionsController < ApplicationController
def create_collection def create_collection
raise I18n.t('finance.financial_transactions.controller.create_collection.error_note_required') if params[:note].blank? raise I18n.t('finance.financial_transactions.controller.create_collection.error_note_required') if params[:note].blank?
type = FinancialTransactionType.find_by_id(params.permit(:type))
params[:financial_transactions].each do |trans| params[:financial_transactions].each do |trans|
# ignore empty amount fields ... # ignore empty amount fields ...
unless trans[:amount].blank? unless trans[:amount].blank?
Ordergroup.find(trans[:ordergroup_id]).add_financial_transaction!(trans[:amount], params[:note], @current_user) Ordergroup.find(trans[:ordergroup_id]).add_financial_transaction!(trans[:amount], params[:note], @current_user, type)
end end
end end
redirect_to finance_ordergroups_url, notice: I18n.t('finance.financial_transactions.controller.create_collection.notice') redirect_to finance_ordergroups_url, notice: I18n.t('finance.financial_transactions.controller.create_collection.notice')

View File

@ -4,6 +4,7 @@ class FinancialTransaction < ActiveRecord::Base
belongs_to :ordergroup belongs_to :ordergroup
belongs_to :user belongs_to :user
belongs_to :financial_link belongs_to :financial_link
belongs_to :financial_transaction_type
validates_presence_of :amount, :note, :user_id, :ordergroup_id validates_presence_of :amount, :note, :user_id, :ordergroup_id
validates_numericality_of :amount, greater_then: -100_000, validates_numericality_of :amount, greater_then: -100_000,
@ -11,8 +12,18 @@ class FinancialTransaction < ActiveRecord::Base
localize_input_of :amount localize_input_of :amount
after_initialize do
initialize_financial_transaction_type
end
# Use this save method instead of simple save and after callback # Use this save method instead of simple save and after callback
def add_transaction! def add_transaction!
ordergroup.add_financial_transaction! amount, note, user ordergroup.add_financial_transaction! amount, note, user, financial_transaction_type
end
protected
def initialize_financial_transaction_type
self.financial_transaction_type ||= FinancialTransactionType.default
end end
end end

View File

@ -0,0 +1,6 @@
class FinancialTransactionClass < ActiveRecord::Base
has_many :financial_transaction_types, dependent: :destroy
validates :name, presence: true
validates_uniqueness_of :name
end

View File

@ -0,0 +1,25 @@
class FinancialTransactionType < ActiveRecord::Base
belongs_to :financial_transaction_class
has_many :financial_transactions, dependent: :restrict_with_exception
validates :name, presence: true
validates_uniqueness_of :name
validates :financial_transaction_class, presence: true
before_destroy :restrict_deleting_last_financial_transaction_type
def self.default
first
end
def self.has_multiple_types
self.count > 1
end
protected
# check if this is the last financial transaction type and deny
def restrict_deleting_last_financial_transaction_type
raise I18n.t('model.financial_transaction_type.no_delete_last') if FinancialTransactionType.count == 1
end
end

View File

@ -231,7 +231,7 @@ class Order < ActiveRecord::Base
end end
# Sets order.status to 'close' and updates all Ordergroup.account_balances # Sets order.status to 'close' and updates all Ordergroup.account_balances
def close!(user) def close!(user, transaction_type = nil)
raise I18n.t('orders.model.error_closed') if closed? raise I18n.t('orders.model.error_closed') if closed?
transaction_note = I18n.t('orders.model.notice_close', :name => name, transaction_note = I18n.t('orders.model.notice_close', :name => name,
:ends => ends.strftime(I18n.t('date.formats.default'))) :ends => ends.strftime(I18n.t('date.formats.default')))
@ -243,7 +243,7 @@ class Order < ActiveRecord::Base
for group_order in gos for group_order in gos
if group_order.ordergroup if group_order.ordergroup
price = group_order.price * -1 # decrease! account balance price = group_order.price * -1 # decrease! account balance
group_order.ordergroup.add_financial_transaction!(price, transaction_note, user) group_order.ordergroup.add_financial_transaction!(price, transaction_note, user, transaction_type)
end end
end end

View File

@ -55,9 +55,9 @@ class Ordergroup < Group
# Creates a new FinancialTransaction for this Ordergroup and updates the account_balance accordingly. # Creates a new FinancialTransaction for this Ordergroup and updates the account_balance accordingly.
# Throws an exception if it fails. # Throws an exception if it fails.
def add_financial_transaction!(amount, note, user, link = nil) def add_financial_transaction!(amount, note, user, transaction_type, link = nil)
transaction do transaction do
t = FinancialTransaction.new(ordergroup: self, amount: amount, note: note, user: user, financial_link: link) t = FinancialTransaction.new(ordergroup: self, amount: amount, note: note, user: user, financial_transaction_type: transaction_type, financial_link: link)
t.save! t.save!
self.account_balance = financial_transactions.sum('amount') self.account_balance = financial_transactions.sum('amount')
save! save!

View File

@ -1,10 +1,14 @@
-title t('.title') = form_tag close_finance_order_path(@order) do
%p!= t('.first_paragraph') %p!= t('.first_paragraph')
%table.table.table-striped{:style => "width:35em"} - if FinancialTransactionType.has_multiple_types
- for group_order in @order.group_orders %p
%tr{:class => cycle('even', 'odd')} %b= heading_helper FinancialTransaction, :financial_transaction_type
%td= group_order.ordergroup_name = select_tag :type, options_for_select(FinancialTransactionType.order(:name).map { |t| [ t.name, t.id ] })
%td.numeric= number_to_currency(group_order.price) %table.table.table-striped{:style => "width:35em"}
.form-actions - for group_order in @order.group_orders
= link_to t('.clear'), close_finance_order_path(@order), method: :patch, class: 'btn btn-primary' %tr{:class => cycle('even', 'odd')}
= link_to t('.or_cancel'), new_finance_order_path(order_id: @order.id) %td= group_order.ordergroup_name
%td.numeric= number_to_currency(group_order.price)
.form-actions
= submit_tag t('.clear'), class: 'btn btn-primary'
= link_to t('.or_cancel'), new_finance_order_path(order_id: @order.id)

View File

@ -5,6 +5,8 @@
= simple_form_for @financial_transaction, :url => finance_ordergroup_transactions_path(@ordergroup), = simple_form_for @financial_transaction, :url => finance_ordergroup_transactions_path(@ordergroup),
:validate => true do |f| :validate => true do |f|
= f.hidden_field :ordergroup_id = f.hidden_field :ordergroup_id
- if FinancialTransactionType.has_multiple_types
= f.association :financial_transaction_type, :as => :radio_buttons
= f.input :amount = f.input :amount
= f.input :note, :as => :text = f.input :note, :as => :text
.form-actions .form-actions

View File

@ -34,6 +34,10 @@
.well.well-small= t('.sidebar') .well.well-small= t('.sidebar')
= form_tag finance_create_transaction_collection_path do = form_tag finance_create_transaction_collection_path do
- if FinancialTransactionType.has_multiple_types
%p
%b= heading_helper FinancialTransaction, :financial_transaction_type
= select_tag :type, options_for_select(FinancialTransactionType.order(:name).map { |t| [ t.name, t.id ] })
%p %p
%b= heading_helper FinancialTransaction, :note %b= heading_helper FinancialTransaction, :note
= text_field_tag :note, params[:note], class: 'input-xlarge', required: 'required' = text_field_tag :note, params[:note], class: 'input-xlarge', required: 'required'

View File

@ -1401,6 +1401,8 @@ de:
model: model:
delivery: delivery:
each_stock_article_must_be_unique: Lieferung darf jeden Lagerartikel höchstens einmal auflisten. each_stock_article_must_be_unique: Lieferung darf jeden Lagerartikel höchstens einmal auflisten.
financial_transaction_type:
no_delete_last: Es muss mindestens ein Finanztransaktionstyp existieren.
group_order: group_order:
stock_ordergroup_name: Lager (%{user}) stock_ordergroup_name: Lager (%{user})
invoice: invoice:

View File

@ -1411,6 +1411,8 @@ en:
model: model:
delivery: delivery:
each_stock_article_must_be_unique: Each stock article must not be listed more than once. each_stock_article_must_be_unique: Each stock article must not be listed more than once.
financial_transaction_type:
no_delete_last: At least one financial transaction type must exist.
group_order: group_order:
stock_ordergroup_name: Stock (%{user}) stock_ordergroup_name: Stock (%{user})
invoice: invoice:

View File

@ -149,7 +149,7 @@ Foodsoft::Application.routes.draw do
put :update_note put :update_note
get :confirm get :confirm
patch :close post :close
patch :close_direct patch :close_direct
get :new_on_order_article_create get :new_on_order_article_create

View File

@ -0,0 +1,26 @@
class CreateFinancialTransactionClassAndTypes < ActiveRecord::Migration
def change
create_table :financial_transaction_classes do |t|
t.string :name, :null => false
end
create_table :financial_transaction_types do |t|
t.string :name, :null => false
t.references :financial_transaction_class, :null => false
end
change_table :financial_transactions do |t|
t.references :financial_transaction_type
end
reversible do |dir|
dir.up do
execute "INSERT INTO financial_transaction_classes (id, name) VALUES (1, 'Standard')"
execute "INSERT INTO financial_transaction_types (id, name, financial_transaction_class_id) VALUES (1, 'Foodsoft', 1)"
execute "UPDATE financial_transactions SET financial_transaction_type_id = 1"
end
end
change_column_null :financial_transactions, :financial_transaction_type_id, false
end
end

View File

@ -92,13 +92,23 @@ ActiveRecord::Schema.define(version: 20171110000000) do
t.text "note" t.text "note"
end end
create_table "financial_transaction_classes", force: :cascade do |t|
t.string "name", null: false
end
create_table "financial_transaction_types", force: :cascade do |t|
t.string "name", null: false
t.integer "financial_transaction_class_id", null: false
end
create_table "financial_transactions", force: :cascade do |t| create_table "financial_transactions", force: :cascade do |t|
t.integer "ordergroup_id", limit: 4, default: 0, null: false t.integer "ordergroup_id", limit: 4, default: 0, null: false
t.decimal "amount", precision: 8, scale: 2, default: 0, null: false t.decimal "amount", precision: 8, scale: 2, default: 0, null: false
t.text "note", limit: 65535, null: false t.text "note", limit: 65535, null: false
t.integer "user_id", limit: 4, default: 0, null: false t.integer "user_id", limit: 4, default: 0, null: false
t.datetime "created_on", null: false t.datetime "created_on", null: false
t.integer "financial_link_id" t.integer "financial_link_id"
t.integer "financial_transaction_type_id", null: false
end end
add_index "financial_transactions", ["ordergroup_id"], name: "index_financial_transactions_on_ordergroup_id", using: :btree add_index "financial_transactions", ["ordergroup_id"], name: "index_financial_transactions_on_ordergroup_id", using: :btree

View File

@ -21,5 +21,9 @@ User.create(
:groups => [administrators] :groups => [administrators]
) )
# First entry for financial transaction types
financial_transaction_class = FinancialTransactionClass.create(:name => "Other")
FinancialTransactionType.create(:name => "Foodcoop", :financial_transaction_class_id => financial_transaction_class.id)
# First entry for article categories # First entry for article categories
ArticleCategory.create(:name => "Other", :description => "other, misc, unknown") ArticleCategory.create(:name => "Other", :description => "other, misc, unknown")

View File

@ -178,8 +178,12 @@ seed_group_orders
## Finances ## Finances
FinancialTransaction.create(:id => 1, :ordergroup_id => 5, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00') FinancialTransactionClass.create(:id => 1, :name => "Other")
FinancialTransaction.create(:id => 3, :ordergroup_id => 6, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Sat, 25 Jan 2014 20:20:37 UTC +00:00')
FinancialTransaction.create(:id => 4, :ordergroup_id => 7, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00') FinancialTransactionType.create(:id => 1, :name => "Foodcoop", :financial_transaction_class_id => 1)
FinancialTransaction.create(:id => 5, :ordergroup_id => 5, :amount => 0.35E2, :note => "iDEAL payment", :user_id => 1, :created_on => 'Wed, 05 Feb 2014 16:49:24 UTC +00:00')
FinancialTransaction.create(:id => 6, :ordergroup_id => 8, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00') FinancialTransaction.create(:id => 1, :ordergroup_id => 5, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', :financial_transaction_type_id => 1)
FinancialTransaction.create(:id => 3, :ordergroup_id => 6, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', :financial_transaction_type_id => 1)
FinancialTransaction.create(:id => 4, :ordergroup_id => 7, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', :financial_transaction_type_id => 1)
FinancialTransaction.create(:id => 5, :ordergroup_id => 5, :amount => 0.35E2, :note => "iDEAL payment", :user_id => 1, :created_on => 'Wed, 05 Feb 2014 16:49:24 UTC +00:00', :financial_transaction_type_id => 1)
FinancialTransaction.create(:id => 6, :ordergroup_id => 8, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', :financial_transaction_type_id => 1)

View File

@ -178,8 +178,12 @@ seed_group_orders
## Finances ## Finances
FinancialTransaction.create(:id => 1, :ordergroup_id => 5, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00') FinancialTransactionClass.create(:id => 1, :name => "Other")
FinancialTransaction.create(:id => 3, :ordergroup_id => 6, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Sat, 25 Jan 2014 20:20:37 UTC +00:00')
FinancialTransaction.create(:id => 4, :ordergroup_id => 7, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00') FinancialTransactionType.create(:id => 1, :name => "Foodcoop", :financial_transaction_class_id => 1)
FinancialTransaction.create(:id => 5, :ordergroup_id => 5, :amount => 0.35E2, :note => "iDEAL payment", :user_id => 1, :created_on => 'Wed, 05 Feb 2014 16:49:24 UTC +00:00')
FinancialTransaction.create(:id => 6, :ordergroup_id => 8, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00') FinancialTransaction.create(:id => 1, :ordergroup_id => 5, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', :financial_transaction_type_id => 1)
FinancialTransaction.create(:id => 3, :ordergroup_id => 6, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', :financial_transaction_type_id => 1)
FinancialTransaction.create(:id => 4, :ordergroup_id => 7, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 0, :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', :financial_transaction_type_id => 1)
FinancialTransaction.create(:id => 5, :ordergroup_id => 5, :amount => 0.35E2, :note => "iDEAL payment", :user_id => 1, :created_on => 'Wed, 05 Feb 2014 16:49:24 UTC +00:00', :financial_transaction_type_id => 1)
FinancialTransaction.create(:id => 6, :ordergroup_id => 8, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', :financial_transaction_type_id => 1)

View File

@ -0,0 +1,14 @@
require 'factory_bot'
FactoryBot.define do
factory :financial_transaction_class do
sequence(:name) { |n| Faker::Lorem.characters(rand(2..12)) + " ##{n}" }
end
factory :financial_transaction_type do
financial_transaction_class
sequence(:name) { |n| Faker::Lorem.words(rand(2..4)).join(' ') + " ##{n}" }
end
end

View File

@ -1,6 +1,7 @@
require_relative '../spec_helper' require_relative '../spec_helper'
feature 'settling an order', js: true do feature 'settling an order', js: true do
let(:ftt) { create :financial_transaction_type }
let(:admin) { create :user, groups:[create(:workgroup, role_finance: true)] } let(:admin) { create :user, groups:[create(:workgroup, role_finance: true)] }
let(:user) { create :user, groups:[create(:ordergroup)] } let(:user) { create :user, groups:[create(:ordergroup)] }
let(:supplier) { create :supplier } let(:supplier) { create :supplier }

View File

@ -1,6 +1,7 @@
require_relative '../spec_helper' require_relative '../spec_helper'
feature 'product distribution', js: true do feature 'product distribution', js: true do
let(:ftt) { create :financial_transaction_type }
let(:admin) { create :admin } let(:admin) { create :admin }
let(:user_a) { create :user, groups: [create(:ordergroup)] } let(:user_a) { create :user, groups: [create(:ordergroup)] }
let(:user_b) { create :user, groups: [create(:ordergroup)] } let(:user_b) { create :user, groups: [create(:ordergroup)] }
@ -13,7 +14,7 @@ feature 'product distribution', js: true do
# make sure users have enough money to order # make sure users have enough money to order
[user_a, user_b].each do |user| [user_a, user_b].each do |user|
ordergroup = Ordergroup.find(user.ordergroup.id) ordergroup = Ordergroup.find(user.ordergroup.id)
ordergroup.add_financial_transaction! 5000, 'for ordering', admin ordergroup.add_financial_transaction! 5000, 'for ordering', admin, ftt
end end
order # make sure order is referenced order # make sure order is referenced
end end