You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1500 lines
61 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
use Tiki\Lib\Logs\LogsLib;
/**
* Basic functions used by the accounting feature
*
* <p>This file contains all functions used by more than one file from the ccsg_accounting feature.
* This feature is a simple accounting/bookkeeping function.</p>
*
* @package accounting
* @author Joern Ott <white@ott-service.de>
* @version 1.2
* @date 2010-11-16
* @copyright LGPL
*/
class AccountingLib extends LogsLib
{
/**
*
* Storing the book data if already requested once, this may save us a few queries
* @var array $_book array with the books structure
*/
private $_book = '';
/**
* Lists all books available to a user
* @param string $order sorting order
* @return array list of books (complete table structure)
*/
public function listBooks($order = 'bookId ASC')
{
$query = "SELECT * FROM tiki_acct_book ORDER BY $order";
return $this->fetchAll($query, []);
}
/**
*
* Creates a new book and gives full permissions to the creator
* @param string $bookName descriptive name of the book
* @param string $bookStartDate first permitted date for the book
* @param string $bookEndDate last permitted date for the book
* @param string $bookCurrency up to 3 letter cuurency code
* @param int $bookCurrencyPos where should the currency symbol appear -1=before, 1=after
* @param int $bookDecimals number of decimal points
* @param string $bookDecPoint separator for the decimal point
* @param string $bookThousand separator for the thousands
* @param string $exportSeparator separator between fields when exporting CSV
* @param string $exportEOL end of line definition, either CR, LF or CRLF
* @param string $exportQuote Quote character to enclose strings in CSV
* @param string $bookClosed 'y' if the book is closed (no more changes), 'n' otherwise
* @param string $bookAutoTax
* @return int/string bookId on success, error message otherwise
*/
public function createBook($bookName, $bookClosed, $bookStartDate, $bookEndDate, $bookCurrency, $bookCurrencyPos, $bookDecimals, $bookDecPoint, $bookThousand, $exportSeparator, $exportEOL, $exportQuote, $bookAutoTax = 'y')
{
global $user;
$userlib = TikiLib::lib('user');
if (strlen($bookName) == 0) {
return "The book must have a name";
}
if (strtotime($bookStartDate) === false) {
return "Invalid start date";
}
if (strtotime($bookEndDate) === false) {
return "Invalid end date";
}
$query = "INSERT INTO `tiki_acct_book`" .
" (`bookName`, `bookClosed`, `bookStartDate`, `bookEndDate`," .
" `bookCurrency`, `bookCurrencyPos`, `bookDecimals`, `bookDecPoint`, `bookThousand`," .
" `exportSeparator`, `exportEOL`, `exportQuote`, `bookAutoTax`)" .
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
$res = $this->query(
$query,
[
$bookName,
$bookClosed,
$bookStartDate,
$bookEndDate,
$bookCurrency,
$bookCurrencyPos,
$bookDecimals,
$bookDecPoint,
$bookThousand,
$exportSeparator,
$exportEOL,
$exportQuote,
$bookAutoTax
]
);
$bookId = $this->lastInsertId();
$this->createTax($bookId, tra('No automated tax'), 0, 'n');
$groupId = $bookId;
do {
//make sure we don't have that group already
$groupname = "accounting_book_$groupId";
$groupexists = $userlib->group_exists($groupname);
if ($groupexists) {
$groupId++;
}
} while ($groupexists);
if ($groupId != $bookId) {
$query = "UPDATE `tiki_acct_book` SET `bookId`=? WHERE `bookId`=?";
$res = $this->query($query, [$groupId, $bookId]);
$bookId = $groupId;
}
$userlib->add_group($groupname);
$userlib->assign_user_to_group($user, $groupname);
$userlib->assign_object_permission($groupname, $bookId, 'accounting book', 'tiki_p_acct_view');
$userlib->assign_object_permission($groupname, $bookId, 'accounting book', 'tiki_p_acct_book');
$userlib->assign_object_permission($groupname, $bookId, 'accounting book', 'tiki_p_acct_manage_accounts');
$userlib->assign_object_permission($groupname, $bookId, 'accounting book', 'tiki_p_acct_book_stack');
$userlib->assign_object_permission($groupname, $bookId, 'accounting book', 'tiki_p_acct_book_import');
$userlib->assign_object_permission($groupname, $bookId, 'accounting book', 'tiki_p_acct_manage_template');
return $bookId;
}
/**
*
* Returns the details for a book with a given bookId
* @param int $bookId Id of the book to retrieve the data for
* @return array Array with book details
*/
public function getBook($bookId)
{
if (! is_array($this->_book) or $this->_book['bookId'] != $bookId) {
$query = "SELECT * FROM `tiki_acct_book` WHERE `bookId`=?";
$res = $this->query($query, [$bookId]);
$this->_book = $res->fetchRow();
}
return $this->_book;
}
/**
*
* This function sets a books status to closed, so transactions can no longer be used
* @param int $bookId id of the book to close
* @return bool true on success
*/
public function closeBook($bookId)
{
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
return false;
}
$query = "UPDATE `tiki_acct_book` SET `bookClosed`='y' WHERE `bookId`=?";
$res = $this->query($query, [$bookId]);
if ($res === false) {
return false;
}
return true;
}
/**
* Returns the complete journal for a given account, if none is provided, the whole journal will be fetched
*
* @param int $bookId id of the current book
* @param int $accountId account for which we should display the journal, defaults to '%' (all accounts)
* @param string $order sorting order
* @param int $limit max number of records to fetch, defaults to 0 = all
* @return array|bool journal with all posts, false on errors
*/
public function getJournal($bookId, $accountId = '%', $order = '`journalId` ASC', $limit = 0)
{
$journal = [];
if ($limit != 0) {
if ($limit < 0) {
$order = str_replace("ASC", "DESC", $order);
}
$order .= " LIMIT " . abs($limit);
}
if ($accountId == '%') {
$query = "SELECT `journalId`, `journalDate`, `journalDescription`, `journalCancelled`" .
" FROM `tiki_acct_journal`" .
" WHERE `journalBookId`=?" .
" ORDER BY $order";
$res = $this->query($query, [$bookId]);
} else {
$query = "SELECT `journalId`, `journalDate`, `journalDescription`, `journalCancelled`" .
" FROM `tiki_acct_journal` INNER JOIN `tiki_acct_item`" .
" ON (`tiki_acct_journal`.`journalBookId`=`tiki_acct_item`.`itemBookId` AND" .
" `tiki_acct_journal`.`journalId`=`tiki_acct_item`.`itemJournalId`)" .
" WHERE `journalBookId`=? AND `itemAccountId` LIKE ?" .
" GROUP BY `journalId`, `journalDate`, `journalDescription`, `journalCancelled`" .
" ORDER BY $order";
$res = $this->query($query, [$bookId, $accountId]);
}
if ($res === false) {
return false;
}
while ($row = $res->fetchRow()) {
$query = "SELECT * FROM `tiki_acct_item` WHERE `itemBookId`=? AND `itemJournalId`=? AND `itemType`=? ORDER BY `itemAccountId` ASC";
$row['debit'] = $this->fetchAll($query, [$bookId, $row['journalId'], -1]);
$row['debitcount'] = count($row['debit']);
$row['credit'] = $this->fetchAll($query, [$bookId, $row['journalId'], 1]);
$row['creditcount'] = count($row['credit']);
$row['maxcount'] = max($row['creditcount'], $row['debitcount']);
$journal[] = $row;
}
return $journal;
}
/**
* Returns the totals for a given book and account
*
* @param int $bookId id of the current book
* @param int $accountId account for which we should fetch the totals, defaults to '%' (all accounts)
* @return array array with three elements debit, credit and the total (credit-debit)
*/
public function getJournalTotals($bookId, $accountId = '%')
{
$journal = [];
$query = "SELECT `itemAccountId`, SUM(`itemAmount`*IF(`itemType`<0,1,0)) AS debit," .
" sum(`itemAmount`*IF(`itemType`>0,1,0)) AS credit" .
" FROM `tiki_acct_journal` INNER JOIN `tiki_acct_item`" .
" ON (`tiki_acct_journal`.`journalBookId`=`tiki_acct_item`.`itemBookId`" .
" AND `tiki_acct_journal`.`journalId`=`tiki_acct_item`.`itemJournalId`)" .
" WHERE `journalBookId`=? AND `itemAccountId` LIKE ?" .
" GROUP BY `itemAccountId`";
$res = $this->query($query, [$bookId, $accountId]);
$totals = $res->fetchRow();
$totals['total'] = $totals['credit'] - $totals['debit'];
return $totals;
}
/**
* Returns a list of accounts as defined in table tiki_acct_account
*
* @param int $bookId id of the book to retrieve the accounts for
* @param string $order order of items, defaults to accountId
* @param boolean $all true = fetch all accounts, false = fetch only unlocked accounts
* @return array list of accounts
*/
public function getAccounts($bookId, $order = "`accountId` ASC", $all = false)
{
$query = 'SELECT * FROM `tiki_acct_account` WHERE `accountBookId`=? ' .
($all ? '' : 'AND `accountLocked`=0 ') .
" ORDER BY $order";
return $this->fetchAll($query, [$bookId]);
}
/**
* Returns an extended list of accounts with totals
*
* @param int $bookId id of the book to fetch the account list for
* @param bool $all true = fetch all accounts or false = only unlocked accounts, defaults to false
* @return array list of accounts
*/
public function getExtendedAccounts($bookId, $all = false)
{
$allcond = $all ? '' : ' AND accountLocked=0';
$query = "SELECT accountBookId, accountId, accountName, accountNotes, accountBudget, accountLocked, " .
" SUM(itemAmount*IF(itemType<0,1,0)) AS debit, SUM(itemAmount*IF(itemType>0,1,0)) AS credit" .
" FROM tiki_acct_account" .
" LEFT JOIN tiki_acct_journal ON tiki_acct_account.accountBookId=tiki_acct_journal.journalBookId" .
" LEFT JOIN tiki_acct_item ON tiki_acct_journal.journalId=tiki_acct_item.itemJournalId" .
" AND tiki_acct_account.accountId=tiki_acct_item.itemAccountId" .
" WHERE tiki_acct_account.accountBookId=? $allcond" .
" GROUP BY accountId, accountName, accountNotes, accountBudget, accountLocked, accountBookId";
return $this->fetchAll($query, [$bookId]);
}
/**
* Returns an array with all data from the account
*
* @param int $bookId id of the current book
* @param int $accountId account id to retrieve
* @param boolean $checkChangeable perform check, if the account is changeable
* @return array account data or false on error
*/
public function getAccount($bookId, $accountId, $checkChangeable = true)
{
$query = "SELECT * FROM `tiki_acct_account` WHERE `accountbookId`=? AND `accountId`=?";
$res = $this->query($query, [$bookId, $accountId]);
$account = $res->fetchRow();
if ($checkChangeable) {
$account['changeable'] = $this->accountChangeable($bookId, $accountId);
}
return $account;
}
/**
* Checks if this accountId can be changed or the account can be deleted.
* This can only be done, if the account has not been used -> no posts exist for the account
*
* @param int $bookId id of the current book
* @param int $accountId account id to check
* @return boolean true, if the account can be changed/deleted
*/
public function accountChangeable($bookId, $accountId)
{
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
return false;
}
$query = "SELECT Count(`itemAccountId`) AS posts" .
" FROM `tiki_acct_journal`" .
" INNER JOIN `tiki_acct_item` ON `tiki_acct_journal`.`journalId`=`tiki_acct_item`.`itemJournalId`" .
" WHERE `journalBookId`=? and `itemAccountId`=?";
$res = $this->query($query, [$bookId, $accountId]);
$posts = $res->fetchRow();
return ($posts['posts'] == 0);
}
/**
* Creates an account with the given information
*
* @param int $bookId id of the current book
* @param int $accountId id of the account to create
* @param string $accountName name of the account to create
* @param string $accountNotes notes for this account
* @param float $accountBudget planned budget for the account
* @param boolean $accountLocked can this account be used, 0=unlocked, 1=locked
* @param int $accountTax taxId for tax automation
* @return array|bool list of errors or true on success
*/
public function createAccount(
$bookId,
$accountId,
$accountName,
$accountNotes,
$accountBudget,
$accountLocked,
$accountTax = 0
) {
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
$errors = [tra("This book has been closed. You can't create new accounts.")];
return $errors;
}
$errors = $this->validateId('accountId', $accountId, 'tiki_acct_account', false, 'accountBookId', $bookId);
if ($accountName == '') {
$errors[] = tra('Account name must not be empty.');
}
$cleanbudget = $this->cleanupAmount($bookId, $accountBudget);
if ($cleanbudget === '') {
$errors[] = tra('Budget is not a valid amount: ') . $accountBudget;
}
if ($accountLocked != 0 and $accountLocked != 1) {
$errors[] = tra('Locked must be either 0 or 1.');
}
if ($accountTax != 0) {
$errors = array_merge($errors, $this->validateId('taxId', $accountTax, 'tiki_acct_tax', true, 'taxBookId', $bookId));
}
if (! empty($errors)) {
return $errors;
}
$query = 'INSERT INTO tiki_acct_account' .
' SET accountBookId=?, accountId=?, accountName=?,' .
' accountNotes=?, accountBudget=?, accountLocked=?, accountTax=?';
$res = $this->query(
$query,
[
$bookId,
$accountId,
$accountName,
$accountNotes,
$cleanbudget,
$accountLocked,
$accountTax
]
);
if ($res === false) {
$errors[] = tra('Error creating account') & " $accountId: " . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
return $errors;
}
return true;
}
/**
* Unlocks or locks an account which means it can not be used accidentally for booking
*
* @param int $bookId current book
* @param int $accountId account to lock
* @return bool true on success
*/
public function changeAccountLock($bookId, $accountId)
{
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
return false;
}
$query = "UPDATE `tiki_acct_account` SET `accountLocked` = NOT `accountLocked`
WHERE `accountBookId`=? AND `accountId`=?";
$res = $this->query($query, [$bookId, $accountId]);
if ($res === false) {
return false;
}
return true;
}
/**
* Updates an account with the given information
*
* @param int $bookId id of the current book
* @param int $accountId original id of the account
* @param int $newAccountId new id of the account (only if the account is changeable)
* @param string $accountName name of the account
* @param string $accountNotes notes for the account
* @param float $accountBudget planned yearly budget for the account
* @param boolean $accountLocked can this account be used 0=unlocked, 1=locked
* @param int $accountTax id of the auto tax type, defaults to 0
* @return array|bool list of errors, true on success
*/
public function updateAccount(
$bookId,
$accountId,
$newAccountId,
$accountName,
$accountNotes,
$accountBudget,
$accountLocked,
$accountTax = 0
) {
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
$errors = [tra("This book has been closed. You can't modify the account.")];
return $errors;
}
$errors = $this->validateId('accountId', $newAccountId, 'tiki_acct_account', true, 'accountBookId', $bookId);
if ($accountId != $newAccountId) {
if (! $this->accountChangeable($bookId, $accountId)) {
$errors[] = tra('AccountId %0 is already in use and must not be changed. Please disable it if it is no longer needed.', $args = [$accountId]);
}
}
if ($accountName === '') {
$errors[] = tra('Account name must not be empty.');
}
$cleanbudget = $this->cleanupAmount($bookId, $accountBudget);
if ($cleanbudget === '') {
$errors[] = tra('Budget is not a valid amount: ') . $cleanbudget;
}
if ($accountLocked != 0 and $accountLocked != 1) {
$errors[] = tra('Locked must be either 0 or 1.');
}
if ($accountTax != 0) {
$errors = array_merge($errors, $this->validateId('taxId', $accountTax, 'tiki_acct_tax', true, 'taxBookId', $bookId));
}
if (count($errors) != 0) {
return $errors;
}
$query = "UPDATE tiki_acct_account SET accountId=?, accountName=?,
accountNotes=?, accountBudget=?, accountLocked=?, accountTax=?
WHERE accountBookId=? AND accountId=?";
$res = $this->query(
$query,
[
$newAccountId,
$accountName,
$accountNotes,
$cleanbudget,
$accountLocked,
$accountTax,
$bookId,
$accountId
]
);
if ($res === false) {
$errors[] = tra('Error updating account') & " $accountId: " . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
return $errors;
}
return true;
}
/**
* Delete an account (if deleteable)
*
* @param int $bookId id of the current book
* @param int $accountId account id to delete
* @param bool $checkChangeable check, if the account is unused and can be deleted
* @return array|bool array with errors or true, if deletion was successful
*/
public function deleteAccount($bookId, $accountId, $checkChangeable = true)
{
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
return [tra("This book has been closed. You can't delete the account.")];
}
if (! $this->accountChangeable($bookId, $accountId)) {
return [tra('Account is already in use and must not be deleted. Please disable it, if it is no longer needed.')];
}
$query = "DELETE FROM `tiki_acct_account` WHERE `accountBookId`=? AND `accountId`=?";
$res = $this->query($query, [$bookId, $accountId]);
return true;
}
/**
*
* Do a manual rollback, if the creation of a complete booking fails.
* This is a workaround for missing transaction support
* @param int $bookId id of the current book
* @param int $journalId id of the entry to roll back
* @return string Text messages stating the success/failure of the rollback
*/
public function manualRollback($bookId, $journalId)
{
$errors = [];
$query = "DELETE FROM `tiki_acct_item` WHERE `itemBookId`=? AND `itemJournalId`=?";
$res = $this->query($query, [$bookId, $journalId]);
$rollback = ($res !== false);
$query = "DELETE FROM `tiki_acct_journal` WHERE `journalBookId`=? AND `journalId`=?";
$res = $this->query($query, [$bookId, $journalId]);
$rollback = $rollback and ($res !== false);
if (! $rollback) {
return tra('Rollback failed, inconsistent database: Cleanup needed for journalId %0 in book %1', [$journalId, $bookId]);
} else {
return tra('successfully rolled back #') . " $journalId";
}
}
/**
* Checks if the book date is within the books limits
*
* @param array $book book array
* @param DateTime $Date
* @retun array|bool
*/
public function checkBookDates($book, $Date)
{
$StartDate = new DateTime($book['bookStartDate']);
if ($Date < $StartDate) {
return [tra("The date of the transaction is before the start date of this book.")];
}
$EndDate = new DateTime($book['bookEndDate']);
if ($Date > $EndDate) {
return [tra("The date of the transaction is after the end date of this book.")];
}
return true;
}
/**
* books a simple transaction
*
* @param int $bookId id of the current book
* @param string $journalDate date of the transaction
* @param string $journalDescription description of this transaction
* @param int $debitAccount account to debit
* @param int $creditAccount account to credit
* @param double $amount amount to transfer between the accounts
* @param string $debitText text for the debit post, defaults to an empty string
* @param string $creditText text for the credit post, defaults to an empty string
* @return int|array list of errors or journalId on success
*/
public function simpleBook(
$bookId,
$journalDate,
$journalDescription,
$debitAccount,
$creditAccount,
$amount,
$debitText = '',
$creditText = ''
) {
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
return [tra("This book has been closed. Bookings can no longer be made in it.")];
} try {
$date = new DateTime($journalDate);
} catch (Exception $e) {
return [tra("Invalid booking date.")];
}
$errors = $this->checkBookDates($book, $date);
if (is_array($errors)) {
return $errors;
}
$errors = [];
$query = "INSERT INTO `tiki_acct_journal` (`journalBookId`, `journalDate`, `journalDescription`,
`journalCancelled`, `journalTs`)
VALUES (?,?,?,0,NOW())";
$res = $this->query($query, [$bookId, $date->toString('Y-M-d'), $journalDescription]);
if ($res === false) {
$errors[] = tra('Booking error creating journal entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$this->rollback();
return $errors;
}
$journalId = $this->lastInsertId();
$query = "INSERT INTO `tiki_acct_item` (`itemBookId`, `itemJournalId`, `itemAccountId`, `itemType`,
`itemAmount`, `itemText`, `itemTs`)
VALUES (?, ?, ?, ?, ?, ?, NOW())";
$res = $this->query($query, [$bookId, $journalId, $debitAccount, -1, $amount, $debitText]);
if ($res === false) {
$errors[] = tra('Booking error creating debit entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->manualRollback($bookId, $journalId);
return $errors;
}
$res = $this->query($query, [$bookId, $journalId, $creditAccount, 1, $amount, $creditText]);
if ($res === false) {
$errors[] = tra('Booking error creating credit entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->manualRollback($bookId, $journalId);
return $errors;
}
// everything ok
return $journalId;
}
/**
* books a complex transaction with multiple accounts on one side
*
* @param int $bookId id of the current book
* @param DateTime $journalDate date of the transaction
* @param string $journalDescription description of this transaction
* @param mixed $debitAccount account(s) to debit
* @param mixed $creditAccount account(s) to credit
* @param mixed $debitAmount amount(s) on debit side
* @param mixed $creditAmount amount(s) on credit side
* @param mixed $debitText text(s) for the debit post, defaults to an empty string
* @param mixed $creditText text(s) for the credit post, defaults to an empty string
*
* @return int|array journalID or list of errors
*/
public function book(
$bookId,
$journalDate,
$journalDescription,
$debitAccount,
$creditAccount,
$debitAmount,
$creditAmount,
$debitText = [],
$creditText = []
) {
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
$errors[] = tra("This book has been closed. Bookings can no longer be made in it.");
}
if (! ($journalDate instanceof DateTime)) {
return [tra("Invalid booking date.")];
}
$errors = $this->checkBookDates($book, $journalDate);
if (is_array($errors)) {
return $errors;
}
$errors = [];
if (! is_array($debitAccount)) {
$debitAccount = [$debitAccount];
}
if (! is_array($creditAccount)) {
$creditAccount = [$creditAccount];
}
if (! is_array($debitAmount)) {
$debitAmount = [$debitAmount];
}
if (! is_array($creditAmount)) {
$creditAmount = [$creditAmount];
}
if (! is_array($debitText)) {
$debitText = [$debitText];
}
if (! is_array($creditText)) {
$creditText = [$creditText];
}
if (count($debitAccount) != count($debitAmount) or count($debitAccount) != count($debitText)) {
$errors[] = tra('The number of debit entries differs: ') . count($debitAccount) . '/' . count($debitAmount) . '/' . count($debitText);
}
if (count($creditAccount) != count($creditAmount) or count($creditAccount) != count($creditText)) {
$errors[] = tra('The number of credit entries differs: ') . count($creditAccount) . '/' . count($creditAmount) . '/' . count($creditText);
}
if (count($debitAccount) > 1 and count($creditAccount) > 1) {
$errors[] = tra('Splitting is only allowed on one side.');
}
$checkamount = 0;
for ($i = 0, $icount_debitAmount = count($debitAmount); $i < $icount_debitAmount; $i++) {
$a = $this->cleanupAmount($bookId, $debitAmount[$i]);
if (! is_numeric($a) or $a <= 0) {
$errors[] = tra('Invalid debit amount ') . $debitAmount[$i];
} else {
$checkamount -= $a;
}
if (! is_numeric($debitAccount[$i])) {
$errors[] = tra('Invalid debit account number ') . $debitAccount[$i];
}
}
for ($i = 0, $icount_creditAmount = count($creditAmount); $i < $icount_creditAmount; $i++) {
$a = $this->cleanupAmount($bookId, $creditAmount[$i]);
if (! is_numeric($a) or $a <= 0) {
$errors[] = tra('Invalid credit amount ') . $creditAmount[$i];
} else {
$checkamount += $a;
}
if (! is_numeric($creditAccount[$i])) {
$errors[] = tra('Invalid credit account number ') . $creditAccount[$i];
}
}
if ($checkamount != 0) {
$errors[] = tra('Difference between debit and credit amounts ') . $checkamount;
}
if (count($errors) > 0) {
return $errors;
}
$query = "INSERT INTO `tiki_acct_journal` (`journalBookId`, `journalDate`, `journalDescription`,
`journalCancelled`, `journalTs`)
VALUES (?,?,?,0,NOW())";
$res = $this->query($query, [$bookId, date_format($journalDate, 'Y-m-d'), $journalDescription]);
if ($res === false) {
$errors[] = tra('Booking error creating journal entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
return $errors;
}
$journalId = $this->lastInsertId();
$query = "INSERT INTO `tiki_acct_item` (`itemBookId`, `itemJournalId`, `itemAccountId`, `itemType`,
`itemAmount`, `itemText`, `itemTs`)
VALUES (?, ?, ?, ?, ?, ?, NOW())";
for ($i = 0, $icount_debitAccount = count($debitAccount); $i < $icount_debitAccount; $i++) {
$a = $this->cleanupAmount($bookId, $debitAmount[$i]);
$res = $this->query($query, [$bookId, $journalId, $debitAccount[$i], -1, $a, $debitText[$i]]);
if ($res === false) {
$errors[] = tra('Booking error creating debit entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->manualRollback($bookId, $journalId);
return $errors;
}
}
for ($i = 0, $icount_creditAccount = count($creditAccount); $i < $icount_creditAccount; $i++) {
$a = $this->cleanupAmount($bookId, $creditAmount[$i]);
$res = $this->query($query, [$bookId, $journalId, $creditAccount[$i], 1, $a, $creditText[$i]]);
if ($res === false) {
$errors[] = tra('Booking error creating credit entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->manualRollback($bookId, $journalId);
return $errors;
}
}
return $journalId;
}
/**
*
* Retrieves one entry from the journal
*
* @param int $bookId id of the current book
* @param int $journalId id of the post in the journal
* @return array|bool array with post, false on error
*/
public function getTransaction($bookId, $journalId)
{
$query = 'SELECT `journalId`, `journalDate`, `journalDescription`, `journalCancelled`' .
' FROM `tiki_acct_journal`' .
' WHERE `journalBookId`=? AND `journalId`=?'
;
$res = $this->query($query, [$bookId, $journalId]);
if ($res === false) {
return false;
}
$entry = $res->fetchRow();
$query = "SELECT * FROM `tiki_acct_item` WHERE `itemBookId`=? AND `itemJournalId`=? AND `itemType`=? ORDER BY `itemAccountId` ASC";
$entry['debit'] = $this->fetchAll($query, [$bookId, $entry['journalId'], -1]);
$entry['debitcount'] = count($entry['debit']);
$entry['credit'] = $this->fetchAll($query, [$bookId, $entry['journalId'], 1]);
$entry['creditcount'] = count($entry['credit']);
$entry['maxcount'] = max($entry['creditcount'], $entry['debitcount']);
return $entry;
}
/**
* Declares a statement in the journal as cancelled.
* @param int $bookId id of the current book
* @param int $journalId journalId of the statement to cancel
*/
public function cancelTransaction($bookId, $journalId)
{
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
$errors[] = tra("This book has been closed. Transactions can no longer be cancelled in it.");
}
$query = "UPDATE `tiki_acct_journal` SET `journalCancelled`=1 WHERE `journalBookId`=? and `journalId`=?";
$res = $this->query($query, [$bookId, $journalId]);
return true;
}
/**
* Returns the complete stack
*
* @param int $bookId id of the current book
* @return array|bool stack with all posts, false on errors
*/
public function getStack($bookId)
{
$stack = [];
$query = "SELECT * FROM `tiki_acct_stack` WHERE `stackBookId`=?";
$res = $this->query($query, [$bookId]);
if ($res === false) {
return false;
}
while ($row = $res->fetchRow()) {
$query = "SELECT * FROM `tiki_acct_stackitem` WHERE `stackBookId`=? AND `stackItemStackId`=? AND `stackItemType`=? ORDER BY `stackItemAccountId` ASC";
$row['debit'] = $this->fetchAll($query, [$bookId, $row['stackId'], -1]);
$row['debitcount'] = count($row['debit']);
$row['credit'] = $this->fetchAll($query, [$bookId, $row['stackId'], 1]);
$row['creditcount'] = count($row['credit']);
$row['maxcount'] = max($row['creditcount'], $row['debitcount']);
$stack[] = $row;
}
return $stack;
}
/**
*
* Do a manual rollback, if the creation of a complete booking fails.
* This is a workaround for missing transaction support
* @param int $bookId id of the current book
* @param int $stackId id of the entry to roll back
* @return string Text messages stating the success/failure of the rollback
*/
public function stackManualRollback($bookId, $stackId)
{
$errors = [];
$query = "DELETE FROM `tiki_acct_stackitem` WHERE `stackitemBookId`=? AND `stackitemJournalId`=?";
$res = $this->query($query, [$bookId, $stackId]);
$rollback = ($res !== false);
$query = "DELETE FROM `tiki_acct_stack` WHERE `stackBookId`=? AND `stackId`=?";
$res = $this->query($query, [$bookId, $stackId]);
$rollback = $rollback and ($res !== false);
if (! $rollback) {
return tra('Rollback failed, inconsistent database: Cleanup needed for stackId %0 in book %1', [$stackId, $bookId]);
} else {
return tra('successfully rolled back #') . " $stackId";
}
}
/**
* books a complex transaction with multiple accounts on one side into the stack
*
* @param int $bookId id of the current book
* @param DateTime $stackDate date of the transaction
* @param string $stackDescription description of this transaction
* @param mixed $debitAccount account(s) to debit
* @param mixed $creditAccount account(s) to credit
* @param mixed $debitAmount amount(s) on debit side
* @param mixed $creditAmount amount(s) on credit side
* @param mixed $debitText text(s) for the debit post, defaults to an empty string
* @param mixed $creditText text(s) for the credit post, defaults to an empty string
*
* @return int|array stackID or list of errors
*/
public function stackBook(
$bookId,
$stackDate,
$stackDescription,
$debitAccount,
$creditAccount,
$debitAmount,
$creditAmount,
$debitText = [],
$creditText = []
) {
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
$errors[] = tra("This book has been closed. Bookings can no longer be made in it.");
}
$date = $stackDate;
$errors = $this->checkBookDates($book, $date);
if (is_array($errors)) {
return $errors;
}
$errors = [];
if (! is_array($debitAccount)) {
$debitAccount = [$debitAccount];
}
if (! is_array($creditAccount)) {
$creditAccount = [$creditAccount];
}
if (! is_array($debitAmount)) {
$debitAmount = [$debitAmount];
}
if (! is_array($creditAmount)) {
$creditAmount = [$creditAmount];
}
if (! is_array($debitText)) {
$debitText = [$debitText];
}
if (! is_array($creditText)) {
$creditText = [$creditText];
}
if (count($debitAccount) != count($debitAmount) or count($debitAccount) != count($debitText)) {
$errors[] = tra('The number of debit entries differs: ') . count($debitAccount) . '/' . count($debitAmount) . '/' . count($debitText);
}
if (count($creditAccount) != count($creditAmount) or count($creditAccount) != count($creditText)) {
$errors[] = tra('The number of credit entries differs: ') . count($creditAccount) . '/' . count($creditAmount) . '/' . count($creditText);
}
if (count($debitAccount) > 1 and count($creditAccount) > 1) {
$errors[] = tra('Splitting is only allowed on one side.');
}
$checkamount = 0;
for ($i = 0, $icount_debitAmount = count($debitAmount); $i < $icount_debitAmount; $i++) {
$a = $this->cleanupAmount($bookId, $debitAmount[$i]);
if (! is_numeric($a) or $a <= 0) {
$errors[] = tra('Invalid debit amount ') . $debitAmount[$i];
} else {
$checkamount -= $a;
}
if (! is_numeric($debitAccount[$i])) {
$errors[] = tra('Invalid debit account number ') . $debitAccount[$i];
}
}
for ($i = 0, $icount_creditAmount = count($creditAmount); $i < $icount_creditAmount; $i++) {
$a = $this->cleanupAmount($bookId, $creditAmount[$i]);
if (! is_numeric($a) or $a <= 0) {
$errors[] = tra('Invalid credit amount ') . $creditAmount[$i];
} else {
$checkamount += $a;
}
if (! is_numeric($creditAccount[$i])) {
$errors[] = tra('Invalid credit account number ') . $creditAccount[$i];
}
}
if ($checkamount != 0) {
$errors[] = tra('Difference between debit and credit amounts ') . $checkamount;
}
if (count($errors) > 0) {
return $errors;
}
$query = "INSERT INTO `tiki_acct_stack` (`stackBookId`, `stackDate`, `stackDescription`) VALUES (?,?,?)";
$res = $this->query($query, [$bookId, date('Y-m-d', $date->getTimestamp()), $stackDescription]);
if ($res === false) {
$errors[] = tra('Booking error creating stack entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
return $errors;
}
$stackId = $this->lastInsertId();
$query = "INSERT INTO `tiki_acct_stackitem` (`stackBookId`, `stackItemStackId`, `stackItemAccountId`, `stackItemType`,
`stackItemAmount`, `stackItemText`)
VALUES (?, ?, ?, ?, ?, ?)";
for ($i = 0, $icount_debitAccount = count($debitAccount); $i < $icount_debitAccount; $i++) {
$a = $this->cleanupAmount($bookId, $debitAmount[$i]);
$res = $this->query($query, [$bookId, $stackId, $debitAccount[$i], -1, $a, $debitText[$i]]);
if ($res === false) {
$errors[] = tra('Booking error creating stack debit entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->stackManualRollback($bookId, $stackId);
return $errors;
}
}
for ($i = 0, $icount_creditAccount = count($creditAccount); $i < $icount_creditAccount; $i++) {
$a = $this->cleanupAmount($bookId, $creditAmount[$i]);
$res = $this->query($query, [$bookId, $stackId, $creditAccount[$i], 1, $a, $creditText[$i]]);
if ($res === false) {
$errors[] = tra('Booking error creating stack credit entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->manualRollback($bookId, $stackId);
return $errors;
}
}
// everything ok
return $stackId;
}
public function stackUpdate(
$bookId,
$stackId,
$stackDate,
$stackDescription,
$debitAccount,
$creditAccount,
$debitAmount,
$creditAmount,
$debitText = [],
$creditText = []
) {
$book = $this->getBook($bookId);
if ($book['bookClosed'] == 'y') {
$errors[] = tra("This book has been closed. Bookings can no longer be made in it.");
}
$date = $stackDate;
$errors = $this->checkBookDates($book, $date);
if (is_array($errors)) {
return $errors;
}
$errors = [];
if (! is_array($debitAccount)) {
$debitAccount = [$debitAccount];
}
if (! is_array($creditAccount)) {
$creditAccount = [$creditAccount];
}
if (! is_array($debitAmount)) {
$debitAmount = [$debitAmount];
}
if (! is_array($creditAmount)) {
$creditAmount = [$creditAmount];
}
if (! is_array($debitText)) {
$debitText = [$debitText];
}
if (! is_array($creditText)) {
$creditText = [$creditText];
}
if (count($debitAccount) != count($debitAmount) or count($debitAccount) != count($debitText)) {
$errors[] = tra('The number of debit entries differs: ') . count($debitAccount) . '/' . count($debitAmount) . '/' . count($debitText);
}
if (count($creditAccount) != count($creditAmount) or count($creditAccount) != count($creditText)) {
$errors[] = tra('The number of credit entries differs: ') . count($creditAccount) . '/' . count($creditAmount) . '/' . count($creditText);
}
if (count($debitAccount) > 1 and count($creditAccount) > 1) {
$errors[] = tra('Splitting is only allowed on one side.');
}
$checkamount = 0;
for ($i = 0, $icount_debitAmount = count($debitAmount); $i < $icount_debitAmount; $i++) {
$a = $this->cleanupAmount($bookId, $debitAmount[$i]);
if (! is_numeric($a) or $a <= 0) {
$errors[] = tra('Invalid debit amount ') . $debitAmount[$i];
} else {
$checkamount -= $a;
}
if (! is_numeric($debitAccount[$i])) {
$errors[] = tra('Invalid debit account number ') . $debitAccount[$i];
}
}
for ($i = 0, $icount_creditAmount = count($creditAmount); $i < $icount_creditAmount; $i++) {
$a = $this->cleanupAmount($bookId, $creditAmount[$i]);
if (! is_numeric($a) or $a <= 0) {
$errors[] = tra('Invalid credit amount ') . $creditAmount[$i];
} else {
$checkamount += $a;
}
if (! is_numeric($creditAccount[$i])) {
$errors[] = tra('Invalid credit account number ') . $creditAccount[$i];
}
}
if ($checkamount != 0) {
$errors[] = tra('Difference between debit and credit amounts ') . $checkamount;
}
if (count($errors) > 0) {
return $errors;
}
$query = "UPDATE `tiki_acct_stack` SET `stackDate`=?, `stackDescription`=? WHERE `stackBookId`=? AND `stackId`=?";
$res = $this->query($query, [date('Y-m-d', $date->getTimestamp()), $stackDescription, $bookId, $stackId]);
if ($res === false) {
$errors[] = tra('Booking error creating stack entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
return $errors;
}
$query = "DELETE FROM `tiki_acct_stackitem` WHERE `stackBookId`=? AND `stackItemStackId`=?";
$res = $this->query($query, [$bookId, $stackId]);
if ($res === false) {
$errors[] = tra('Booking error creating stack entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->stackManualRollback($bookId, $stackId);
return $errors;
}
$query = "INSERT INTO `tiki_acct_stackitem` (`stackBookId`, `stackItemStackId`, `stackItemAccountId`, `stackItemType`,
`stackItemAmount`, `stackItemText`)
VALUES (?, ?, ?, ?, ?, ?)";
for ($i = 0, $icount_debitAccount = count($debitAccount); $i < $icount_debitAccount; $i++) {
$a = $this->cleanupAmount($bookId, $debitAmount[$i]);
$res = $this->query($query, [$bookId, $stackId, $debitAccount[$i], -1, $a, $debitText[$i]]);
if ($res === false) {
$errors[] = tra('Booking error creating stack debit entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->stackManualRollback($bookId, $stackId);
return $errors;
}
}
for ($i = 0, $icount_creditAccount = count($creditAccount); $i < $icount_creditAccount; $i++) {
$a = $this->cleanupAmount($bookId, $creditAmount[$i]);
$res = $this->query($query, [$bookId, $stackId, $creditAccount[$i], 1, $a, $creditText[$i]]);
if ($res === false) {
$errors[] = tra('Booking error creating stack credit entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->manualRollback($bookId, $stackId);
return $errors;
}
}
// everything ok
return $stackId;
}
/**
* deletes an entry from the stack
* @param int $bookId id of the current book
* @param int $stackId id of the entry to delete
* @return bool|array true on success, array of error messages otherwise
*/
public function stackDelete($bookId, $stackId)
{
$errors = [];
$query = "DELETE FROM `tiki_acct_stackitem` WHERE `stackBookId`=? AND `stackItemStackId`=?";
$res = $this->query($query, [$bookId, $stackId]);
if ($res === false) {
$errors[] = tra('Error deleting entry from stack') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
}
$query = "DELETE FROM `tiki_acct_stack` WHERE `stackBookId`=? AND `stackId`=?";
$res = $this->query($query, [$bookId, $stackId]);
if ($res === false) {
$errors[] = tra('Error deleting entry from stack') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
}
if (count($errors) != 0) {
return $errors;
}
return true;
}
/**
*
* Confirm a transaction and transfer it to the journal
* @param int $bookId id of the current book
* @param int $stackId id of the entry in the stack
*/
public function stackConfirm($bookId, $stackId)
{
$query = "INSERT into `tiki_acct_journal` (`journalBookId`, `journalDate`, `journalDescription`,
`journalCancelled`, `journalTs`)
SELECT ?, `stackDate`, `stackDescription` , 0, NOW() FROM `tiki_acct_stack` WHERE `stackBookId`=? AND `stackId`=?";
$res = $this->query($query, [$bookId, $bookId, $stackId]);
if ($res === false) {
$errors[] = tra('Booking error confirming stack entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
return $errors;
}
$journalId = $this->lastInsertId();
$query = "INSERT INTO `tiki_acct_item` (`itemBookId`, `itemJournalId`, `itemAccountId`, `itemType`,
`itemAmount`, `itemText`, `itemTs`)
SELECT ?, ?, `stackItemAccountId`, `stackItemType`, `stackItemAmount`, `stackItemText`, NOW()
FROM `tiki_acct_stackitem` WHERE `stackBookId`=? AND `stackItemStackId`=?";
$res = $this->query($query, [$bookId, $journalId, $bookId, $stackId]);
if ($res === false) {
$errors[] = tra('Booking error confirming stack entry') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
$errors[] = $this->manualRollback($bookId, $journalId);
return $errors;
}
$this->stackDelete($bookId, $stackId);
$query = "UPDATE `tiki_acct_statement` SET `statementJournalId`=? WHERE `statementBookId`=? AND `statementStackId`=?";
$res = $this->query($query, [$journalId, $bookId, $stackId]);
return true;
}
/**
*
* Retrieves one entry from the stack
*
* @param int $bookId id of the current book
* @param int $journalId id of the post in the journal
* @return array|bool array with post, false on error
*/
public function getStackTransaction($bookId, $stackId)
{
$query = "SELECT * FROM `tiki_acct_stack` WHERE `stackBookId`=? AND `stackId`=?";
$res = $this->query($query, [$bookId, $stackId]);
if ($res === false) {
return false;
}
$entry = $res->fetchRow();
$query = "SELECT * FROM `tiki_acct_stackitem` WHERE `stackBookId`=? AND `stackItemStackId`=? AND `stackItemType`=? ORDER BY `stackItemAccountId` ASC";
$entry['debit'] = $this->fetchAll($query, [$bookId, $entry['stackId'], -1]);
$entry['debitcount'] = count($entry['debit']);
$entry['credit'] = $this->fetchAll($query, [$bookId, $entry['stackId'], 1]);
$entry['creditcount'] = count($entry['credit']);
$entry['maxcount'] = max($entry['creditcount'], $entry['debitcount']);
return $entry;
}
/**
* Returns a list of bankaccounts which are related to internal accounts
* @param int $bookId id if the current book
*
* @return array list of accounts
*/
public function getBankAccounts($bookId)
{
$query = "SELECT * FROM `tiki_acct_bankaccount` INNER JOIN `tiki_acct_account`
ON `tiki_acct_bankaccount`.`bankBookId` = `tiki_acct_account`.`accountBookId` AND
`tiki_acct_bankaccount`.`bankAccountId`=`tiki_acct_account`.`accountId`
WHERE `tiki_acct_bankaccount`.`bankBookId`=?";
return $this->fetchAll($query, [$bookId]);
}
/**
* Returns a list of bank statements which have been uploaded but not yet been processed
*
* @param int $bookId id of the current book
* @param int $accountId id of the account to fetch the statements for
* @return array|bool list of statements or false if an error occurred
*/
public function getOpenStatements($bookId, $accountId)
{
$query = "SELECT * FROM `tiki_acct_statement`
WHERE `statementJournalId`=0 AND `statementStackId`=0
AND `statementBookId`=? AND `statementAccountId`=?";
return $this->fetchAll($query, [$bookId, $accountId]);
}
/**
* Returns the statement with the given Id from the list of statements
*
* @param int $statetmentId id of the statement to retrieve
* @return array|bool statement data or false on error
*/
public function getStatement($statementId)
{
$query = "SELECT * FROM `tiki_acct_statement` WHERE `statementId`=?";
$res = $this->query($query, [$statementId]);
if ($res === false) {
return $res;
}
return $res->fetchRow();
}
/**
* Returns the import specification for a given accountId
* @param int $bookId id of the current book
* @param int $accountId id of the account we want the specs for
* @return array|bool list of statements or false
*/
public function getBankAccount($bookId, $accountId)
{
$query = "SELECT * FROM `tiki_acct_bankaccount` WHERE bankBookId=? and bankAccountId=?";
$res = $this->query($query, [$bookId, $accountId]);
if ($res === false) {
return $res;
}
return $res->fetchRow();
}
/**
* Splits a header line into a matching array according to the specifications
*
* @param string $header line containing headers
* @param array $defs file definitions
* @return array list of statements
*/
public function analyzeHeader($header, $defs)
{
$cols = explode($defs['bankDelimeter'], $header);
$columns = [];
for ($i = 0, $isizeof_cols = count($cols); $i < $isizeof_cols; $i++) {
switch ($cols[$i]) {
case $defs['fieldNameAccount']:
$columns['accountId'] = $i;
break;
case $defs['fieldNameBookingDate']:
$columns['bookingDate'] = $i;
break;
case $defs['fieldNameValueDate']:
$columns['valueDate'] = $i;
break;
case $defs['fieldNameBookingText']:
$columns['bookingText'] = $i;
break;
case $defs['fieldNameReason']:
$columns['reason'] = $i;
break;
case $defs['fieldNameCounterpartName']:
$columns['counterpartName'] = $i;
break;
case $defs['fieldNameCounterpartAccount']:
$columns['counterpartAccount'] = $i;
break;
case $defs['fieldNameCounterpartBankcode']:
$columns['counterpartBankcode'] = $i;
break;
case $defs['fieldNameAmount']:
$columns['amount'] = $i;
break;
case $defs['fieldNameAmountSign']:
$columns['amountSign'] = $i;
break;
}
}
return $columns;
}
/**
* updates journalId in the given statement
*
* @param int $statementId id of the statement to update
* @param int $journalId id of the entry in the journal which was caused by this statement
* @return array|boolean list of errors, empty if no errors were found
*/
public function updateStatement($statementId, $journalId)
{
$errors = [];
$query = "UPDATE `tiki_acct_statement` SET `statementJournalId`=? WHERE `statementId`=?";
$res = $this->query($query, [$journalId, $statementId]);
if ($res === false) {
$errors[] = tra('Error while updating statement:') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
return $errors;
}
return true;
}
/**
* updates journalId in the given statement
*
* @param int $statementId id of the statement to update
* @param int $journalId id of the entry in the journal which was caused by this statement
* @return array|bool list of errors, empty if no errors were found
*/
public function updateStatementStack($statementId, $stackId)
{
$errors = [];
$query = "UPDATE `tiki_acct_statement` SET `statementStackId`=? WHERE `statementId`=?";
$res = $this->query($query, [$stackId, $statementId]);
if ($res === false) {
$errors[] = tra('Error while updating statement:') . $this->ErrorNo() . ": " . $this->ErrorMsg() . "<br /><pre>$query</pre>";
return $errors;
}
return true;
}
/**
*
* Creates a tax setting for automated tax deduction/splitting
* @param int $bookId
* @param string $taxText
* @param double $taxAmount
* @param string $taxIsFix
* @return int id of the newly created tax
*/
public function createTax($bookId, $taxText, $taxAmount, $taxIsFix = 'n')
{
$query = "INSERT INTO `tiki_acct_tax` (`taxBookId`, `taxText`, `taxAmount`, `taxIsFix`) VALUES (?, ?, ?, ?)";
$res = $this->query($query, [$bookId, $taxText, $taxAmount, $taxIsFix]);
return $this->lastInsertId();
}
/**
* removes all unnecessary thousand markers and replaces local decimal characters with "." to enable handling as numbers.
*
* @param int $bookId id of the current book
* @param string $amount date of the transaction
* @return string/float Returns a float or an empty string if the source is not numeric
*/
public function cleanupAmount($bookId, $amount)
{
$book = $this->getBook($bookId);
$a = str_replace($book['bookDecPoint'], '.', str_replace($book['bookThousand'], '', $amount));
if (! is_numeric($a)) {
return '';
}
return (float)$a;
}
/**
* Checks the existence/non-existence of a numerical id in the given table
*
* @param string $idname name of the id field in the table
* @param int $id the id to check
* @param string $table the table to search
* @param boolean $exists true if a record must exist, false if it must not
*
* @return array Returns aa array of errors (empty if none occurred)
*/
public function validateId($idname, $id, $table, $exists = true, $bookIdName = '', $bookId = 0)
{
$errors = [];
if (! is_numeric($id)) {
$errors[] = htmlspecialchars($idname) . ' (' . htmlspecialchars($id) . ')'
. tra('is not a number.');
} elseif ($id <= 0) {
$errors[] = htmlspecialchars($idname) . ' ' . tra('must be greater than 0.');
} else {
//static whitelist based on usage of the validateId function in accountinglib.php
$tablesWhitelist = [
'tiki_acct_tax' => [
'idname' => 'taxId',
'bookIdName' => 'taxBookId'
],
'tiki_acct_account' => [
'idname' => 'accountId',
'bookIdName' => 'accountBookId'
]
];
if (! array_key_exists($table, $tablesWhitelist)) {
$errors[] = tra('Invalid transaction - please contact administrator.');
} elseif ($idname !== $tablesWhitelist[$table]['idname']) {
$errors[] = tra('Invalid transaction - please contact administrator.');
} else {
$query = "SELECT $idname FROM $table WHERE $idname = ?";
$bindvars = [$id];
if ($bookIdName === $tablesWhitelist[$table]['bookIdName']) {
$query .= " AND $bookIdName = ?";
array_push($bindvars, $bookId);
}
$res = $this->query($query, $bindvars);
if ($res === false) {
$errors[] = tra('Error checking') . htmlspecialchars($idname) . ': ' . $this->ErrorNo() . ': '
. $this->ErrorMsg() . '<br /><pre>' . htmlspecialchars($query) . '</pre>';
} else {
if ($exists) {
if ($res->numRows() == 0) {
$errors[] = htmlspecialchars($idname) . ' ' . tra('does not exist.');
}
} else {
if ($res->numRows() > 0) {
$errors[] = htmlspecialchars($idname) . ' ' . tra('already exists');
}
}
}
}
}
return $errors;
}
}