Add automatic generation of financial transactions and links

This commit is contained in:
Patrick Gansterer 2017-01-26 13:27:30 +01:00
parent 91eeac6c40
commit 8e2ca5e7d7
11 changed files with 284 additions and 0 deletions

View file

@ -5,6 +5,14 @@ class Finance::BankAccountsController < Finance::BaseController
redirect_to finance_bank_account_transactions_url(@bank_accounts.first) if @bank_accounts.count == 1
end
def assign_unlinked_transactions
@bank_account = BankAccount.find(params[:id])
count = @bank_account.assign_unlinked_transactions
redirect_to finance_bank_account_transactions_url(@bank_account), notice: t('finance.bank_accounts.controller.assign.notice', count: count)
rescue => error
redirect_to finance_bank_account_transactions_url(@bank_account), alert: t('errors.general_msg', msg: error.message)
end
def import
@bank_account = BankAccount.find(params[:id])
import_method = @bank_account.find_import_method

View file

@ -12,4 +12,14 @@ class BankAccount < ApplicationRecord
# @return [Function] Method wich can be called to import transaction from a bank or nil if unsupported
def find_import_method
end
def assign_unlinked_transactions
count = 0
bank_transactions.without_financial_link.includes(:supplier, :user).each do |t|
if t.assign_to_ordergroup || t.assign_to_invoice
count += 1
end
end
count
end
end

View file

@ -33,4 +33,48 @@ class BankTransaction < ApplicationRecord
def image_url
'data:image/png;base64,' + Base64.encode64(self.image)
end
def assign_to_invoice
return false unless supplier
content = text
content += "\n" + reference if reference.present?
invoices = supplier.invoices.unpaid.select {|i| content.include? i.number}
invoices_sum = invoices.map(&:amount).sum
return false if amount != -invoices_sum
transaction do
link = FinancialLink.new
invoices.each {|i| i.update_attributes! financial_link: link, paid_on: date }
update_attribute :financial_link, link
end
return true
end
def assign_to_ordergroup
m = BankTransactionReference.parse(reference)
return unless m
return false if m[:parts].values.sum != amount
group = Ordergroup.find_by_id(m[:group])
return false unless group
usr = m[:user] ? User.find_by_id(m[:user]) : group.users.first
return false unless usr
transaction do
note = "ID=#{id} (#{amount})"
link = FinancialLink.new
m[:parts].each do |short, value|
ftt = FinancialTransactionType.find_by_name_short(short)
return false unless ftt
group.add_financial_transaction! value, note, usr, ftt, link if value > 0
end
update_attribute :financial_link, link
end
return true
end
end

View file

@ -1,6 +1,7 @@
- title t('.title', name: @bank_account.name, balance: number_to_currency(@bank_account.balance))
- content_for :actionbar do
= link_to t('.assign_unlinked_transactions'), assign_unlinked_transactions_finance_bank_account_path(@bank_account), class: 'btn'
= link_to t('.import_transaction'), import_finance_bank_account_path(@bank_account), class: 'btn btn-primary'
.well.well-small

View file

@ -775,6 +775,8 @@ en:
without_extra_charge: 'without extra charge:'
bank_accounts:
controller:
assign:
notice: '%{count} transactions have been assigned'
import:
notice: '%{count} new transactions have been imported'
index:

View file

@ -195,6 +195,7 @@ Foodsoft::Application.routes.draw do
resources :bank_accounts, only: [:index] do
member do
get :assign_unlinked_transactions
get :import
end

View file

@ -0,0 +1,20 @@
class BankTransactionReference
# parses a string from a bank transaction field
def self.parse(data)
m = /(^|\s)FS(?<group>\d+)(\.(?<user>\d+))?(?<parts>([A-Za-z]+\d+(\.\d+)?)+)(\s|$)/.match(data)
return unless m
parts = {}
m[:parts].scan(/([A-Za-z]+)(\d+(\.\d+)?)/) do |category, value|
value = value.to_f
value += parts[category] if parts[category]
parts[category] = value
end
ret = { group: m[:group], parts: parts }
ret[:user] = m[:user] if m[:user]
return ret
end
end

View file

@ -0,0 +1,15 @@
require 'factory_bot'
FactoryBot.define do
factory :bank_account do
name { Faker::Bank.name }
iban { Faker::Bank.iban }
end
factory :bank_transaction do
date { Faker::Date.backward(days: 14) }
text { Faker::Lorem.sentence }
end
end

15
spec/factories/invoice.rb Normal file
View file

@ -0,0 +1,15 @@
require 'factory_bot'
FactoryBot.define do
factory :invoice do
supplier
number { rand(1..99999) }
amount { rand(0.1..26.0).round(2) }
after :create do |invoice|
invoice.supplier.reload
end
end
end

View file

@ -0,0 +1,60 @@
require_relative '../spec_helper'
describe BankTransactionReference do
it 'returns nil for empty input' do
expect(BankTransactionReference.parse('')).to be nil
end
it 'returns nil for invalid string' do
expect(BankTransactionReference.parse('invalid')).to be nil
end
it 'returns nil for FS1A' do
expect(BankTransactionReference.parse('FS1A')).to be nil
end
it 'returns nil for FS1.1A' do
expect(BankTransactionReference.parse('FS1.1A')).to be nil
end
it 'returns nil for xFS1A1' do
expect(BankTransactionReference.parse('xFS1A1')).to be nil
end
it 'returns nil for FS1A1x' do
expect(BankTransactionReference.parse('FS1A1x')).to be nil
end
it 'returns correct value for FS1A1' do
expect(BankTransactionReference.parse('FS1A1')).to be { { group: 1, parts: { A: 1 } } }
end
it 'returns correct value for FS1A1' do
expect(BankTransactionReference.parse('FS1.2A3')).to be { { group: 1, user: 2, parts: { A: 3 } } }
end
it 'returns correct value for FS1A2B3' do
expect(BankTransactionReference.parse('FS1A2B3C4')).to be { { group: 1, parts: { A: 2, B: 3, C: 4 } } }
end
it 'returns correct value for FS1A2B3A4' do
expect(BankTransactionReference.parse('FS1A2B3C4')).to be { { group: 1, parts: { A: 6, B: 3 } } }
end
it 'returns correct value for FS1A2.34B5.67C8.90' do
expect(BankTransactionReference.parse('FS1A2B3C4')).to be { { group: 1, parts: { A: 2.34, B: 5.67, C: 8.90 } } }
end
it 'returns correct value for FS123A456 with prefix' do
expect(BankTransactionReference.parse('x FS123A456')).to be { { group: 123, parts: { A: 456 } } }
end
it 'returns correct value for FS234A567 with suffix' do
expect(BankTransactionReference.parse('FS234A567 x')).to be { { group: 234, parts: { A: 567 } } }
end
it 'returns correct value for FS34.56A67.89 with prefix and suffix' do
expect(BankTransactionReference.parse('x FS34.56A67.89 x')).to be { { group: 34, user: 56, parts: { A: 67.89 } } }
end
end

View file

@ -0,0 +1,108 @@
require_relative '../spec_helper'
describe BankTransaction do
let(:bank_account) { create :bank_account }
let(:ordergroup) { create :ordergroup }
let(:supplier) { create :supplier, iban: Faker::Bank.iban }
let!(:user) { create :user, groups: [ordergroup] }
let!(:ftt_a) { create :financial_transaction_type, name_short: 'A' }
let!(:ftt_b) { create :financial_transaction_type, name_short: 'B' }
describe 'supplier' do
let!(:invoice1) { create :invoice, supplier: supplier, number: '11', amount: 10 }
let!(:invoice2) { create :invoice, supplier: supplier, number: '22', amount: 20 }
let!(:invoice3) { create :invoice, supplier: supplier, number: '33', amount: 30 }
let!(:invoice4) { create :invoice, supplier: supplier, number: '44', amount: 40 }
let!(:invoice5) { create :invoice, supplier: supplier, number: '55', amount: 50 }
let!(:bank_transaction1) { create :bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '11', amount: 10 }
let!(:bank_transaction2) { create :bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '22', amount: -20 }
let!(:bank_transaction3) { create :bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '33,44', amount: -70 }
let!(:bank_transaction4) { create :bank_transaction, bank_account: bank_account, iban: supplier.iban, text: '55', amount: -50 }
it 'ignores invoices with invalid amount' do
expect(bank_transaction1.assign_to_invoice).to be false
end
it 'can assign single invoice' do
expect(bank_transaction2.assign_to_invoice).to be true
invoice2.reload
expect(invoice2.paid_on).to eq bank_transaction2.date
expect(invoice2.financial_link).to eq bank_transaction2.financial_link
end
it 'can assign multiple invoice' do
expect(bank_transaction3.assign_to_invoice).to be true
[invoice3, invoice4].each(&:reload)
expect(invoice3.paid_on).to eq bank_transaction3.date
expect(invoice4.paid_on).to eq bank_transaction3.date
expect(invoice3.financial_link).to eq bank_transaction3.financial_link
expect(invoice4.financial_link).to eq bank_transaction3.financial_link
end
it 'can assign single invoice with number in text' do
expect(bank_transaction4.assign_to_invoice).to be true
invoice5.reload
expect(invoice5.paid_on).to eq bank_transaction4.date
expect(invoice5.financial_link).to eq bank_transaction4.financial_link
end
end
describe 'ordergroup' do
let!(:bank_transaction1) { create :bank_transaction, bank_account: bank_account, reference: "invalid", amount: 10 }
let!(:bank_transaction2) { create :bank_transaction, bank_account: bank_account, reference: "FS99A10", amount: 10 }
let!(:bank_transaction3) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}.99A10", amount: 10 }
let!(:bank_transaction4) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10", amount: 99 }
let!(:bank_transaction5) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10", amount: 10 }
let!(:bank_transaction6) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10B20", amount: 30 }
let!(:bank_transaction7) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}.#{user.id}A10", amount: 10 }
let!(:bank_transaction8) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}X10", amount: 10 }
it 'ignores transaction with invalid reference' do
expect(bank_transaction1.assign_to_ordergroup).to be nil
end
it 'ignores transaction with invalid ordergroup' do
expect(bank_transaction2.assign_to_ordergroup).to be false
end
it 'ignores transaction with invalid user' do
expect(bank_transaction3.assign_to_ordergroup).to be false
end
it 'ignores transaction with invalid sum' do
expect(bank_transaction4.assign_to_ordergroup).to be false
end
it 'add transaction with one part' do
expect(bank_transaction5.assign_to_ordergroup).to be true
ft_a = user.ordergroup.financial_transactions.where(financial_transaction_type: ftt_a).first
expect(ft_a.amount).to eq 10
expect(ft_a.financial_link).to eq bank_transaction5.financial_link
end
it 'add transaction with multiple parts' do
expect(bank_transaction6.assign_to_ordergroup).to be true
ft_a = user.ordergroup.financial_transactions.where(financial_transaction_type: ftt_a).first
ft_b = user.ordergroup.financial_transactions.where(financial_transaction_type: ftt_b).first
expect(ft_a.amount).to eq 10
expect(ft_a.financial_link).to eq bank_transaction6.financial_link
expect(ft_b.amount).to eq 20
expect(ft_b.financial_link).to eq bank_transaction6.financial_link
end
it 'add transaction with one part and user' do
expect(bank_transaction7.assign_to_ordergroup).to be true
ft_a = user.ordergroup.financial_transactions.where(financial_transaction_type: ftt_a).first
expect(ft_a.amount).to eq 10
expect(ft_a.financial_link).to eq bank_transaction7.financial_link
end
it 'ignores transaction with invalid short name' do
expect(bank_transaction8.assign_to_ordergroup).to be false
end
end
end