Add BankAccountConnector to implement bank import methods in plugins

This commit is contained in:
Patrick Gansterer 2020-02-24 14:22:58 +01:00
parent d476993321
commit 5d84156bd8
11 changed files with 281 additions and 22 deletions

View file

@ -15,16 +15,36 @@ class Finance::BankAccountsController < Finance::BaseController
def import
@bank_account = BankAccount.find(params[:id])
import_method = @bank_account.find_import_method
if import_method
count = import_method.call(@bank_account)
redirect_to finance_bank_account_transactions_url(@bank_account), notice: t('.notice', count: count)
importer = @bank_account.find_connector
if importer
importer.load params[:state] && YAML.load(params[:state])
ok = importer.import params[:controls]
importer.finish if ok
flash.notice = t('.notice', count: importer.count) if ok
@auto_submit = importer.auto_submit
@controls = importer.controls
#TODO: encrypt state
@state = YAML.dump importer.dump
else
# @todo add import for csv files as fallback
redirect_to finance_bank_account_transactions_url(@bank_account), alert: t('.no_import_method')
ok = true
flash.alert = t('.no_import_method')
end
needs_redirect = ok
rescue => error
redirect_to finance_bank_account_transactions_url(@bank_account), alert: t('errors.general_msg', msg: error.message)
flash.alert = t('errors.general_msg', msg: error.message)
needs_redirect = true
ensure
return unless needs_redirect
redirect_path = finance_bank_account_transactions_url(@bank_account)
if request.post?
@js_redirect = redirect_path
else
redirect_to redirect_path
end
end
end

View file

@ -10,7 +10,9 @@ class BankAccount < ApplicationRecord
validates_numericality_of :balance, :message => I18n.t('bank_account.model.invalid_balance')
# @return [Function] Method wich can be called to import transaction from a bank or nil if unsupported
def find_import_method
def find_connector
klass = BankAccountConnector.find iban
return klass.new self if klass
end
def assign_unlinked_transactions

View file

@ -0,0 +1,33 @@
= form_tag import_finance_bank_account_path(@bank_account), class: 'form-horizontal',
data: { auto_submit: @auto_submit}, id: 'import_form', method: :post, remote: true do
= hidden_field_tag :import_uid, @import_uid
= hidden_field_tag :state, @state
- for control in @controls
- name = control.name
.control-group
- if name
- if control.type == :hidden
= hidden_field_tag "controls[#{control.name}]", control.value
- else
%label(for=name class='control-label')
= control.label + ':'
.controls
- if control.type == :password
= password_field_tag "controls[#{control.name}]", control.value
-else
= text_field_tag "controls[#{control.name}]", control.value
- else
= control.text
- if @auto_submit
:javascript
var form = $('#import_form');
setTimeout(function() {
form.submit();
}, form.data('auto-submit'));
- else
.control-group
.controls
= submit_tag t('.submit'), class: 'btn btn-primary'

View file

@ -1,12 +1,4 @@
- title t('.title', name: @bank_account.name)
= form_for :bank_accounts, :url => parse_upload_finance_bank_account_path(@bank_account),
:html => { multipart: true, class: "form-horizontal" } do |f|
.control-group
%label(for="bank_transactions_file")= t '.file_label'
= f.file_field "file"
.form-actions
= submit_tag t('.submit'), class: 'btn btn-primary'
= link_to t('ui.or_cancel'), finance_bank_account_transactions_path(@bank_account)
#import
= render "import"

View file

@ -0,0 +1,4 @@
- if @js_redirect
document.location.replace('#{escape_javascript(@js_redirect)}');
- else
$('#import').html('#{escape_javascript(render("import"))}');

View file

@ -516,6 +516,13 @@ de:
text_1: 'Du kannst hier eine Tabelle hochladen, um die Artikel des Lieferanten %{supplier} zu aktualisieren. Excel (xls, xlsx) und OpenOffice (ods) Tabellen werden akzeptiert, darüber hinaus Dateien im Format "csv" (comma-separated values, mit dem Spaltentrennzeichen ";" und utf-8 Kodierung). Nur das erste Tabellenblatt wird importiert und die Spalten müssen in der folgenden Anordnung vorliegen:'
text_2: Die hier gezeigten Spalten sind Beispiele. Ist ein "x" in der ersten Spalte, wird der Artikel aussortiert und entfernt. Das erlaubt Dir die Tabelle zu ändern und schnell viele Artikel auf ein Mal zu entfernen, zum Beispiel wenn Artkiel des Lieferanten nicht mehr verfügbar sind. Die Kategorie wird der Foodsoft Kategorie zugeordnet (durch die Kategorienamen und die Importnamen).
title: Artikel des Lieferanten %{supplier} hochladen
bank_account_connector:
confirm_app: Bitte bestätige den Code %{code} in deiner App.
fields:
email: E-Mail
pin: PIN
password: Passwort
username: Benutzername
config:
hints:
applepear_url: Seite, auf der das Äpfel- und Birnensystem für Aufgaben erklärt wird.
@ -781,6 +788,8 @@ de:
import:
notice: 'Es wurden %{count} neue Transaktionen importiert.'
no_import_method: Für dieses Bankkonto ist keine Importmethode konfiguriert.
submit: Abschicken
title: Banktransaktionen für %{name} importieren
index:
title: Bankkonten
bank_transactions:

View file

@ -536,6 +536,13 @@ en:
text_1: 'Here you can upload a spreadsheet to update the articles of %{supplier}. Excel (xls, xlsx) and OpenOffice (ods) spreadsheets are accepted, as well as comma-separated files (csv, columns separated by ";" with utf-8 encoding). Only the first sheet will be imported, and columns must be in the following order:'
text_2: The rows shown here are examples. When there is an "x" in the first column, the article is outlisted and will be removed. This allows you to edit the spreadsheet and quickly remove many articles at once, for example when articles become unavailable with the supplier. The category will be matched to your Foodsoft category list (both by category name and import names).
title: Upload articles of %{supplier}
bank_account_connector:
confirm_app: Please confirum the code %{code} in your app.
fields:
email: E-Mail
pin: PIN
password: Password
username: Username
config:
hints:
applepear_url: Website where the apple and pear system for tasks is explained.
@ -806,6 +813,8 @@ en:
import:
notice: '%{count} new transactions have been imported'
no_import_method: For this bank account no import method is configured.
submit: Abschicken
title: Import bank transactions for %{name}
index:
title: Bank Accounts
bank_transactions:

View file

@ -197,6 +197,7 @@ Foodsoft::Application.routes.draw do
member do
get :assign_unlinked_transactions
get :import
post :import
end
resources :bank_transactions, as: :transactions

View file

@ -0,0 +1,155 @@
class BankAccountConnector
class TextItem
def initialize(text)
@text = text
end
def name
nil
end
def text
@text
end
end
class TextField
def initialize(name, value, label)
@name = name
@value = value
@label = label
end
def type
nil
end
def name
@name
end
def value
@value
end
def label
@label || @name.to_s
end
end
class PasswordField < TextField
def type
:password
end
end
class HiddenField < TextField
def type
:hidden
end
end
@@registered_classes = Set.new
def self.register(klass)
@@registered_classes.add klass
end
def self.find(iban)
@@registered_classes.each do |klass|
return klass if klass.handles(iban)
end
nil
end
def initialize(bank_account)
@bank_account = bank_account
@auto_submit = nil
@controls = []
@count = 0
end
def iban
@bank_account.iban
end
def auto_submit
@auto_submit
end
def controls
@controls
end
def count
@count
end
def text(data)
@controls += [TextItem.new(data)]
end
def wait_with_text(auto_submit, data)
@auto_submit = auto_submit
@controls += [TextItem.new(data)]
end
def wait_for_app(code)
hidden_field :twofactor, code
wait_with_text 3000, t('.confirm_app', code: code)
nil
end
def text_field(name, value=nil)
@controls += [TextField.new(name, value, t(name))]
end
def hidden_field(name, value)
@controls += [HiddenField.new(name, value, 'HIDDEN')]
end
def password_field(name, value=nil)
@controls += [PasswordField.new(name, value, t(name))]
end
def set_balance(amount)
@bank_account.balance = amount
end
def continuation_point
@bank_account.import_continuation_point
end
def set_continuation_point(data)
@bank_account.import_continuation_point = data
end
def update_or_create_transaction(external_id, data={})
@bank_account.bank_transactions.where(external_id: external_id).first_or_create.update(data)
@count += 1
end
def finish
@bank_account.last_import = Time.now
@bank_account.save!
end
def load(data)
end
def dump
end
def t(key, args={})
return t(".fields.#{key}") unless key.is_a? String
I18n.t 'bank_account_connector' + key, args
end
end

View file

@ -0,0 +1,31 @@
class BankAccountConnectorExternal < BankAccountConnector
def load(data)
@connector = create_connector
@connector.load data
end
def dump
@connector.dump
end
def connector_import
set_balance @connector.balance iban
cp = @connector.transactions iban, continuation_point do |t|
update_or_create_transaction t[:id], map_transaction(t)
end
set_continuation_point cp if cp
end
def connector_logout
@connector.logout
end
def import(data)
return false unless connector_login(data)
connector_import
connector_logout
true
end
end

View file

@ -67,11 +67,14 @@ namespace :foodsoft do
desc "Import and assign bank transactions"
task :import_and_assign_bank_transactions => :environment do
BankAccount.find_each do |ba|
import_method = ba.find_import_method
next unless import_method
import_count = import_method.call(ba)
importer = ba.find_connector
next unless importer
importer.load nil
ok = importer.import nil
next unless ok
importer.finish
assign_count = ba.assign_unlinked_transactions
rake_say "#{ba.name}: imported #{import_count}, assigned #{assign_count}"
rake_say "#{ba.name}: imported #{importer.count}, assigned #{assign_count}"
end
end
end