Tvorba parseru bankovních výpisů
Tento návod popisuje jak vytvořit parser bankovních výpisů integrovaný do systému FreenetIS.
Postup parseru pro import ze souboru
Budeme vytvářet parser pro banku ABC, který je schopný parsovat výpisy ve formátu CSV.
- Ve složce /application/libraries/importers vytvoříme soubor Abc_Csv_Bank_Statement_File_Importer.php, do kterého vložíme kostru PHP třídy reprezentující parser a jejíž metody implementujeme:
<?php defined('SYSPATH') or die('No direct script access.'); /* * This file is part of open source system FreenetIS * and it is released under GPLv3 licence. * * More info about licence can be found: * http://www.gnu.org/licenses/gpl-3.0.html * * More info about project can be found: * http://www.freenetis.org/ * */ /** * Parser for ABC bank that parses CSV files. * * @author <vaše jméno> */ class Abc_Csv_Bank_Statement_File_Importer extends Bank_Statement_File_Importer { protected function check_file_data_format() { } protected function get_header_data() { } protected function parse_file_data() { } protected function store(&$stats = array()) { } }
- Metoda check_file_data_format kontroluje formát vstupního souboru, jehož obsah ve formě řetězce lze získat pomocí volání $this->get_file_data(). Metoda vrací TRUE pokud je soubor v pořádku jinak FALSE.
- Metoda get_header_data slouží pro získání hlavičky výpisu (číslo bank. účtu, zůstatky, apod.). Hlavičkou je míněna instance třídy Header_Data. Příklad implementace metody:
protected function get_header_data() { // parsování hlavičky z get_file_data .... $hd = new Header_Data(<číslo bank. účtu>, <číslo banky>); // nepovinné vlastnosti, které se mohou používat v dalších metodách $hd->currency = 'CZK'; $hd->openingBalance = <poč stav>; $hd->closingBalance = <konečný stav>; $hd->dateStart = <datum od>; $hd->dateEnd = <datum do>; // ... return $hd; }
- Metoda parse_file_data realizuje samotné parsování výpisu a dočasné uložení jeho obsahu v libovolném formátu uvnitř vytvořené třídy. K uloženému obsahu poté přistupujeme (ukládáme) v poslední metodě store. Obvykle si pro potřeby uložení vytvoříme ve třídě členskou proměnnou. Funkce vrací TRUE pokud parsování proběhlo v pořádku jinak FALSE. Chyby lze jako u ostatních metod hlásit zděděnými metodami add_error(message) nebo add_exception_error(exception).
- Metoda store ukládá připravená rozparsovaná data do databáze FreenetISu. Při její implementaci je nutné znát vnitřní mechanismy účtování FreenetISu. Nejjednodušší implementace, která umožňuje pouze přijímat členské příspěvky je následující:
protected function store(&$stats = array()) { $statement = new Bank_statement_Model(); $ba = $this->get_bank_account(); $user_id = $this->get_user_id(); try { /* header */ $statement->transaction_start(); $header = $this->get_header_data(); // bank statement $statement->bank_account_id = $ba->id; $statement->user_id = $this->get_user_id(); $statement->type = $this->get_importer_name(); $statement->from = $header->dateStart; $statement->to = $header->dateEnd; $statement->save_throwable(); /* transactions */ // preparation of system double-entry accounts $member_fees = ORM::factory('account')->get_account_by_attribute(Account_attribute_Model::MEMBER_FEES); $operating = ORM::factory('account')->get_account_by_attribute(Account_attribute_Model::OPERATING); $account = $ba->get_related_account_by_attribute_id(Account_attribute_Model::BANK); // model preparation $bt = new Bank_transfer_Model(); $fee_model = new Fee_Model(); // statistics preparation $stats['unidentified_nr'] = 0; $stats['invoices'] = 0; $stats['invoices_nr'] = 0; $stats['member_fees'] = 0; $stats['member_fees_nr'] = 0; $stats['interests'] = 0; $stats['interests_nr'] = 0; $stats['deposits'] = 0; $stats['deposits_nr'] = 0; // miscellaneous preparation $now = date('Y-m-d H:i:s'); $number = 0; // saving each bank listing item foreach ($this->preparedData as $item) //// ZDE PŘISTUPUJEME K PŘEDPARSOVANÝM DATŮM { // try to find counter bank account in database $counter_ba = ORM::factory('bank_account')->where(array ( 'account_nr' => $item['protiucet'], 'bank_nr' => $item['kod_banky'] ))->find(); // counter bank account does not exist? let's create new one if (!$counter_ba->id) { $counter_ba->clear(); $counter_ba->set_logger(FALSE); $counter_ba->name = $item['nazev_protiuctu']; $counter_ba->account_nr = $item['protiucet']; $counter_ba->bank_nr = $item['kod_banky']; $counter_ba->member_id = NULL; $counter_ba->save_throwable(); } // inbound transfer - member fee // let's identify member $member_id = $this->find_member_by_vs($item['vs']); if (!$member_id) { $stats['unidentified_nr']++; } // double-entry incoming transfer $transfer_id = Transfer_Model::insert_transfer( $member_fees->id, $account->id, null, $member_id, $user_id, null, $item['datum'], $now, $item['zprava'], abs($item['castka']) ); // incoming bank transfer $bt->clear(); $bt->set_logger(false); $bt->origin_id = $counter_ba->id; $bt->destination_id = $ba->id; $bt->transfer_id = $transfer_id; $bt->bank_statement_id = $statement->id; $bt->transaction_code = $item['id_pohybu']; $bt->number = $number; $bt->constant_symbol = $item['ks']; $bt->variable_symbol = $item['vs']; $bt->specific_symbol = $item['ss']; $bt->save(); // assign transfer? (0 - invalid id, 1 - assoc id, other are ordinary members) if ($member_id && $member_id != Member_Model::ASSOCIATION) { $ca = ORM::factory('account')->where('member_id', $member_id)->find(); // has credit account? if ($ca->id) { // add affected member for notification $this->add_affected_member($member_id); // assigning transfer $a_transfer_id = Transfer_Model::insert_transfer( $account->id, $ca->id, $transfer_id, $member_id, $user_id, null, $item['datum'], $now, __('Assigning of transfer'), abs($item['castka']) ); // transaction fee $fee = $fee_model->get_by_date_type($item['datum'], 'transfer fee'); if ($fee && $fee->fee > 0) { $tf_transfer_id = Transfer_Model::insert_transfer( $ca->id, $operating->id, $transfer_id, $member_id, $user_id, null, $item['datum'], $now, __('Transfer fee'), $fee->fee ); } if (!$counter_ba->member_id) { $counter_ba->member_id = $member_id; $counter_ba->save_throwable(); } } } // member fee stats $stats['member_fees'] += abs($item['castka']); $stats['member_fees_nr']++; // line number increase $number++; } // done $statement->transaction_commit(); // return return $statement; } catch (Duplicity_Exception $e) { $statement->transaction_rollback(); throw $e; } catch (Exception $e) { $statement->transaction_rollback(); Log::add_exception($e); $this->add_exception_error($e); return NULL; } }
Implementace metody store je složitější, a proto je vhodné ji konzultovat s vývojáři zodpovědnými za účetnictví.
Použití zhotoveného parseru (třídy)
Parser je strukturován do výše uvedených pevně předepsaných metod, aby bylo možné jeho životní cyklus řídit automaticky dalšími částmi systému. Nemusíme díky tomu řešit načítání souboru, hlášení chyb, oznámení o přijaté platbě, aj.
Implementovaný parser je nutné zaregistrovat v souboru /application/libraries/Bank_Statement_File_Importer.php v proměnné $drivers. Příklad:
private static $drivers = array ( // ..... /* ABC - CSV - Obtained from ABC e-banking */ array ( 'name' => 'ABC CSV importer', // název 'class' => 'Abc_Csv_Bank_Statement_File_Importer', // třída 'bank_type' => Bank_account_Model::TYPE_ABC, 'extensions' => array('csv') ) );
Pokud není typ banky ABC v systému zaveden, je nutné v souboru /application/models/Bank_account_Model.php typ dodefinovat, což obnáší definici nové konstanty pro typ a název typu v proměnné $type_name. Typem se zajišťuje, aby nebyl použit pro import výpisu nevhodný parser vzhledem k bankovnímu účtu.
Posledním krokem je modifikace kontroleru import (/application/controllers/import.php), ve kterém se lze plně inspirovat jeho částí pro FIO parser.
Postup parseru pro import z API
Značně se liší dle příslušného API. Oproti souboru je navíc nutné implementovat nastavení přístupu k API u bank. účtu a někdy také v parseru implementovat metodu before_download, která připravuje půdu pro stáhnutí souboru z API. Příklad hledejte u FIO parseru.