From e7657b987f9929c60dcbc2d64a3fc7d09a17417c Mon Sep 17 00:00:00 2001 From: Patrick Gansterer Date: Sat, 4 Mar 2017 14:15:18 +0100 Subject: [PATCH] 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. --- .../finance/balancing_controller.rb | 3 ++- .../financial_transactions_controller.rb | 3 ++- app/models/financial_transaction.rb | 13 +++++++++- app/models/financial_transaction_class.rb | 6 +++++ app/models/financial_transaction_type.rb | 25 ++++++++++++++++++ app/models/order.rb | 4 +-- app/models/ordergroup.rb | 4 +-- app/views/finance/balancing/confirm.html.haml | 24 ++++++++++------- .../financial_transactions/new.html.haml | 2 ++ .../new_collection.html.haml | 4 +++ config/locales/de.yml | 2 ++ config/locales/en.yml | 2 ++ config/routes.rb | 2 +- ...e_financial_transaction_class_and_types.rb | 26 +++++++++++++++++++ db/schema.rb | 20 ++++++++++---- db/seeds/minimal.seeds.rb | 4 +++ db/seeds/small.en.seeds.rb | 14 ++++++---- db/seeds/small.nl.seeds.rb | 14 ++++++---- spec/factories/financial_transaction_type.rb | 14 ++++++++++ spec/integration/balancing_spec.rb | 1 + .../product_distribution_example_spec.rb | 3 ++- 21 files changed, 156 insertions(+), 34 deletions(-) create mode 100644 app/models/financial_transaction_class.rb create mode 100644 app/models/financial_transaction_type.rb create mode 100644 db/migrate/20160219123220_create_financial_transaction_class_and_types.rb create mode 100644 spec/factories/financial_transaction_type.rb diff --git a/app/controllers/finance/balancing_controller.rb b/app/controllers/finance/balancing_controller.rb index e4f6e137..11ab6e85 100644 --- a/app/controllers/finance/balancing_controller.rb +++ b/app/controllers/finance/balancing_controller.rb @@ -68,7 +68,8 @@ class Finance::BalancingController < Finance::BaseController # Balances the Order, Update of the Ordergroup.account_balances def close @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') rescue => error diff --git a/app/controllers/finance/financial_transactions_controller.rb b/app/controllers/finance/financial_transactions_controller.rb index 7e9e26ec..1141425f 100644 --- a/app/controllers/finance/financial_transactions_controller.rb +++ b/app/controllers/finance/financial_transactions_controller.rb @@ -55,10 +55,11 @@ class Finance::FinancialTransactionsController < ApplicationController def create_collection 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| # ignore empty amount fields ... 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 redirect_to finance_ordergroups_url, notice: I18n.t('finance.financial_transactions.controller.create_collection.notice') diff --git a/app/models/financial_transaction.rb b/app/models/financial_transaction.rb index 8ce83b0b..ba87c039 100644 --- a/app/models/financial_transaction.rb +++ b/app/models/financial_transaction.rb @@ -4,6 +4,7 @@ class FinancialTransaction < ActiveRecord::Base belongs_to :ordergroup belongs_to :user belongs_to :financial_link + belongs_to :financial_transaction_type validates_presence_of :amount, :note, :user_id, :ordergroup_id validates_numericality_of :amount, greater_then: -100_000, @@ -11,8 +12,18 @@ class FinancialTransaction < ActiveRecord::Base localize_input_of :amount + after_initialize do + initialize_financial_transaction_type + end + # Use this save method instead of simple save and after callback 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 diff --git a/app/models/financial_transaction_class.rb b/app/models/financial_transaction_class.rb new file mode 100644 index 00000000..0fe8b625 --- /dev/null +++ b/app/models/financial_transaction_class.rb @@ -0,0 +1,6 @@ +class FinancialTransactionClass < ActiveRecord::Base + has_many :financial_transaction_types, dependent: :destroy + + validates :name, presence: true + validates_uniqueness_of :name +end diff --git a/app/models/financial_transaction_type.rb b/app/models/financial_transaction_type.rb new file mode 100644 index 00000000..0d68ceb5 --- /dev/null +++ b/app/models/financial_transaction_type.rb @@ -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 diff --git a/app/models/order.rb b/app/models/order.rb index b9b8d2e5..e1205c77 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -231,7 +231,7 @@ class Order < ActiveRecord::Base end # 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? transaction_note = I18n.t('orders.model.notice_close', :name => name, :ends => ends.strftime(I18n.t('date.formats.default'))) @@ -243,7 +243,7 @@ class Order < ActiveRecord::Base for group_order in gos if group_order.ordergroup 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 diff --git a/app/models/ordergroup.rb b/app/models/ordergroup.rb index d314fc63..5754c879 100644 --- a/app/models/ordergroup.rb +++ b/app/models/ordergroup.rb @@ -55,9 +55,9 @@ class Ordergroup < Group # Creates a new FinancialTransaction for this Ordergroup and updates the account_balance accordingly. # 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 - 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! self.account_balance = financial_transactions.sum('amount') save! diff --git a/app/views/finance/balancing/confirm.html.haml b/app/views/finance/balancing/confirm.html.haml index 58f5ae1f..0e90e8f9 100644 --- a/app/views/finance/balancing/confirm.html.haml +++ b/app/views/finance/balancing/confirm.html.haml @@ -1,10 +1,14 @@ --title t('.title') -%p!= t('.first_paragraph') -%table.table.table-striped{:style => "width:35em"} - - for group_order in @order.group_orders - %tr{:class => cycle('even', 'odd')} - %td= group_order.ordergroup_name - %td.numeric= number_to_currency(group_order.price) -.form-actions - = link_to t('.clear'), close_finance_order_path(@order), method: :patch, class: 'btn btn-primary' - = link_to t('.or_cancel'), new_finance_order_path(order_id: @order.id) += form_tag close_finance_order_path(@order) do + %p!= t('.first_paragraph') + - 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 ] }) + %table.table.table-striped{:style => "width:35em"} + - for group_order in @order.group_orders + %tr{:class => cycle('even', 'odd')} + %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) diff --git a/app/views/finance/financial_transactions/new.html.haml b/app/views/finance/financial_transactions/new.html.haml index fe3e43cf..582e5443 100644 --- a/app/views/finance/financial_transactions/new.html.haml +++ b/app/views/finance/financial_transactions/new.html.haml @@ -5,6 +5,8 @@ = simple_form_for @financial_transaction, :url => finance_ordergroup_transactions_path(@ordergroup), :validate => true do |f| = f.hidden_field :ordergroup_id + - if FinancialTransactionType.has_multiple_types + = f.association :financial_transaction_type, :as => :radio_buttons = f.input :amount = f.input :note, :as => :text .form-actions diff --git a/app/views/finance/financial_transactions/new_collection.html.haml b/app/views/finance/financial_transactions/new_collection.html.haml index 40a1e4e0..7e157dee 100644 --- a/app/views/finance/financial_transactions/new_collection.html.haml +++ b/app/views/finance/financial_transactions/new_collection.html.haml @@ -34,6 +34,10 @@ .well.well-small= t('.sidebar') = 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 %b= heading_helper FinancialTransaction, :note = text_field_tag :note, params[:note], class: 'input-xlarge', required: 'required' diff --git a/config/locales/de.yml b/config/locales/de.yml index 939dcc0f..d92e67bf 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1401,6 +1401,8 @@ de: model: delivery: 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: stock_ordergroup_name: Lager (%{user}) invoice: diff --git a/config/locales/en.yml b/config/locales/en.yml index a0210aae..e8f6f796 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1411,6 +1411,8 @@ en: model: delivery: 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: stock_ordergroup_name: Stock (%{user}) invoice: diff --git a/config/routes.rb b/config/routes.rb index 5eb546bd..96d248b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -149,7 +149,7 @@ Foodsoft::Application.routes.draw do put :update_note get :confirm - patch :close + post :close patch :close_direct get :new_on_order_article_create diff --git a/db/migrate/20160219123220_create_financial_transaction_class_and_types.rb b/db/migrate/20160219123220_create_financial_transaction_class_and_types.rb new file mode 100644 index 00000000..65ac111f --- /dev/null +++ b/db/migrate/20160219123220_create_financial_transaction_class_and_types.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 86fafc00..029215f9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -92,13 +92,23 @@ ActiveRecord::Schema.define(version: 20171110000000) do t.text "note" 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| - t.integer "ordergroup_id", limit: 4, default: 0, null: false - t.decimal "amount", precision: 8, scale: 2, default: 0, null: false - t.text "note", limit: 65535, null: false - t.integer "user_id", limit: 4, default: 0, null: false - t.datetime "created_on", null: false + t.integer "ordergroup_id", limit: 4, default: 0, null: false + t.decimal "amount", precision: 8, scale: 2, default: 0, null: false + t.text "note", limit: 65535, null: false + t.integer "user_id", limit: 4, default: 0, null: false + t.datetime "created_on", null: false t.integer "financial_link_id" + t.integer "financial_transaction_type_id", null: false end add_index "financial_transactions", ["ordergroup_id"], name: "index_financial_transactions_on_ordergroup_id", using: :btree diff --git a/db/seeds/minimal.seeds.rb b/db/seeds/minimal.seeds.rb index b3724f30..e31461de 100644 --- a/db/seeds/minimal.seeds.rb +++ b/db/seeds/minimal.seeds.rb @@ -21,5 +21,9 @@ User.create( :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 ArticleCategory.create(:name => "Other", :description => "other, misc, unknown") diff --git a/db/seeds/small.en.seeds.rb b/db/seeds/small.en.seeds.rb index a95679ca..ccad542e 100644 --- a/db/seeds/small.en.seeds.rb +++ b/db/seeds/small.en.seeds.rb @@ -178,8 +178,12 @@ seed_group_orders ## 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') -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') -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') +FinancialTransactionClass.create(:id => 1, :name => "Other") + +FinancialTransactionType.create(:id => 1, :name => "Foodcoop", :financial_transaction_class_id => 1) + +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) diff --git a/db/seeds/small.nl.seeds.rb b/db/seeds/small.nl.seeds.rb index 318eafaf..c1b07d30 100644 --- a/db/seeds/small.nl.seeds.rb +++ b/db/seeds/small.nl.seeds.rb @@ -178,8 +178,12 @@ seed_group_orders ## 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') -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') -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') +FinancialTransactionClass.create(:id => 1, :name => "Other") + +FinancialTransactionType.create(:id => 1, :name => "Foodcoop", :financial_transaction_class_id => 1) + +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) diff --git a/spec/factories/financial_transaction_type.rb b/spec/factories/financial_transaction_type.rb new file mode 100644 index 00000000..45882ff2 --- /dev/null +++ b/spec/factories/financial_transaction_type.rb @@ -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 diff --git a/spec/integration/balancing_spec.rb b/spec/integration/balancing_spec.rb index 95591427..55434622 100644 --- a/spec/integration/balancing_spec.rb +++ b/spec/integration/balancing_spec.rb @@ -1,6 +1,7 @@ require_relative '../spec_helper' feature 'settling an order', js: true do + let(:ftt) { create :financial_transaction_type } let(:admin) { create :user, groups:[create(:workgroup, role_finance: true)] } let(:user) { create :user, groups:[create(:ordergroup)] } let(:supplier) { create :supplier } diff --git a/spec/integration/product_distribution_example_spec.rb b/spec/integration/product_distribution_example_spec.rb index e0ffea7d..ecc8570d 100644 --- a/spec/integration/product_distribution_example_spec.rb +++ b/spec/integration/product_distribution_example_spec.rb @@ -1,6 +1,7 @@ require_relative '../spec_helper' feature 'product distribution', js: true do + let(:ftt) { create :financial_transaction_type } let(:admin) { create :admin } let(:user_a) { 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 [user_a, user_b].each do |user| ordergroup = Ordergroup.find(user.ordergroup.id) - ordergroup.add_financial_transaction! 5000, 'for ordering', admin + ordergroup.add_financial_transaction! 5000, 'for ordering', admin, ftt end order # make sure order is referenced end