* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ /** * \file bankimport/class/bankstatement.class.php * \ingroup bankimport * \brief Class for PDF bank statements from FinTS */ require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; /** * Class BankImportStatement * Represents a PDF bank statement imported via FinTS */ class BankImportStatement extends CommonObject { /** * @var string ID to identify managed object */ public $element = 'bankstatement'; /** * @var string Name of table without prefix where object is stored */ public $table_element = 'bankimport_statement'; /** * @var int Entity */ public $entity; /** * @var string IBAN */ public $iban; /** * @var string Statement number */ public $statement_number; /** * @var int Statement year */ public $statement_year; /** * @var int Statement date */ public $statement_date; /** * @var int Period from */ public $date_from; /** * @var int Period to */ public $date_to; /** * @var float Opening balance */ public $opening_balance; /** * @var float Closing balance */ public $closing_balance; /** * @var string Currency */ public $currency = 'EUR'; /** * @var string Filename */ public $filename; /** * @var string Filepath */ public $filepath; /** * @var int Filesize */ public $filesize; /** * @var string Import batch key */ public $import_key; /** * @var int Creation timestamp */ public $datec; /** * @var int User who created */ public $fk_user_creat; /** * @var string Private note */ public $note_private; /** * @var string Public note */ public $note_public; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { global $conf; $this->db = $db; $this->entity = $conf->entity; } /** * Create statement in database * * @param User $user User that creates * @return int <0 if KO, Id of created object if OK */ public function create($user) { global $conf; $now = dol_now(); $this->db->begin(); $sql = "INSERT INTO ".MAIN_DB_PREFIX."bankimport_statement ("; $sql .= "entity, iban, statement_number, statement_year, statement_date,"; $sql .= "date_from, date_to, opening_balance, closing_balance, currency,"; $sql .= "filename, filepath, filesize, import_key, datec, fk_user_creat"; $sql .= ") VALUES ("; $sql .= ((int) $this->entity).","; $sql .= ($this->iban ? "'".$this->db->escape($this->iban)."'" : "NULL").","; $sql .= "'".$this->db->escape($this->statement_number)."',"; $sql .= ((int) $this->statement_year).","; $sql .= ($this->statement_date ? "'".$this->db->idate($this->statement_date)."'" : "NULL").","; $sql .= ($this->date_from ? "'".$this->db->idate($this->date_from)."'" : "NULL").","; $sql .= ($this->date_to ? "'".$this->db->idate($this->date_to)."'" : "NULL").","; $sql .= ($this->opening_balance !== null ? ((float) $this->opening_balance) : "NULL").","; $sql .= ($this->closing_balance !== null ? ((float) $this->closing_balance) : "NULL").","; $sql .= "'".$this->db->escape($this->currency)."',"; $sql .= ($this->filename ? "'".$this->db->escape($this->filename)."'" : "NULL").","; $sql .= ($this->filepath ? "'".$this->db->escape($this->filepath)."'" : "NULL").","; $sql .= ($this->filesize ? ((int) $this->filesize) : "NULL").","; $sql .= ($this->import_key ? "'".$this->db->escape($this->import_key)."'" : "NULL").","; $sql .= "'".$this->db->idate($now)."',"; $sql .= ((int) $user->id); $sql .= ")"; dol_syslog(get_class($this)."::create", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX."bankimport_statement"); $this->datec = $now; $this->fk_user_creat = $user->id; $this->db->commit(); return $this->id; } else { $this->error = $this->db->lasterror(); $this->db->rollback(); return -1; } } /** * Load statement from database * * @param int $id Id of statement to load * @return int <0 if KO, 0 if not found, >0 if OK */ public function fetch($id) { $sql = "SELECT t.*"; $sql .= " FROM ".MAIN_DB_PREFIX."bankimport_statement as t"; $sql .= " WHERE t.rowid = ".((int) $id); dol_syslog(get_class($this)."::fetch", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { if ($this->db->num_rows($resql)) { $obj = $this->db->fetch_object($resql); $this->id = $obj->rowid; $this->entity = $obj->entity; $this->iban = $obj->iban; $this->statement_number = $obj->statement_number; $this->statement_year = $obj->statement_year; $this->statement_date = $this->db->jdate($obj->statement_date); $this->date_from = $this->db->jdate($obj->date_from); $this->date_to = $this->db->jdate($obj->date_to); $this->opening_balance = $obj->opening_balance; $this->closing_balance = $obj->closing_balance; $this->currency = $obj->currency; $this->filename = $obj->filename; $this->filepath = $obj->filepath; $this->filesize = $obj->filesize; $this->import_key = $obj->import_key; $this->datec = $this->db->jdate($obj->datec); $this->fk_user_creat = $obj->fk_user_creat; $this->note_private = $obj->note_private; $this->note_public = $obj->note_public; $this->db->free($resql); return 1; } else { $this->db->free($resql); return 0; } } else { $this->error = $this->db->lasterror(); return -1; } } /** * Check if statement already exists (by number, year, iban) * * @return int 0 if not exists, rowid if exists */ public function exists() { $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."bankimport_statement"; $sql .= " WHERE statement_number = '".$this->db->escape($this->statement_number)."'"; $sql .= " AND statement_year = ".((int) $this->statement_year); $sql .= " AND iban = '".$this->db->escape($this->iban)."'"; $sql .= " AND entity = ".((int) $this->entity); $resql = $this->db->query($sql); if ($resql) { if ($this->db->num_rows($resql) > 0) { $obj = $this->db->fetch_object($resql); return $obj->rowid; } } return 0; } /** * Fetch all statements with filters * * @param string $sortfield Sort field * @param string $sortorder Sort order (ASC/DESC) * @param int $limit Limit * @param int $offset Offset * @param array $filter Filters array * @param string $mode 'list' returns array, 'count' returns count * @return array|int Array of statements, count or -1 on error */ public function fetchAll($sortfield = 'statement_year,statement_number', $sortorder = 'DESC', $limit = 0, $offset = 0, $filter = array(), $mode = 'list') { $sql = "SELECT t.rowid"; $sql .= " FROM ".MAIN_DB_PREFIX."bankimport_statement as t"; $sql .= " WHERE t.entity = ".((int) $this->entity); // Apply filters if (!empty($filter['iban'])) { $sql .= " AND t.iban LIKE '%".$this->db->escape($filter['iban'])."%'"; } if (!empty($filter['year'])) { $sql .= " AND t.statement_year = ".((int) $filter['year']); } // Count mode if ($mode == 'count') { $sqlcount = preg_replace('/SELECT t\.rowid/', 'SELECT COUNT(*) as total', $sql); $resqlcount = $this->db->query($sqlcount); if ($resqlcount) { $objcount = $this->db->fetch_object($resqlcount); return (int) $objcount->total; } return 0; } // Sort and limit $sql .= $this->db->order($sortfield, $sortorder); if ($limit > 0) { $sql .= $this->db->plimit($limit, $offset); } dol_syslog(get_class($this)."::fetchAll", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $result = array(); while ($obj = $this->db->fetch_object($resql)) { $statement = new BankImportStatement($this->db); $statement->fetch($obj->rowid); $result[] = $statement; } $this->db->free($resql); return $result; } else { $this->error = $this->db->lasterror(); return -1; } } /** * Delete statement * * @param User $user User that deletes * @return int <0 if KO, >0 if OK */ public function delete($user) { $this->db->begin(); // Delete file if exists if ($this->filepath && file_exists($this->filepath)) { @unlink($this->filepath); } $sql = "DELETE FROM ".MAIN_DB_PREFIX."bankimport_statement"; $sql .= " WHERE rowid = ".((int) $this->id); dol_syslog(get_class($this)."::delete", LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $this->db->commit(); return 1; } else { $this->error = $this->db->lasterror(); $this->db->rollback(); return -1; } } /** * Get full path to PDF file * * @return string Full path or empty string */ public function getFilePath() { if ($this->filepath && file_exists($this->filepath)) { return $this->filepath; } return ''; } /** * Get storage directory for statements * * @return string Directory path */ public static function getStorageDir() { global $conf; $dir = $conf->bankimport->dir_output.'/statements'; if (!is_dir($dir)) { dol_mkdir($dir); } return $dir; } /** * Save PDF content to file * * @param string $pdfContent Binary PDF content * @return int <0 if KO, >0 if OK */ public function savePDF($pdfContent) { $dir = self::getStorageDir(); // Generate filename $this->filename = sprintf('statement_%s_%d_%s.pdf', preg_replace('/[^A-Z0-9]/', '', $this->iban), $this->statement_year, $this->statement_number ); $this->filepath = $dir.'/'.$this->filename; // Write file $result = file_put_contents($this->filepath, $pdfContent); if ($result !== false) { $this->filesize = strlen($pdfContent); return 1; } $this->error = 'Failed to write PDF file'; return -1; } /** * Save uploaded PDF file * * @param array $fileInfo Element from $_FILES array * @return int <0 if KO, >0 if OK */ public function saveUploadedPDF($fileInfo) { // Validate upload if (empty($fileInfo['tmp_name']) || !is_uploaded_file($fileInfo['tmp_name'])) { $this->error = 'No file uploaded'; return -1; } // Check file size (max 10MB) if ($fileInfo['size'] > 10 * 1024 * 1024) { $this->error = 'File too large (max 10MB)'; return -1; } // Check MIME type $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $fileInfo['tmp_name']); finfo_close($finfo); if ($mimeType !== 'application/pdf') { $this->error = 'Only PDF files are allowed'; return -1; } $dir = self::getStorageDir(); // Generate filename $ibanPart = !empty($this->iban) ? preg_replace('/[^A-Z0-9]/', '', strtoupper($this->iban)) : 'KONTO'; $this->filename = sprintf('Kontoauszug_%s_%d_%s.pdf', $ibanPart, $this->statement_year, str_pad($this->statement_number, 3, '0', STR_PAD_LEFT) ); $this->filepath = $dir.'/'.$this->filename; // Check if file already exists if (file_exists($this->filepath)) { // Add timestamp to make unique $this->filename = sprintf('Kontoauszug_%s_%d_%s_%s.pdf', $ibanPart, $this->statement_year, str_pad($this->statement_number, 3, '0', STR_PAD_LEFT), date('His') ); $this->filepath = $dir.'/'.$this->filename; } // Move uploaded file if (!move_uploaded_file($fileInfo['tmp_name'], $this->filepath)) { $this->error = 'Failed to save file'; return -1; } $this->filesize = filesize($this->filepath); return 1; } /** * Get next available statement number for a year * * @param int $year Year * @return string Next statement number */ public function getNextStatementNumber($year) { $sql = "SELECT MAX(CAST(statement_number AS UNSIGNED)) as maxnum"; $sql .= " FROM ".MAIN_DB_PREFIX."bankimport_statement"; $sql .= " WHERE statement_year = ".((int) $year); $sql .= " AND entity = ".((int) $this->entity); $resql = $this->db->query($sql); if ($resql) { $obj = $this->db->fetch_object($resql); $nextNum = ($obj->maxnum !== null) ? ((int) $obj->maxnum + 1) : 1; return (string) $nextNum; } return '1'; } }