dolibarr.bankimport/class/bankstatement.class.php
2026-02-13 18:30:28 +01:00

504 lines
12 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* 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';
}
}