3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Transaction class for galette
10 * Copyright © 2011-2023 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2011-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2011-07-31
37 namespace Galette\Entity
;
40 use Galette\Events\GaletteEvent
;
43 use Laminas\Db\Sql\Expression
;
44 use Galette\Repository\Contributions
;
46 use Galette\Core\History
;
47 use Galette\Core\Login
;
48 use Galette\Features\Dynamics
;
51 * Transaction class for galette
56 * @author Johan Cwiklinski <johan@x-tnd.be>
57 * @copyright 2010-2023 The Galette Team
58 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
59 * @link http://galette.tuxfamily.org
60 * @since Available since 0.7dev - 2010-03-11
62 * @property integer $id
63 * @property date $date
64 * @property integer $amount
65 * @property string $description
66 * @property integer $member
72 public const TABLE
= 'transactions';
73 public const PK
= 'trans_id';
78 private $_description;
81 //fields list and their translation
92 * @param Db $zdb Database instance
93 * @param Login $login Login instance
94 * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load
95 * a specific transaction, or null to just
98 public function __construct(Db
$zdb, Login
$login, $args = null)
101 $this->login
= $login;
104 * Fields configuration. Each field is an array and must reflect:
110 * I'd prefer a static private variable for this...
111 * But call to the _T function does not seem to be allowed there :/
113 $this->_fields
= array(
115 'label' => null, //not a field in the form
118 'trans_date' => array(
119 'label' => _T("Date:"), //not a field in the form
122 'trans_amount' => array(
123 'label' => _T("Amount:"),
124 'propname' => 'amount'
126 'trans_desc' => array(
127 'label' => _T("Description:"),
128 'propname' => 'description'
130 Adherent
::PK
=> array(
131 'label' => _T("Originator:"),
132 'propname' => 'member'
135 if ($args === null ||
is_int($args)) {
136 $this->_date
= date("Y-m-d");
138 if (is_int($args) && $args > 0) {
141 } elseif (is_object($args)) {
142 $this->loadFromRS($args);
145 $this->loadDynamicFields();
149 * Loads a transaction from its id
151 * @param int $id the identifier for the transaction to load
153 * @return bool true if query succeed, false otherwise
155 public function load($id)
157 if (!$this->login
->isLogged()) {
159 'Non-logged-in users cannot load transaction id `' . $id,
166 $select = $this->zdb
->select(self
::TABLE
, 't');
167 $select->where([self
::PK
=> $id]);
169 array('a' => PREFIX_DB
. Adherent
::TABLE
),
170 't.' . Adherent
::PK
. '=a.' . Adherent
::PK
,
174 //restrict query on current member id if he's not admin nor staff member
175 if (!$this->login
->isAdmin() && !$this->login
->isStaff()) {
178 ->equalTo('a.' . Adherent
::PK
, $this->login
->id
)
180 ->equalTo('a.parent_id', $this->login
->id
)
183 ->equalTo('t.' . self
::PK
, $id)
186 $select->where
->equalTo(self
::PK
, $id);
189 $results = $this->zdb
->execute($select);
190 if ($results->count() > 0) {
191 /** @var ArrayObject $result */
192 $result = $results->current();
193 $this->loadFromRS($result);
197 'Transaction id `' . $id . '` does not exists',
202 } catch (Throwable
$e) {
204 'Cannot load transaction form id `' . $id . '` | ' .
213 * Remove transaction (and all associated contributions) from database
215 * @param History $hist History
216 * @param boolean $transaction Activate transaction mode (defaults to true)
220 public function remove(History
$hist, $transaction = true)
226 $this->zdb
->connection
->beginTransaction();
229 //remove associated contributions if needeed
230 if ($this->getDispatchedAmount() > 0) {
231 $c = new Contributions($this->zdb
, $this->login
);
232 $clist = $c->getListFromTransaction($this->_id
);
234 foreach ($clist as $cid) {
237 $c->remove($cids, $hist, false);
240 //remove transaction itself
241 $delete = $this->zdb
->delete(self
::TABLE
);
242 $delete->where([self
::PK
=> $this->_id
]);
243 $del = $this->zdb
->execute($delete);
244 if ($del->count() > 0) {
245 $this->dynamicsRemove(true);
248 'Transaction has not been removed!',
255 $this->zdb
->connection
->commit();
258 $emitter->dispatch(new GaletteEvent('transaction.remove', $this));
260 } catch (Throwable
$e) {
262 $this->zdb
->connection
->rollBack();
265 'An error occurred trying to remove transaction #' .
266 $this->_id
. ' | ' . $e->getMessage(),
274 * Populate object from a resultset row
276 * @param ArrayObject $r the resultset row
280 private function loadFromRS(ArrayObject
$r)
283 $this->_id
= $r->$pk;
284 $this->_date
= $r->trans_date
;
285 $this->_amount
= $r->trans_amount
;
286 $this->_description
= $r->trans_desc
;
287 $adhpk = Adherent
::PK
;
288 $this->_member
= (int)$r->$adhpk;
290 $this->loadDynamicFields();
294 * Check posted values validity
296 * @param array $values All values to check, basically the $_POST array
297 * after sending the form
298 * @param array $required Array of required fields
299 * @param array $disabled Array of disabled fields
303 public function check($values, $required, $disabled)
305 $this->errors
= array();
307 $fields = array_keys($this->_fields
);
308 foreach ($fields as $key) {
309 //first, let's sanitize values
310 $key = strtolower($key);
311 $prop = '_' . $this->_fields
[$key]['propname'];
313 if (isset($values[$key])) {
314 $value = trim($values[$key]);
319 // if the field is enabled, check it
320 if (!isset($disabled[$key])) {
321 // now, check validity
327 $d = \DateTime
::createFromFormat(__("Y-m-d"), $value);
329 //try with non localized date
330 $d = \DateTime
::createFromFormat("Y-m-d", $value);
332 throw new \
Exception('Incorrect format');
335 $this->$prop = $d->format('Y-m-d');
336 } catch (Throwable
$e) {
338 'Wrong date format. field: ' . $key .
339 ', value: ' . $value . ', expected fmt: ' .
340 __("Y-m-d") . ' | ' . $e->getMessage(),
343 $this->errors
[] = str_replace(
350 $this->getFieldLabel($key)
352 _T("- Wrong date format (%date_format) for %field!")
357 $this->_member
= (int)$value;
360 $this->_amount
= $value;
361 $value = strtr($value, ',', '.');
362 if (!is_numeric($value)) {
363 $this->errors
[] = _T("- The amount must be an integer!");
367 /** TODO: retrieve field length from database and check that */
368 $this->_description
= strip_tags($value);
369 if (mb_strlen($value) > 150) {
370 $this->errors
[] = _T("- Transaction description must be 150 characters long maximum.");
378 // missing required fields?
379 foreach ($required as $key => $val) {
381 $prop = '_' . $this->_fields
[$key]['propname'];
382 if (!isset($disabled[$key]) && !isset($this->$prop)) {
383 $this->errors
[] = str_replace(
385 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
386 _T("- Mandatory field %field empty.")
392 if ($this->_id
!= '') {
393 $dispatched = $this->getDispatchedAmount();
394 if ($dispatched > $this->_amount
) {
395 $this->errors
[] = _T("- Sum of all contributions exceed corresponding transaction amount.");
399 $this->dynamicsCheck($values, $required, $disabled);
401 if (count($this->errors
) > 0) {
403 'Some errors has been thew attempting to edit/store a transaction' .
404 print_r($this->errors
, true),
407 return $this->errors
;
410 'Transaction checked successfully.',
418 * Store the transaction
420 * @param History $hist History
424 public function store(History
$hist)
431 $this->zdb
->connection
->beginTransaction();
433 $fields = $this->getDbFields($this->zdb
);
435 foreach ($fields as $field) {
436 $prop = '_' . $this->_fields
[$field]['propname'];
437 $values[$field] = $this->$prop;
441 if (!isset($this->_id
) ||
$this->_id
== '') {
442 //we're inserting a new transaction
443 unset($values[self
::PK
]);
444 $insert = $this->zdb
->insert(self
::TABLE
);
445 $insert->values($values);
446 $add = $this->zdb
->execute($insert);
447 if ($add->count() > 0) {
448 $this->_id
= $this->zdb
->getLastGeneratedValue($this);
452 _T("Transaction added"),
453 Adherent
::getSName($this->zdb
, $this->_member
)
456 $event = 'transaction.add';
458 $hist->add(_T("Fail to add new transaction."));
459 throw new \
RuntimeException(
460 'An error occurred inserting new transaction!'
464 //we're editing an existing transaction
465 $update = $this->zdb
->update(self
::TABLE
);
466 $update->set($values)->where([self
::PK
=> $this->_id
]);
467 $edit = $this->zdb
->execute($update);
468 //edit == 0 does not mean there were an error, but that there
469 //were nothing to change
470 if ($edit->count() > 0) {
472 _T("Transaction updated"),
473 Adherent
::getSName($this->zdb
, $this->_member
)
477 $event = 'transaction.edit';
482 $this->dynamicsStore(true);
485 $this->zdb
->connection
->commit();
487 //send event at the end of process, once all has been stored
488 if ($event !== null) {
489 $emitter->dispatch(new GaletteEvent($event, $this));
493 } catch (Throwable
$e) {
494 $this->zdb
->connection
->rollBack();
496 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
497 $e->getTraceAsString(),
505 * Retrieve amount that has already been dispatched into contributions
509 public function getDispatchedAmount(): float
511 if (empty($this->_id
)) {
516 $select = $this->zdb
->select(Contribution
::TABLE
);
519 'sum' => new Expression('SUM(montant_cotis)')
521 )->where([self
::PK
=> $this->_id
]);
523 $results = $this->zdb
->execute($select);
524 $result = $results->current();
525 $dispatched_amount = $result->sum
;
526 return (double)$dispatched_amount;
527 } catch (Throwable
$e) {
529 'An error occurred retrieving dispatched amounts | ' .
538 * Retrieve amount that has not yet been dispatched into contributions
542 public function getMissingAmount()
544 if (empty($this->_id
)) {
545 return (double)$this->amount
;
549 $select = $this->zdb
->select(Contribution
::TABLE
);
552 'sum' => new Expression('SUM(montant_cotis)')
554 )->where([self
::PK
=> $this->_id
]);
556 $results = $this->zdb
->execute($select);
557 $result = $results->current();
558 $dispatched_amount = $result->sum
;
559 return (double)$this->_amount
- (double)$dispatched_amount;
560 } catch (Throwable
$e) {
562 'An error occurred retrieving missing amounts | ' .
571 * Retrieve fields from database
573 * @param Db $zdb Database instance
577 public function getDbFields(Db
$zdb)
579 $columns = $zdb->getColumns(self
::TABLE
);
581 foreach ($columns as $col) {
582 $fields[] = $col->getName();
588 * Get the relevant CSS class for current transaction
590 * @return string current transaction row class
592 public function getRowClass()
594 return ($this->getMissingAmount() == 0) ?
595 'transaction-normal' : 'transaction-uncomplete';
599 * Global getter method
601 * @param string $name name of the property we want to retrive
603 * @return mixed the called property
605 public function __get($name)
607 $forbidden = array();
609 $rname = '_' . $name;
610 if (!in_array($name, $forbidden) && property_exists($this, $rname)) {
613 if ($this->$rname != '') {
615 $d = new \
DateTime($this->$rname);
616 return $d->format(__("Y-m-d"));
617 } catch (Throwable
$e) {
618 //oops, we've got a bad date :/
620 'Bad date (' . $this->$rname . ') | ' .
624 return $this->$rname;
629 if ($this->$rname !== null) {
630 return (int)$this->$rname;
634 if ($this->$rname !== null) {
635 return (double)$this->$rname;
639 return $this->$rname;
644 'Property %1$s does not exists for transaction',
654 * Global isset method
655 * Required for twig to access properties via __get
657 * @param string $name name of the property we want to retrive
661 public function __isset($name)
663 $forbidden = array();
665 $rname = '_' . $name;
666 if (!in_array($name, $forbidden) && property_exists($this, $rname)) {
676 * @param string $field Field name
680 public function getFieldLabel($field)
682 $label = $this->_fields
[$field]['label'];
684 $label = str_replace(' ', ' ', $label);
685 //remove trailing ':' and then trim
686 $label = trim(trim($label, ':'));
691 * Handle files (dynamics files)
693 * @param array $files Files sent
697 public function handleFiles($files)
701 $this->dynamicsFiles($files);
703 if (count($this->errors
) > 0) {
705 'Some errors has been thew attempting to edit/store a transaction files' . "\n" .
706 print_r($this->errors
, true),
709 return $this->errors
;
716 * Can current logged-in user display transaction
718 * @param Login $login Login instance
722 public function canShow(Login
$login): bool
724 //non-logged-in members cannot show contributions
725 if (!$login->isLogged()) {
729 //admin and staff users can edit, as well as member itself
730 if (!$this->id ||
$this->id
&& $login->id
== $this->_member ||
$login->isAdmin() ||
$login->isStaff()) {
734 //parent can see their children transactions
735 $parent = new Adherent($this->zdb
);
738 ->enableDep('children')
739 ->load($this->login
->id
);
740 if ($parent->hasChildren()) {
741 foreach ($parent->children
as $child) {
742 if ($child->id
=== $this->_member
) {