freenetis-github/application/libraries/importers/Fio/NewFioCsvParser.php @ a29b0d63
a29b0d63 | Michal Kliment | <?php
|
|
/*
|
|||
* 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/
|
|||
*
|
|||
*/
|
|||
/**
|
|||
* Auxiliary class for parsing CSV bank account listings from czech bank
|
|||
* "FIO banka". Listing may be obtain from from the ebanking web application
|
|||
* of FIO bank.
|
|||
*
|
|||
* The CSV format looks like this:
|
|||
* "Datum";"ID operace";"ID pokynu";"KS";"Název banky";"Název protiúčtu";
|
|||
* "Objem";"Měna";"Protiúčet";"Kód banky";"Zadal";"SS";"Typ";"Poznámka";
|
|||
* "VS";"Upřesnění - objem";"Upřesnění - měna";"Zpráva pro příjemce"
|
|||
* "Suma";"";"";"";"";"";"-188170,4";"CZK";"";"";"";"";"";"";"";"";"";""
|
|||
*
|
|||
* @author Ondřej Fibich <fibich@freenetis.org>
|
|||
* @since 1.1.11
|
|||
* @todo i18n of error messages
|
|||
*/
|
|||
class NewFioCsvParser
|
|||
{
|
|||
/**
|
|||
* CSV column separator.
|
|||
*/
|
|||
const CSV_COL_DELIM = ';';
|
|||
/**
|
|||
* CSV column value wrapper.
|
|||
*/
|
|||
const CSV_COL_WRAPPER = '"';
|
|||
/**
|
|||
* Last line date string that is used for end of statement detection.
|
|||
*/
|
|||
const HEADER_LINE_DATE_VALUE = 'Suma';
|
|||
/**
|
|||
* Default CSV file encoding.
|
|||
*/
|
|||
const DEFAULT_CHARSET = 'UTF-8';
|
|||
/**
|
|||
* All fields available must be used and sorted alphabetically
|
|||
*
|
|||
* @var array[string]
|
|||
*/
|
|||
private static $fields = array
|
|||
(
|
|||
'datum' => 'Datum',
|
|||
'id_pohybu' => 'ID operace',
|
|||
'id_pokynu' => 'ID pokynu',
|
|||
'ks' => 'KS',
|
|||
'nazev_banky' => 'Název banky',
|
|||
'nazev_protiuctu' => 'Název protiúčtu',
|
|||
'castka' => 'Objem',
|
|||
'mena' => 'Měna',
|
|||
'protiucet' => 'Protiúčet',
|
|||
'kod_banky' => 'Kód banky',
|
|||
'provedl' => 'Zadal',
|
|||
'ss' => 'SS',
|
|||
'typ' => 'Typ',
|
|||
'identifikace' => 'Poznámka',
|
|||
'vs' => 'VS',
|
|||
'upresneni_objem' => 'Upřesnění - objem',
|
|||
'upresneni_mena' => 'Upřesnění - měna',
|
|||
'zprava' => 'Zpráva pro příjemce'
|
|||
);
|
|||
/**
|
|||
* FIO statement columns fields names.
|
|||
*
|
|||
* @return array
|
|||
*/
|
|||
public static function get_fields()
|
|||
{
|
|||
return self::$fields;
|
|||
}
|
|||
/**
|
|||
* Parse bank statement in CSV format that is passed as string.
|
|||
*
|
|||
* @param string $csv string containing the original csv file.
|
|||
* @param string $charset optional charset name of file, default is UTF-8
|
|||
* @return array[array] Integer-indexed array of associative arrays.
|
|||
* Each associative array represents one line of the CSV
|
|||
* @throws Exception on parse error
|
|||
*/
|
|||
public function parse($csv, $charset = self::DEFAULT_CHARSET)
|
|||
{
|
|||
$total_sum = -1;
|
|||
$sum = 0;
|
|||
$keys = array_keys(self::$fields);
|
|||
$lines = self::transformFileToLineArray($csv, $charset);
|
|||
$result = array();
|
|||
// check each line of CSV
|
|||
for ($i = 0; $i < count($lines); $i++)
|
|||
{
|
|||
$line = trim($lines[$i]);
|
|||
// header
|
|||
if ($i == 0)
|
|||
{
|
|||
$this->checkHeaders($line);
|
|||
}
|
|||
else if ($i == 1)
|
|||
{
|
|||
$total_sum = $this->parseHeaderLine($line);
|
|||
}
|
|||
else if (empty($line)) // empty last line?
|
|||
{
|
|||
break;
|
|||
}
|
|||
else
|
|||
{
|
|||
// data lines
|
|||
$cols = $this->parseLine($line, $keys);
|
|||
// add data row
|
|||
$sum += $cols['castka'];
|
|||
$result[] = $cols;
|
|||
}
|
|||
}
|
|||
$this->checkIntegrity($total_sum, $sum);
|
|||
return $result;
|
|||
}
|
|||
/**
|
|||
* Check whether parser accept given CSV file.
|
|||
*
|
|||
* @param string $csv string containing the original csv file.
|
|||
* @param string $charset optional charset name of file, default is UTF-8
|
|||
* @return boolean
|
|||
*/
|
|||
public function accept_file($csv, $charset = self::DEFAULT_CHARSET)
|
|||
{
|
|||
$lines = self::transformFileToLineArray($csv, $charset);
|
|||
if (count($lines) < 3)
|
|||
{
|
|||
return FALSE;
|
|||
}
|
|||
try
|
|||
{
|
|||
$this->checkHeaders(trim($lines[0]));
|
|||
$this->parseHeaderLine(trim($lines[1]));
|
|||
return TRUE;
|
|||
}
|
|||
catch (Exception $ex)
|
|||
{
|
|||
return FALSE;
|
|||
}
|
|||
}
|
|||
/**
|
|||
* Parse statement header line with total sum.
|
|||
*
|
|||
* @param string $line_str raw CSV line
|
|||
*/
|
|||
private function parseHeaderLine($line_str)
|
|||
{
|
|||
$fields_keys = array_keys(self::$fields);
|
|||
$columns = $this->parseLine($line_str, $fields_keys, FALSE);
|
|||
if ($columns['datum'] != self::HEADER_LINE_DATE_VALUE)
|
|||
{
|
|||
throw new Exception('Chybná hlavička výpisu.');
|
|||
}
|
|||
if (empty($columns['castka']))
|
|||
{
|
|||
throw new Exception('Chybná hlavička výpisu (suma).');
|
|||
}
|
|||
return $columns['castka'];
|
|||
}
|
|||
/**
|
|||
* Checks headers that start data part of statement.
|
|||
*
|
|||
* @param integer $header_line header line
|
|||
* @throws Exception
|
|||
*/
|
|||
private function checkHeaders($header_line)
|
|||
{
|
|||
$em = __("Nelze parsovat hlavičku Fio výpisu. Ujistěte se, že jste "
|
|||
. "zvolili všech " . count(self::$fields) . " sloupců k importu "
|
|||
. "v internetovém bankovnictví.");
|
|||
$expected_header_cols = array_values(self::$fields);
|
|||
// first column has issue with some UTF-8 characters
|
|||
$fix_hl = preg_replace('/^[\x00-\x1F\x80-\xFF]+/', '', $header_line);
|
|||
// extract header
|
|||
$header_cols = str_getcsv($fix_hl, self::CSV_COL_DELIM,
|
|||
self::CSV_COL_WRAPPER);
|
|||
// check if extracted
|
|||
if (empty($header_cols))
|
|||
{
|
|||
throw new Exception($em);
|
|||
}
|
|||
// check if count match
|
|||
if (count($header_cols) != count($expected_header_cols))
|
|||
{
|
|||
throw new Exception($em);
|
|||
}
|
|||
// check each column
|
|||
for ($i = count($header_cols) - 1; $i >= 0; $i--)
|
|||
{
|
|||
if ($header_cols[$i] != $expected_header_cols[$i])
|
|||
{
|
|||
throw new Exception($em);
|
|||
}
|
|||
}
|
|||
}
|
|||
/**
|
|||
* Parse line of dump
|
|||
*
|
|||
* @param string $line
|
|||
* @param array $keys
|
|||
* @param boolean $parse_date
|
|||
* @return array
|
|||
* @throws Exception
|
|||
*/
|
|||
private function parseLine($line, $keys, $parse_date = TRUE)
|
|||
{
|
|||
$cols = str_getcsv($line, self::CSV_COL_DELIM, self::CSV_COL_WRAPPER);
|
|||
if (count($cols) != count($keys))
|
|||
{
|
|||
throw new Exception('Chybný počet políček v položce výpisu.');
|
|||
}
|
|||
// Convert to associative array
|
|||
$assoc_cols = array_combine($keys, $cols);
|
|||
// Convert date
|
|||
if ($parse_date)
|
|||
{
|
|||
$assoc_cols['datum'] = self::parseDate($assoc_cols['datum']);
|
|||
}
|
|||
// Amount has to be converted
|
|||
$assoc_cols['castka'] = self::parseAmount($assoc_cols['castka']);
|
|||
// Trim leading zeros from VS
|
|||
$assoc_cols['vs'] = ltrim($assoc_cols['vs'], '0');
|
|||
// join both "upresneni"
|
|||
$assoc_cols['upresneni'] = trim($assoc_cols['upresneni_objem'] . ' ' .
|
|||
$assoc_cols['upresneni_mena']);
|
|||
// column prevod N/A
|
|||
$assoc_cols['prevod'] = NULL;
|
|||
return $assoc_cols;
|
|||
}
|
|||
/**
|
|||
* Checks parsed money amount agains data from integrity line.
|
|||
*
|
|||
* @param array $sum
|
|||
* @param integer $calculated_sum total counted sum
|
|||
* @throws Exception on integrity error
|
|||
*/
|
|||
private function checkIntegrity($sum, $calculated_sum)
|
|||
{
|
|||
if (abs($sum - $calculated_sum) > 0.0001)
|
|||
{
|
|||
throw new Exception("Chybný kontrolní součet částky "
|
|||
. "('$calculated_sum' != '$sum').");
|
|||
}
|
|||
}
|
|||
/**
|
|||
* Normalize string amount to double value.
|
|||
*
|
|||
* @example " 1000 278,40 " -> "1000278.40"
|
|||
* @param string $amount
|
|||
* @return double
|
|||
* @throws InvalidArgumentException on invalid passed amount
|
|||
*/
|
|||
public static function parseAmount($amount)
|
|||
{
|
|||
$norm_amount = str_replace(array(' ', ','), array('', '.'), $amount);
|
|||
if (!is_numeric($norm_amount))
|
|||
{
|
|||
$m = __('Invalid amount format') . ': ' . $amount;
|
|||
throw new InvalidArgumentException($m);
|
|||
}
|
|||
return doubleval($norm_amount);
|
|||
}
|
|||
/**
|
|||
* Parse date from format DD.MM.YYYY into YYYY-MM-DD.
|
|||
*
|
|||
* @param string $date in format DD.MM.YYYY
|
|||
* @return string date in format YYYY-MM-DD
|
|||
* @throws InvalidArgumentException on invalid date format
|
|||
*/
|
|||
public static function parseDate($date)
|
|||
{
|
|||
$matches = NULL;
|
|||
if (!preg_match("/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/", $date, $matches))
|
|||
{
|
|||
$m = __('Invalid date format') . ': ' . $date;
|
|||
throw new InvalidArgumentException($m);
|
|||
}
|
|||
$timestamp = mktime(0, 0, 0, $matches[2], $matches[1], $matches[3]);
|
|||
return date('Y-m-d', $timestamp);
|
|||
}
|
|||
/**
|
|||
* Transforms file content in passed charset into array of its lines encoded
|
|||
* in UTF-8 encoding. This function must handle differences of end of line
|
|||
* separators on all platforms.
|
|||
*
|
|||
* @param string $file_content file countent to be transformed
|
|||
* @param string $charset charset of file content
|
|||
* @return array array of lines in UTF-8 charset
|
|||
*/
|
|||
public static function transformFileToLineArray($file_content, $charset)
|
|||
{
|
|||
$internal_charset = 'UTF-8';
|
|||
$fc_utf8 = NULL;
|
|||
// transform to uTF-8
|
|||
if (strtolower($charset) != strtolower($internal_charset))
|
|||
{
|
|||
$fc_utf8 = iconv($charset, $internal_charset, $file_content);
|
|||
}
|
|||
else
|
|||
{
|
|||
$fc_utf8 = $file_content;
|
|||
}
|
|||
// eplode lines
|
|||
return preg_split("/\r\n|\n|\r/", $fc_utf8);
|
|||
}
|
|||
}
|