- Fix color for staff members on member cards
- Display first staff members on public lists
- Identify sponsors in members list
+- Add scheduled payments feature
+- Dispatch contribution into scheduled payments
1.0.3 -> 1.0.4
'pdfModels' => 'staff',
'attendance_sheet_details' => 'groupmanager',
'attendance_sheet' => 'groupmanager',
- '/(.+)?document(.+)?/i' => 'staff'
+ '/(.+)?document(.+)?/i' => 'staff',
+ 'myScheduledPayments' => 'member',
+ '/(.+)?scheduledPayment(.+)?/i' => 'staff'
];
'/contribution/do-mass-add',
[Crud\ContributionsController::class, 'doMassAddContributions']
)->setName('doMassAddContributions')->add($authenticate);
+
+$app->get(
+ '/scheduled-payments/mine',
+ [Crud\ScheduledPaymentController::class, 'myList']
+)->setName('myScheduledPayments')->add($authenticate);
+
+$app->get(
+ '/scheduled-payments[/{option:page|order|member}/{value:\d+|all}]',
+ [Crud\ScheduledPaymentController::class, 'list']
+)->setName('scheduledPayments')->add($authenticate);
+
+$app->post(
+ '/scheduled-payments/filter',
+ [Crud\ScheduledPaymentController::class, 'filter']
+)->setName('filterScheduledPayments')->add($authenticate);
+
+$app->get(
+ '/scheduled-payment/{id_cotis:\d+}/add',
+ [Crud\ScheduledPaymentController::class, 'add']
+)->setName('addScheduledPayment')->add($authenticate);
+
+$app->get(
+ '/scheduled-payment/edit/{id:\d+}',
+ [Crud\ScheduledPaymentController::class, 'edit']
+)->setName('editScheduledPayment')->add($authenticate);
+
+$app->post(
+ '/scheduled-payments/{id_cotis:\d+}/add',
+ [Crud\ScheduledPaymentController::class, 'doAdd']
+)->setName('doAddScheduledPayment')->add($authenticate);
+
+$app->post(
+ '/scheduled-payments/edit/{id:\d+}',
+ [Crud\ScheduledPaymentController::class, 'doEdit']
+)->setName('doEditScheduledPayment')->add($authenticate);
+
+//Batch actions on scheduled payments list
+$app->post(
+ '/scheduled-payments/batch',
+ [Crud\ScheduledPaymentController::class, 'handleBatch']
+)->setName('batch-scheduledPaymentslist')->add($authenticate);
+
+//scheduled payments list CSV export
+$app->map(
+ ['GET', 'POST'],
+ '/scheduled-payments/export/csv',
+ [CsvController::class, 'scheduledPaymentsExport']
+)->setName('csv-scheduledPaymentslist')->add($authenticate);
+
+$app->get(
+ '/scheduled-payment/remove' . '/{id:\d+}',
+ [Crud\ScheduledPaymentController::class, 'confirmDelete']
+)->setName('removeScheduledPayment')->add($authenticate);
+
+$app->get(
+ '/scheduled-payment/batch/remove',
+ [Crud\ScheduledPaymentController::class, 'confirmDelete']
+)->setName('removeScheduledPayments')->add($authenticate);
+
+$app->post(
+ '/scheduled-payment/remove[/{id}]',
+ [Crud\ScheduledPaymentController::class, 'delete']
+)->setName('doRemoveScheduledPayment')->add($authenticate);
KEY (type)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
+-- table for payments schedules
+DROP TABLE IF EXISTS galette_payments_schedules;
+CREATE TABLE galette_payments_schedules (
+ id_schedule int(10) unsigned NOT NULL auto_increment,
+ id_cotis int(10) unsigned NOT NULL,
+ id_paymenttype int(10) unsigned NOT NULL,
+ creation_date datetime NOT NULL,
+ scheduled_date datetime NOT NULL,
+ amount decimal(15, 2) NOT NULL,
+ paid tinyint(1) DEFAULT FALSE,
+ comment text,
+ PRIMARY KEY (id_schedule),
+ FOREIGN KEY (id_cotis) REFERENCES galette_cotisations (id_cotis) ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY (id_paymenttype) REFERENCES galette_paymenttypes (type_id) ON DELETE CASCADE ON UPDATE CASCADE
+);
+
-- table for database version
DROP TABLE IF EXISTS galette_database;
CREATE TABLE galette_database (
MINVALUE 1
CACHE 1;
+-- sequence for payments schedules
+DROP SEQUENCE IF EXISTS galette_payments_schedules_id_seq;
+CREATE SEQUENCE galette_payments_schedules_id_seq
+ START 1
+ INCREMENT 1
+ MAXVALUE 2147483647
+ MINVALUE 1
+ CACHE 1;
+
-- Schema
-- REMINDER: Create order IS important, dependencies first !!
DROP TABLE IF EXISTS galette_paymenttypes CASCADE;
-- add index on table to look for type
CREATE INDEX galette_documents_idx ON galette_documents (type);
+-- table for payments schedules
+DROP TABLE IF EXISTS galette_payments_schedules CASCADE;
+CREATE TABLE galette_payments_schedules (
+ id_schedule integer DEFAULT nextval('galette_payments_schedules_id_seq'::text) NOT NULL,
+ id_cotis integer REFERENCES galette_cotisations (id_cotis) ON DELETE CASCADE ON UPDATE CASCADE,
+ id_paymenttype integer REFERENCES galette_paymenttypes (type_id) ON DELETE RESTRICT ON UPDATE CASCADE,
+ creation_date date NOT NULL,
+ scheduled_date date NOT NULL,
+ amount decimal(15,2) NOT NULL,
+ paid boolean DEFAULT FALSE,
+ comment text,
+ PRIMARY KEY (id_schedule)
+);
+
-- table for database version
DROP TABLE IF EXISTS galette_database CASCADE;
CREATE TABLE galette_database (
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
+-- change fields types and default values
+ALTER TABLE galette_cotisations CHANGE montant_cotis montant_cotis decimal(15,2) NOT NULL;
+ALTER TABLE galette_transactions CHANGE trans_amount trans_amount decimal(15,2) NOT NULL;
+
+-- table for payments schedules
+DROP TABLE IF EXISTS galette_payments_schedules;
+CREATE TABLE galette_payments_schedules (
+ id_schedule int(10) unsigned NOT NULL auto_increment,
+ id_cotis int(10) unsigned NOT NULL,
+ id_paymenttype int(10) unsigned NOT NULL,
+ creation_date datetime NOT NULL,
+ scheduled_date datetime NOT NULL,
+ amount decimal(15,2) NOT NULL,
+ paid tinyint(1) DEFAULT FALSE,
+ comment text,
+ PRIMARY KEY (id_schedule),
+ FOREIGN KEY (id_cotis) REFERENCES galette_cotisations (id_cotis) ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY (id_paymenttype) REFERENCES galette_paymenttypes (type_id) ON DELETE CASCADE ON UPDATE CASCADE
+);
+
-- change fields types and default values
ALTER TABLE galette_cotisations CHANGE montant_cotis montant_cotis decimal(15,2) NOT NULL;
ALTER TABLE galette_transactions CHANGE trans_amount trans_amount decimal(15,2) NOT NULL;
-- add index on table to look for type
CREATE INDEX galette_documents_idx ON galette_documents (type);
+-- change fields types and default values
+ALTER TABLE galette_cotisations ALTER COLUMN montant_cotis TYPE decimal(15,2);
+ALTER TABLE galette_cotisations ALTER COLUMN montant_cotis DROP DEFAULT;
+ALTER TABLE galette_cotisations ALTER COLUMN montant_cotis SET NOT NULL;
+ALTER TABLE galette_transactions ALTER COLUMN trans_amount TYPE decimal(15,2);
+ALTER TABLE galette_transactions ALTER COLUMN trans_amount DROP DEFAULT;
+ALTER TABLE galette_transactions ALTER COLUMN trans_amount SET NOT NULL;
+
+-- sequence for payments schedules
+DROP SEQUENCE IF EXISTS galette_payments_schedules_id_seq;
+CREATE SEQUENCE galette_payments_schedules_id_seq
+ START 1
+ INCREMENT 1
+ MAXVALUE 2147483647
+ MINVALUE 1
+ CACHE 1;
+
+-- table for payments schedules
+DROP TABLE IF EXISTS galette_payments_schedules CASCADE;
+CREATE TABLE galette_payments_schedules (
+ id_schedule integer DEFAULT nextval('galette_payments_schedules_id_seq'::text) NOT NULL,
+ id_cotis integer REFERENCES galette_cotisations (id_cotis) ON DELETE CASCADE ON UPDATE CASCADE,
+ id_paymenttype integer REFERENCES galette_paymenttypes (type_id) ON DELETE RESTRICT ON UPDATE CASCADE,
+ creation_date date NOT NULL,
+ scheduled_date date NOT NULL,
+ amount decimal(15,2) NOT NULL,
+ paid boolean DEFAULT FALSE,
+ comment text,
+ PRIMARY KEY (id_schedule)
+);
-- change fields types and default values
ALTER TABLE galette_cotisations ALTER COLUMN montant_cotis TYPE decimal(15,2);
ALTER TABLE galette_cotisations ALTER COLUMN montant_cotis DROP DEFAULT;
namespace Galette\Controllers\Crud;
+use Galette\Entity\PaymentType;
+use Galette\Entity\ScheduledPayment;
use Galette\Features\BatchList;
use Analog\Analog;
use Galette\Controllers\CrudController;
// contribution types
$params['type_cotis_options'] = $contributions_types;
+ if ($contrib->id != '') {
+ $params['scheduled'] = new ScheduledPayment($this->zdb, $contrib->id);
+ }
+
// members
$m = new Members();
$members = $m->getDropdownMembers(
if (count($error_detected) == 0) {
$this->session->contribution = null;
if ($contrib->isTransactionPart() && $contrib->transaction->getMissingAmount() > 0) {
- //new contribution
+ //if part of a transaction, and transaction is not fully allocated, create a new contribution
$redirect_url = $this->routeparser->urlFor(
'addContribution',
[
- 'type' => $post['contrib_type'] ?? $type
+ 'type' => $post['contrib_type'] ?? $type
]
) . '?' . Transaction::PK . '=' . $contrib->transaction->id .
'&' . Adherent::PK . '=' . $contrib->member;
+ } elseif ($contrib->payment_type === PaymentType::SCHEDULED/* && !$contrib->isScheduleFullyAllocated() */) {
+ //if payment type is a payment schedule, and schedule is not fully allocated, create a new schedule entry
+ $redirect_url = $this->routeparser->urlFor(
+ 'addScheduledPayment',
+ [
+ Contribution::PK => $contrib->id
+ ]
+ );
} else {
//contributions list for member
$redirect_url = $this->routeparser->urlFor(
--- /dev/null
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\Controllers\Crud;
+
+use Galette\Controllers\CrudController;
+use Galette\Entity\Adherent;
+use Galette\Entity\Contribution;
+use Galette\Entity\ScheduledPayment;
+use Galette\Filters\ScheduledPaymentsList;
+use Galette\Repository\ScheduledPayments;
+use Slim\Psr7\Request;
+use Slim\Psr7\Response;
+use Galette\Repository\PaymentTypes;
+
+/**
+ * Galette payment types controller
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ */
+
+class ScheduledPaymentController extends CrudController
+{
+ // CRUD - Create
+
+ /**
+ * Add page
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ * @param integer $id_cotis Contribution id
+ *
+ * @return Response
+ */
+ public function add(Request $request, Response $response, int $id_cotis = 0): Response
+ {
+ if (isset($this->session->scheduled_payment)) {
+ $scheduled = $this->session->scheduled_payment;
+ unset($this->session->scheduled_payment);
+ } else {
+ $scheduled = new ScheduledPayment($this->zdb);
+ }
+ $scheduled->setContribution($id_cotis);
+ $mode = $request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest' ? 'ajax' : '';
+
+ if ($scheduled->getMissingAmount() == 0) {
+ $this->flash->addMessage(
+ 'error_detected',
+ _T("Contribution is fully scheduled!")
+ );
+ return $response
+ ->withStatus(301)
+ ->withHeader(
+ 'Location',
+ $this->routeparser->urlFor(
+ 'editContribution',
+ [
+ 'type' => ($scheduled->getContribution()->isFee() ? Contribution::TYPE_FEE : Contribution::TYPE_DONATION),
+ 'id' => $id_cotis
+ ]
+ )
+ );
+ }
+
+ // display page
+ $this->view->render(
+ $response,
+ 'pages/scheduledpayment_form.html.twig',
+ [
+ 'page_title' => _T("Add scheduled payment"),
+ 'scheduled' => $scheduled,
+ 'mode' => $mode
+ ]
+ );
+ return $response;
+ }
+
+ /**
+ * Add action
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ *
+ * @return Response
+ */
+ public function doAdd(Request $request, Response $response): Response
+ {
+ return $this->store($request, $response, null);
+ }
+
+ // /CRUD - Create
+ // CRUD - Read
+
+ /**
+ * List page
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ * @param string|null $option One of 'page' or 'order'
+ * @param integer|string|null $value Value of the option
+ *
+ * @return Response
+ */
+ public function list(Request $request, Response $response, string $option = null, int|string $value = null): Response
+ {
+ $get = $request->getQueryParams();
+ $ajax = false;
+ $session_varname = $this->getFilterName();
+
+ if (
+ ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')
+ || isset($get['ajax'])
+ && $get['ajax'] == 'true'
+ ) {
+ $ajax = true;
+ $session_varname = 'ajax_' . $session_varname;
+ }
+
+ if (isset($this->session->$session_varname)) {
+ $filters = $this->session->$session_varname;
+ } else {
+ $filters = new ScheduledPaymentsList();
+ }
+
+ if ($ajax && $get[Contribution::PK]) {
+ $filters->from_contribution = (int)$get[Contribution::PK];
+ }
+
+ if ($option !== null) {
+ switch ($option) {
+ case 'page':
+ $filters->current_page = (int)$value;
+ break;
+ case 'order':
+ $filters->orderby = $value;
+ break;
+ }
+ }
+
+ $scheduled = new ScheduledPayments(
+ $this->zdb,
+ $this->login,
+ $filters
+ );
+ $list = $scheduled->getList();
+
+ //store filters into session
+ if ($ajax === false) {
+ $this->session->$session_varname = $filters;
+ }
+
+ //assign pagination variables to the template and add pagination links
+ $filters->setViewPagination($this->routeparser, $this->view);
+
+ // display page
+ $this->view->render(
+ $response,
+ 'pages/scheduledpayments_list.html.twig',
+ [
+ 'page_title' => _T("Scheduled payments management"),
+ 'scheduled' => $scheduled,
+ 'list' => $list,
+ 'nb' => $scheduled->getCount(),
+ 'filters' => $filters,
+ 'mode' => $ajax ? 'ajax' : ''
+ ]
+ );
+ return $response;
+ }
+
+ /**
+ * List page for logged-in member
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ * @param string|null $type One of 'transactions' or 'contributions'
+ *
+ * @return Response
+ */
+ public function myList(Request $request, Response $response, string $type = null): Response
+ {
+ return $this->list(
+ $request->withQueryParams(
+ $request->getQueryParams() + [
+ Adherent::PK => $this->login->id
+ ]
+ ),
+ $response
+ );
+ }
+
+ /**
+ * Scheduled payments filtering
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ *
+ * @return Response
+ */
+ public function filter(Request $request, Response $response): Response
+ {
+ $ajax = false;
+ $filter_name = $this->getFilterName();
+ if ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') {
+ $ajax = true;
+ $filter_name = 'ajax_' . $filter_name;
+ }
+
+ $post = $request->getParsedBody();
+ $error_detected = [];
+
+ if ($this->session->$filter_name !== null) {
+ $filters = $this->session->$filter_name;
+ } else {
+ $filters = new ScheduledPaymentsList();
+ }
+
+ if (isset($post['clear_filter'])) {
+ $filters->reinit($ajax);
+ } else {
+ if (
+ (isset($post['nbshow']) && is_numeric($post['nbshow']))
+ ) {
+ $filters->show = $post['nbshow'];
+ }
+
+ if (isset($post['date_field'])) {
+ $filters->date_field = $post['date_field'];
+ }
+
+ if (isset($post['end_date_filter']) || isset($post['start_date_filter'])) {
+ if (isset($post['start_date_filter'])) {
+ $filters->start_date_filter = $post['start_date_filter'];
+ }
+ if (isset($post['end_date_filter'])) {
+ $filters->end_date_filter = $post['end_date_filter'];
+ }
+ }
+
+ if (isset($post['payment_type_filter'])) {
+ $ptf = (int)$post['payment_type_filter'];
+ $ptypes = new PaymentTypes(
+ $this->zdb,
+ $this->preferences,
+ $this->login
+ );
+ $ptlist = $ptypes->getList(false);
+ if (isset($ptlist[$ptf])) {
+ $filters->payment_type_filter = $ptf;
+ } elseif ($ptf == -1) {
+ $filters->payment_type_filter = null;
+ } else {
+ $error_detected[] = _T("- Unknown payment type!");
+ }
+ }
+ }
+
+ $this->session->$filter_name = $filters;
+
+ if (count($error_detected) > 0) {
+ //report errors
+ foreach ($error_detected as $error) {
+ $this->flash->addMessage(
+ 'error_detected',
+ $error
+ );
+ }
+ }
+
+ return $response
+ ->withStatus(301)
+ ->withHeader('Location', $this->routeparser->urlFor('scheduledPayments'));
+ }
+
+ /**
+ * Batch actions handler
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ *
+ * @return Response
+ */
+ public function handleBatch(Request $request, Response $response): Response
+ {
+ $filter_name = $this->getFilterName();
+ $post = $request->getParsedBody();
+
+ if (isset($post['entries_sel'])) {
+ $filters = $this->session->$filter_name ?? new ScheduledPaymentsList();
+ $filters->selected = $post['entries_sel'];
+ $this->session->$filter_name = $filters;
+
+ if (isset($post['csv'])) {
+ return $response
+ ->withStatus(301)
+ ->withHeader('Location', $this->routeparser->urlFor('csv-scheduledPaymentslist'));
+ }
+
+ if (isset($post['delete'])) {
+ return $response
+ ->withStatus(301)
+ ->withHeader('Location', $this->routeparser->urlFor('removeScheduledPayments'));
+ }
+
+ throw new \RuntimeException('Does not know what to batch :(');
+ } else {
+ $this->flash->addMessage(
+ 'error_detected',
+ _T("No scheduled payment was selected, please check at least one.")
+ );
+
+ return $response
+ ->withStatus(301)
+ ->withHeader('Location', $this->routeparser->urlFor('scheduledPayments'));
+ }
+ }
+
+ // /CRUD - Read
+ // CRUD - Update
+
+ /**
+ * Edit page
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ * @param integer $id Scheduled payment id
+ *
+ * @return Response
+ */
+ public function edit(Request $request, Response $response, int $id): Response
+ {
+ if (isset($this->session->scheduled_payment)) {
+ $scheduled = $this->session->scheduled_payment;
+ unset($this->session->scheduled_payment);
+ } else {
+ $scheduled = new ScheduledPayment($this->zdb, $id);
+ }
+ $mode = $request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest' ? 'ajax' : '';
+
+ // display page
+ $this->view->render(
+ $response,
+ 'pages/scheduledpayment_form.html.twig',
+ [
+ 'page_title' => _T("Edit scheduled payment"),
+ 'scheduled' => $scheduled,
+ 'mode' => $mode
+ ]
+ );
+ return $response;
+ }
+
+ /**
+ * Edit action
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ * @param integer $id Type id
+ *
+ * @return Response
+ */
+ public function doEdit(Request $request, Response $response, int $id): Response
+ {
+ return $this->store($request, $response, $id);
+ }
+
+ /**
+ * Store
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ * @param ?integer $id Type id
+ *
+ * @return Response
+ */
+ public function store(Request $request, Response $response, int $id = null): Response
+ {
+ $post = $request->getParsedBody();
+
+ if (isset($post['cancel'])) {
+ return $response
+ ->withStatus(301)
+ ->withHeader('Location', $this->cancelUri($this->getArgs($request)));
+ }
+
+ $error_detected = [];
+ $msg = null;
+
+ $redirect_uri = $this->redirectUri($this->getArgs($request));
+ $scheduled = new ScheduledPayment($this->zdb, $id);
+
+ if (!$scheduled->check($post)) {
+ $this->session->scheduled_payment = $scheduled;
+ if ($id === null) {
+ $redirect_uri = $this->routeparser->urlFor(
+ 'addScheduledPayment',
+ [
+ Contribution::PK => $post[Contribution::PK]
+ ]
+ );
+ } else {
+ $redirect_uri = $this->routeparser->urlFor(
+ 'editScheduledPayment',
+ [
+ 'id' => (string)$scheduled->getId()
+ ]
+ );
+ }
+ $error_detected = $scheduled->getErrors();
+ } else {
+ $res = $scheduled->store();
+ if (!$res) {
+ $this->session->scheduled_payment = $scheduled;
+ if ($id === null) {
+ $error_detected[] = _T("Scheduled payment has not been added!");
+ } else {
+ $error_detected[] = _T("Scheduled payment has not been modified!");
+ //redirect to edition
+ $redirect_uri = $this->routeparser->urlFor('editScheduledPayment', ['id' => (string)$id]);
+ }
+ } else {
+ if ($id === null) {
+ $msg = _T("Scheduled payment has been successfully added.");
+ } else {
+ $msg = _T("Scheduled payment has been successfully modified.");
+ }
+ }
+ }
+
+ if (count($error_detected) > 0) {
+ foreach ($error_detected as $error) {
+ $this->flash->addMessage(
+ 'error_detected',
+ $error
+ );
+ }
+ } else {
+ $this->flash->addMessage(
+ 'success_detected',
+ $msg
+ );
+ }
+
+ return $response
+ ->withStatus(301)
+ ->withHeader('Location', $redirect_uri);
+ }
+
+
+ // /CRUD - Update
+ // CRUD - Delete
+
+ /**
+ * Get redirection URI
+ *
+ * @param array<string,mixed> $args Route arguments
+ *
+ * @return string
+ */
+ public function redirectUri(array $args): string
+ {
+ return $this->routeparser->urlFor('scheduledPayments');
+ }
+
+ /**
+ * Get form URI
+ *
+ * @param array<string,mixed> $args Route arguments
+ *
+ * @return string
+ */
+ public function formUri(array $args): string
+ {
+ return $this->routeparser->urlFor(
+ 'doRemoveScheduledPayment',
+ $args
+ );
+ }
+
+ /**
+ * Get confirmation removal page title
+ *
+ * @param array<string,mixed> $args Route arguments
+ *
+ * @return string
+ */
+ public function confirmRemoveTitle(array $args): string
+ {
+ return _Tn('Remove scheduled payment', 'Remove scheduled payments', (count($args['ids'] ?? []) > 1 ? 3 : 1));
+ }
+
+ /**
+ * Remove object
+ *
+ * @param array<string,mixed> $args Route arguments
+ * @param array<string,mixed> $post POST values
+ *
+ * @return boolean
+ */
+ protected function doDelete(array $args, array $post): bool
+ {
+ $scheduleds = new ScheduledPayments($this->zdb, $this->login);
+ $rm = $scheduleds->remove($args['ids'] ?? $args['id'], $this->history);
+ return $rm;
+ }
+
+ // CRUD - Delete
+
+ /**
+ * Get filter name in session4
+ *
+ * @param array<string,mixed>|null $args Route arguments
+ *
+ * @return string
+ */
+ public function getFilterName(array $args = null): string
+ {
+ return 'filter_scheduled_payments';
+ }
+}
namespace Galette\Controllers;
use Galette\Filters\ContributionsList;
+use Galette\Filters\ScheduledPaymentsList;
use Galette\IO\ContributionsCsv;
+use Galette\IO\ScheduledPaymentsCsv;
use Laminas\Db\ResultSet\ResultSet;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
return $this->sendResponse($response, $filepath, $filename);
}
+
+ /**
+ * Scheduled payments CSV exports
+ *
+ * @param Request $request PSR Request
+ * @param Response $response PSR Response
+ *
+ * @return Response
+ */
+ public function scheduledPaymentsExport(Request $request, Response $response): Response
+ {
+ $post = $request->getParsedBody();
+ $get = $request->getQueryParams();
+
+ $session_var = $post['session_var'] ?? $get['session_var'] ?? 'filter_scheduled_payments';
+
+ if (isset($this->session->$session_var)) {
+ $filters = $this->session->$session_var;
+ } else {
+ $filters = new ScheduledPaymentsList();
+ }
+
+ $csv = new ScheduledPaymentsCsv(
+ $this->zdb,
+ $this->login
+ );
+ $csv->exportScheduledPayments($filters);
+
+ $filepath = $csv->getPath();
+ $filename = $csv->getFileName();
+
+ return $this->sendResponse($response, $filepath, $filename);
+ }
}
$this->preferences,
$this->login
);
- $ptlist = $ptypes->getList();
+ $ptlist = $ptypes->getList(false);
$m = new Members();
$s = new Status($this->zdb);
'args' => ['type' => 'contributions']
]
],
+ [
+ 'label' => _T('My scheduled payments'),
+ 'title' => _T('View and filter all my scheduled payments'),
+ 'route' => [
+ 'name' => 'myScheduledPayments'
+ ]
+ ],
[
'label' => _T('My transactions'),
'title' => _T('View and filter all my transactions'),
'aliases' => ['editContribution']
]
],
+ [
+ 'label' => _T("List of scheduled payments"),
+ 'title' => _T("View and filter scheduled payments"),
+ 'route' => [
+ 'name' => 'scheduledPayments',
+ 'aliases' => ['addScheduledPayment', 'editScheduledPayment']
+ ]
+ ],
[
'label' => _T("List of transactions"),
'title' => _T("View and filter transactions"),
$this->retrieveEndDate();
}
if (isset($args['payment_type'])) {
- $this->payment_type = $args['payment_type'];
+ $this->setPaymentType((int)$args['payment_type']);
}
} elseif (is_object($args)) {
$this->loadFromRS($args);
break;
case 'type_paiement_cotis':
if ($value != '') {
- $this->payment_type = (int)$value;
+ $this->setPaymentType((int)$value);
}
break;
case 'info_cotis':
return 'contribution';
}
+ /**
+ * Does contribution have attached scheduled payment?
+ *
+ * @return bool
+ * @throws Throwable
+ */
+ public function hasSchedule(): bool
+ {
+ $schedule = new ScheduledPayment($this->zdb);
+ return $schedule->isContributionHandled($this->id ?? 0);
+ }
+
+ /**
+ * Is schedule fully allocated
+ *
+ * @return bool
+ * @throws Throwable
+ */
+ public function isScheduleFullyAllocated(): bool
+ {
+ $schedule = new ScheduledPayment($this->zdb);
+ return $schedule->isFullyAllocated($this);
+ }
+
/**
* Set (and check) payment type
*
$this->ptypes_list = $ptypes->getList();
}
if (isset($this->ptypes_list[$value])) {
- $this->payment_type = $value;
+ if (isset($this->id) && $this->payment_type != $value && $this->hasSchedule()) {
+ $this->errors[] = _T("Cannot change payment type if there is an attached scheduled payment");
+ } else {
+ $this->payment_type = $value;
+ }
} else {
Analog::log(
'Unknown payment type ' . $value,
private Db $zdb;
private int $id;
+ public const SCHEDULED = 7;
public const OTHER = 6;
public const CASH = 1;
public const CREDITCARD = 2;
*
* @param integer $id Identifier
*
- * @return void
+ * @return bool
*/
- private function load(int $id): void
+ public function load(int $id): bool
{
try {
$select = $this->zdb->select(self::TABLE);
$this->id = $id;
$this->name = $res->type_name;
+ return true;
} catch (Throwable $e) {
Analog::log(
'An error occurred loading payment type #' . $id . "Message:\n" .
self::CREDITCARD => _T("Credit card"),
self::CHECK => _T("Check"),
self::TRANSFER => _T("Transfer"),
- self::PAYPAL => _T("Paypal")
+ self::PAYPAL => _T("Paypal"),
+ self::SCHEDULED => _T("Payment schedule")
];
} else {
$systypes = [
self::CREDITCARD => "Credit card",
self::CHECK => "Check",
self::TRANSFER => "Transfer",
- self::PAYPAL => "Paypal"
+ self::PAYPAL => "Paypal",
+ self::SCHEDULED => "Payment schedule"
];
}
return $systypes;
--- /dev/null
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\Entity;
+
+use ArrayObject;
+use DateTime;
+use Galette\Features\EntityHelper;
+use Laminas\Db\Sql\Expression;
+use Laminas\Db\Sql\Predicate\IsNull;
+use Laminas\Db\Sql\Predicate\Operator;
+use Laminas\Db\Sql\Predicate\PredicateSet;
+use Throwable;
+use Galette\Core\Db;
+use Analog\Analog;
+
+/**
+ * Scheduled payment
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ */
+
+class ScheduledPayment
+{
+ use EntityHelper;
+
+ public const TABLE = 'payments_schedules';
+ public const PK = 'id_schedule';
+ private Db $zdb;
+ private int $id;
+ private Contribution $contribution;
+ private PaymentType $payment_type;
+ private string $creation_date;
+ private string $scheduled_date;
+ private float $amount;
+ private bool $is_paid = false;
+ private ?string $comment = null;
+ /** @var string[] */
+ private array $errors = [];
+
+ /**
+ * Main constructor
+ *
+ * @param Db $zdb Database instance
+ * @param ArrayObject<string,int|string>|int|null $args Arguments
+ */
+ public function __construct(Db $zdb, ArrayObject|int $args = null)
+ {
+ $this->zdb = $zdb;
+ $now = new DateTime();
+ $this->creation_date = $now->format('Y-m-d');
+ $this->scheduled_date = $now->format('Y-m-d');
+
+ $this->setFields();
+
+ if (is_int($args)) {
+ $this->load($args);
+ } elseif ($args instanceof ArrayObject) {
+ $this->loadFromRs($args);
+ }
+ }
+
+ /**
+ * Load a scheduled payment from its identifier
+ *
+ * @param integer $id Identifier
+ *
+ * @return bool
+ */
+ public function load(int $id): bool
+ {
+ try {
+ $select = $this->zdb->select(self::TABLE);
+ $select->limit(1)->where([self::PK => $id]);
+
+ $results = $this->zdb->execute($select);
+ $rs = $results->current();
+
+ if (!$rs) {
+ return false;
+ }
+ $this->loadFromRs($rs);
+ return true;
+ } catch (Throwable $e) {
+ Analog::log(
+ 'An error occurred loading scheduled payment #' . $id . "Message:\n" .
+ $e->getMessage(),
+ Analog::ERROR
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * Load scheduled payment from a db ResultSet
+ *
+ * @param ArrayObject<string, int|string> $rs ResultSet
+ *
+ * @return void
+ */
+ private function loadFromRs(ArrayObject $rs): void
+ {
+ global $login;
+
+ $pk = self::PK;
+ $this->id = $rs->$pk;
+ $this->contribution = new Contribution($this->zdb, $login, $rs->{Contribution::PK});
+ $this->payment_type = new PaymentType($this->zdb, $rs->id_paymenttype);
+ $this->creation_date = $rs->creation_date;
+ $this->scheduled_date = $rs->scheduled_date;
+ $this->amount = $rs->amount;
+ $this->is_paid = (bool)$rs->paid;
+ $this->comment = $rs->comment;
+ }
+
+ /**
+ * Check data
+ *
+ * @param array<string,mixed> $data Data
+ *
+ * @return boolean
+ */
+ public function check(array $data): bool
+ {
+ global $login;
+
+ $this->errors = [];
+ $this->contribution = new Contribution($this->zdb, $login);
+
+ if (!isset($data[Contribution::PK]) || !is_numeric($data[Contribution::PK])) {
+ $this->errors[] = _T('Contribution is required');
+ } else {
+ if (!$this->contribution->load($data[Contribution::PK])) {
+ $this->errors[] = _T('Unable to load contribution');
+ } else {
+ if (isset($data['amount'])) {
+ //Amount is not required (will defaults to contribution amount)
+ if (!is_numeric($data['amount']) || $data['amount'] <= 0) {
+ $this->errors[] = _T('Amount must be a positive number');
+ } else {
+ $not_allocated = $this->contribution->amount - $this->getAllocation($this->contribution->id);
+ if (isset($this->id)) {
+ $not_allocated += $this->amount;
+ }
+ if ($data['amount'] > $not_allocated) {
+ $this->errors[] = _T('Amount cannot be greater than non allocated amount');
+ }
+ }
+ }
+ if ($this->contribution->payment_type !== PaymentType::SCHEDULED) {
+ $this->errors[] = _T('Payment type for contribution must be set to scheduled');
+ }
+ }
+ }
+
+ if (!isset($data['id_paymenttype']) || !is_numeric($data['id_paymenttype'])) {
+ $this->errors[] = _T('Payment type is required');
+ } else {
+ //no schedule inception allowed!
+ if ($data['id_paymenttype'] === PaymentType::SCHEDULED) {
+ $this->errors[] = _T('Cannot schedule a scheduled payment!');
+ } else {
+ $this->payment_type = new PaymentType($this->zdb, $data['id_paymenttype']);
+ }
+ }
+
+ if (!isset($data['scheduled_date'])) {
+ $this->errors[] = _T('Scheduled date is required');
+ }
+
+ if (count($this->errors) > 0) {
+ return false;
+ }
+
+ $this
+ ->setContribution($data[Contribution::PK])
+ ->setPaymentType($data['id_paymenttype'])
+ ->setCreationDate($data['creation_date'] ?? date('Y-m-d'))
+ ->setScheduledDate($data['scheduled_date'])
+ ->setAmount($data['amount'] ?? $this->contribution->amount)
+ ->setPaid($data['is_paid'] ?? false)
+ ->setComment($data['comment'] ?? null);
+
+ return count($this->errors) === 0;
+ }
+
+ /**
+ * Store scheduled payment in database
+ *
+ * @return boolean
+ */
+ public function store(): bool
+ {
+ $data = array(
+ Contribution::PK => $this->contribution->id,
+ 'id_paymenttype' => $this->payment_type->id,
+ 'scheduled_date' => $this->scheduled_date,
+ 'amount' => $this->amount,
+ 'paid' => ($this->is_paid ? true : ($this->zdb->isPostgres() ? 'false' : 0)),
+ 'comment' => $this->comment
+ );
+ try {
+ if (isset($this->id) && $this->id > 0) {
+ $update = $this->zdb->update(self::TABLE);
+ $update->set($data)->where([self::PK => $this->id]);
+ $this->zdb->execute($update);
+ } else {
+ $data['creation_date'] = $this->creation_date;
+ $insert = $this->zdb->insert(self::TABLE);
+ $insert->values($data);
+ $add = $this->zdb->execute($insert);
+ if (!$add->count() > 0) {
+ Analog::log('Not stored!', Analog::ERROR);
+ return false;
+ }
+
+ $this->id = $this->zdb->getLastGeneratedValue($this);
+ }
+ return true;
+ } catch (Throwable $e) {
+ Analog::log(
+ 'An error occurred storing shceduled payment: ' . $e->getMessage() .
+ "\n" . print_r($data, true),
+ Analog::ERROR
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * Remove current
+ *
+ * @return boolean
+ */
+ public function remove(): bool
+ {
+ $id = $this->id;
+
+ try {
+ $delete = $this->zdb->delete(self::TABLE);
+ $delete->where([self::PK => $id]);
+ $this->zdb->execute($delete);
+ Analog::log(
+ 'Scheduled Payment #' . $id . ' deleted successfully.',
+ Analog::INFO
+ );
+ return true;
+ } catch (Throwable $e) {
+ Analog::log(
+ 'Unable to delete scheduled payment ' . $id . ' | ' . $e->getMessage(),
+ Analog::ERROR
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * Get identifier
+ *
+ * @return ?int
+ */
+ public function getId(): ?int
+ {
+ return $this->id ?? null;
+ }
+
+ /**
+ * Get contribution
+ *
+ * @return Contribution
+ */
+ public function getContribution(): Contribution
+ {
+ return $this->contribution;
+ }
+
+ /**
+ * Set contribution
+ *
+ * @param int|Contribution $contribution Contribution instance or id
+ *
+ * @return self
+ */
+ public function setContribution(int|Contribution $contribution): self
+ {
+ if (is_int($contribution)) {
+ global $login;
+ try {
+ $contrib = new Contribution($this->zdb, $login);
+ if ($contrib->load($contribution)) {
+ $this->contribution = $contrib;
+ } else {
+ throw new \RuntimeException('Cannot load contribution #' . $contribution);
+ }
+ } catch (Throwable $e) {
+ Analog::log(
+ 'Unable to load contribution #' . $contribution . ' | ' . $e->getMessage(),
+ Analog::ERROR
+ );
+ $this->errors[] = _T('Unable to load contribution');
+ }
+ } else {
+ $this->contribution = $contribution;
+ }
+ return $this;
+ }
+
+ /**
+ * Get payment type
+ *
+ * @return PaymentType
+ */
+ public function getPaymentType(): PaymentType
+ {
+ global $preferences;
+
+ return $this->payment_type ?? new PaymentType($this->zdb, $preferences->pref_default_paymenttype);
+ }
+
+ /**
+ * Set payment type
+ *
+ * @param int|PaymentType $payment_type Payment type instance or id
+ *
+ * @return self
+ */
+ public function setPaymentType(int|PaymentType $payment_type): self
+ {
+ if (is_int($payment_type)) {
+ try {
+ $ptype = new PaymentType($this->zdb);
+ if ($ptype->load($payment_type)) {
+ $this->payment_type = $ptype;
+ } else {
+ throw new \RuntimeException('Cannot load payment type #' . $payment_type);
+ }
+ } catch (Throwable $e) {
+ Analog::log(
+ 'Unable to load payment type #' . $payment_type . ' | ' . $e->getMessage(),
+ Analog::ERROR
+ );
+ $this->errors[] = _T('Unable to load payment type');
+ }
+ } else {
+ $this->payment_type = $payment_type;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get creation date
+ *
+ * @param bool $formatted Get formatted date, or DateTime object
+ *
+ * @return string|DateTime|null
+ */
+ public function getCreationDate(bool $formatted = true): string|DateTime|null
+ {
+ return $this->getDate('creation_date', $formatted);
+ }
+
+ /**
+ * Set creation date
+ *
+ * @param string $creation_date Creation date
+ *
+ * @return self
+ */
+ public function setCreationDate(string $creation_date): self
+ {
+ $this->setDate('creation_date', $creation_date);
+ return $this;
+ }
+
+ /**
+ * Get scheduled date
+ *
+ * @param bool $formatted Get formatted date, or DateTime object
+ *
+ * @return string|DateTime|null
+ */
+ public function getScheduledDate(bool $formatted = true): string|DateTime|null
+ {
+ return $this->getDate('scheduled_date', $formatted);
+ }
+
+ /**
+ * Set scheduled date
+ *
+ * @param string $scheduled_date Scheduled date
+ *
+ * @return self
+ */
+ public function setScheduledDate(string $scheduled_date): self
+ {
+ $this->setDate('scheduled_date', $scheduled_date);
+ return $this;
+ }
+
+ /**
+ * Get amount
+ *
+ * @return float
+ */
+ public function getAmount(): ?float
+ {
+ return $this->amount ?? null;
+ }
+
+ /**
+ * Set amount
+ *
+ * @param float $amount Amount
+ *
+ * @return self
+ */
+ public function setAmount(float $amount): self
+ {
+ $this->amount = $amount;
+ return $this;
+ }
+
+ /**
+ * Is payment done?
+ *
+ * @return bool
+ */
+ public function isPaid(): bool
+ {
+ return $this->is_paid;
+ }
+
+ /**
+ * Set paid
+ *
+ * @param bool $is_paid Paid status
+ *
+ * @return self
+ */
+ public function setPaid(bool $is_paid = true): self
+ {
+ $this->is_paid = $is_paid;
+ return $this;
+ }
+
+ /**
+ * Get comment
+ *
+ * @return string
+ */
+ public function getComment(): ?string
+ {
+ return $this->comment;
+ }
+
+ /**
+ * Set comment
+ *
+ * @param ?string $comment Comment
+ *
+ * @return self
+ */
+ public function setComment(?string $comment): self
+ {
+ $this->comment = $comment;
+ return $this;
+ }
+
+ /**
+ * Is a contribution handled from a scheduled payment?
+ *
+ * @param int $id_cotis Contribution identifier
+ *
+ * @return bool
+ * @throws Throwable
+ */
+ public function isContributionHandled(int $id_cotis): bool
+ {
+ $select = $this->zdb->select(self::TABLE);
+ $select->limit(1)->where([Contribution::PK => $id_cotis]);
+
+ $results = $this->zdb->execute($select);
+ return ($results->count() > 0);
+ }
+
+ /**
+ * Get allocated amount
+ *
+ * @param int $id_cotis Contribution identifier
+ *
+ * @return float
+ * @throws Throwable
+ */
+ public function getAllocation(int $id_cotis): float
+ {
+ $select = $this->zdb->select(self::TABLE);
+ $select->columns(['allocation' => new Expression('SUM(amount)')]);
+ $select->where([Contribution::PK => $id_cotis]);
+
+ $results = $this->zdb->execute($select);
+ $result = $results->current();
+ return $result->allocation ?? 0;
+ }
+
+ /**
+ * Get allocated amount for current contribution
+ *
+ * @return float
+ * @throws Throwable
+ */
+ public function getAllocated(): float
+ {
+ return $this->getAllocation($this->contribution->id);
+ }
+
+ /**
+ * Get missing amount
+ *
+ * @return float
+ */
+ public function getMissingAmount(): float
+ {
+ return $this->contribution->amount - $this->getAllocated();
+ }
+
+ /**
+ * Is scheduled payment fully allocated?
+ *
+ * @param Contribution $contrib Contribution
+ *
+ * @return bool
+ */
+ public function isFullyAllocated(Contribution $contrib): bool
+ {
+ return !($this->getAllocation($contrib->id) < $contrib->amount);
+ }
+
+ /**
+ * Get not fully allocated scheduled payments
+ *
+ * @return Contribution[]
+ */
+ public function getNotFullyAllocated(): array
+ {
+ $select = $this->zdb->select(Contribution::TABLE, 'c');
+ $select->columns([Contribution::PK, 'montant_cotis']);
+ $select->quantifier('DISTINCT');
+
+ $select->join(
+ array('s' => PREFIX_DB . self::TABLE),
+ //$on,
+ 'c.' . Contribution::PK . '=s.' . Contribution::PK,
+ array('allocated' => new Expression('SUM(s.amount)')),
+ $select::JOIN_LEFT
+ );
+
+ $select->group('c.' . Contribution::PK);
+ $select->where(['c.type_paiement_cotis' => PaymentType::SCHEDULED]);
+ $select->having([
+ new PredicateSet(
+ array(
+ new Operator(
+ /** @phpstan-ignore-next-line */
+ new \Laminas\Db\Sql\Predicate\Expression('SUM(s.amount)'),
+ '<',
+ new \Laminas\Db\Sql\Predicate\Expression('c.montant_cotis')
+ ),
+ /** @phpstan-ignore-next-line */
+ new IsNull(new \Laminas\Db\Sql\Predicate\Expression('SUM(s.amount)'))
+ ),
+ PredicateSet::OP_OR
+ )
+ ]);
+
+ $results = $this->zdb->execute($select);
+
+ return $results->toArray();
+ }
+
+ /**
+ * Get errors
+ *
+ * @return string[]
+ */
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+
+ /**
+ * Set fields, must populate $this->fields
+ *
+ * @return self
+ */
+ protected function setFields(): self
+ {
+ $this->fields = array(
+ self::PK => array(
+ 'label' => _T('Scheduled payment ID'), //not a field in the form
+ 'propname' => 'id'
+ ),
+ Contribution::PK => array(
+ 'label' => _T('Contribution ID'), //not a field in the form
+ 'propname' => 'contribution'
+ ),
+ 'id_paymenttype' => array(
+ 'label' => _T('Payment type'),
+ 'propname' => 'payment_type'
+ ),
+ 'creation_date' => array(
+ 'label' => _T('Creation date'),
+ 'propname' => 'creation_date'
+ ),
+ 'scheduled_date' => array(
+ 'label' => _T('Scheduled date'),
+ 'propname' => 'scheduled_date'
+ ),
+ 'amount' => array(
+ 'label' => _T('Amount'),
+ 'propname' => 'amount'
+ ),
+ 'paid' => array(
+ 'label' => _T('Paid'),
+ 'propname' => 'is_paid'
+ ),
+ 'comment' => array(
+ 'label' => _T('Comment'),
+ 'propname' => 'comment'
+ )
+ );
+
+ return $this;
+ }
+}
*/
abstract protected function setFields(): self;
+ /**
+ * Get fields
+ *
+ * @return array<string, array<string, string>>
+ */
+ public function getFields(): array
+ {
+ return $this->fields;
+ }
+
/**
* Global isset method
* Required for twig to access properties via __get
--- /dev/null
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\Filters;
+
+use Throwable;
+use Analog\Analog;
+use Galette\Core\Pagination;
+
+/**
+ * Contributions lists filters and paginator
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ *
+ * @property ?string $start_date_filter
+ * @property ?string $end_date_filter
+ * @property integer $date_field
+ * @property ?integer $payment_type_filter
+ * @property integer|false $from_contribution
+ * @property string $rstart_date_filter
+ * @property string $rend_date_filter
+ * @property array $selected
+ */
+
+class ScheduledPaymentsList extends Pagination
+{
+ public const ORDERBY_DATE = 0;
+ public const ORDERBY_MEMBER = 1;
+ public const ORDERBY_SCHEDULED_DATE = 2;
+ public const ORDERBY_CONTRIBUTION = 3;
+ public const ORDERBY_AMOUNT = 5;
+ public const ORDERBY_PAYMENT_TYPE = 7;
+ public const ORDERBY_ID = 8;
+
+ public const DATE_RECORD = 0;
+
+ public const DATE_SCHEDULED = 1;
+
+ //filters
+ private ?int $date_field = null;
+ private ?string $start_date_filter = null;
+ private ?string $end_date_filter = null;
+ private ?int $payment_type_filter = null;
+ private int|false $from_contribution = false;
+
+
+ /** @var array<int> */
+ private array $selected = [];
+
+ /** @var array<string> */
+ protected array $list_fields = array(
+ 'start_date_filter',
+ 'end_date_filter',
+ 'date_field',
+ 'payment_type_filter',
+ 'from_contribution',
+ 'selected'
+ );
+
+ /** @var array<string> */
+ protected array $virtuals_list_fields = array(
+ 'rstart_date_filter',
+ 'rend_date_filter'
+ );
+
+ /**
+ * Default constructor
+ */
+ public function __construct()
+ {
+ $this->reinit();
+ }
+
+ /**
+ * Returns the field we want to default set order to
+ *
+ * @return int|string
+ */
+ protected function getDefaultOrder(): int|string
+ {
+ return self::ORDERBY_SCHEDULED_DATE;
+ }
+
+ /**
+ * Reinit default parameters
+ *
+ * @param boolean $ajax Called form an ajax query
+ *
+ * @return void
+ */
+ public function reinit(bool $ajax = false): void
+ {
+ parent::reinit();
+ $this->date_field = self::DATE_SCHEDULED;
+ $this->start_date_filter = null;
+ $this->end_date_filter = null;
+ $this->payment_type_filter = null;
+ $this->selected = [];
+
+ if ($ajax === false) {
+ $this->from_contribution = false;
+ }
+ }
+
+ /**
+ * Global getter method
+ *
+ * @param string $name name of the property we want to retrieve
+ *
+ * @return mixed the called property
+ */
+ public function __get(string $name)
+ {
+ if (in_array($name, $this->pagination_fields)) {
+ return parent::__get($name);
+ } else {
+ if (in_array($name, $this->list_fields) || in_array($name, $this->virtuals_list_fields)) {
+ switch ($name) {
+ case 'start_date_filter':
+ case 'end_date_filter':
+ if ($this->$name === null) {
+ return $this->$name;
+ }
+ try {
+ $d = \DateTime::createFromFormat(__("Y-m-d"), $this->$name);
+ if ($d === false) {
+ //try with non localized date
+ $d = \DateTime::createFromFormat("Y-m-d", $this->$name);
+ if ($d === false) {
+ throw new \Exception('Incorrect format');
+ }
+ }
+ return $d->format(__("Y-m-d"));
+ } catch (Throwable $e) {
+ //oops, we've got a bad date :/
+ Analog::log(
+ 'Bad date (' . $this->$name . ') | ' .
+ $e->getMessage(),
+ Analog::INFO
+ );
+ return $this->$name;
+ }
+ case 'rstart_date_filter':
+ case 'rend_date_filter':
+ //same as above, but raw format
+ $rname = substr($name, 1);
+ return $this->$rname;
+ default:
+ return $this->$name;
+ }
+ } else {
+ Analog::log(
+ '[ScheduledPaymentsList] Unable to get property `' . $name . '`',
+ Analog::WARNING
+ );
+ }
+ }
+ }
+
+ /**
+ * Global isset method
+ * Required for twig to access properties via __get
+ *
+ * @param string $name name of the property we want to retrieve
+ *
+ * @return bool
+ */
+ public function __isset(string $name): bool
+ {
+ if (in_array($name, $this->pagination_fields)) {
+ return true;
+ } elseif (in_array($name, $this->list_fields) || in_array($name, $this->virtuals_list_fields)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Global setter method
+ *
+ * @param string $name name of the property we want to assign a value to
+ * @param mixed $value a relevant value for the property
+ *
+ * @return void
+ */
+ public function __set(string $name, $value): void
+ {
+ if (in_array($name, $this->pagination_fields)) {
+ parent::__set($name, $value);
+ } else {
+ switch ($name) {
+ case 'start_date_filter':
+ case 'end_date_filter':
+ try {
+ if ($value !== '') {
+ $y = \DateTime::createFromFormat(__("Y"), $value);
+ if ($y !== false) {
+ $month = 1;
+ $day = 1;
+ if ($name === 'end_date_filter') {
+ $month = 12;
+ $day = 31;
+ }
+ $y->setDate(
+ (int)$y->format('Y'),
+ $month,
+ $day
+ );
+ $this->$name = $y->format('Y-m-d');
+ }
+
+ $ym = \DateTime::createFromFormat(__("Y-m"), $value);
+ if ($y === false && $ym !== false) {
+ $day = 1;
+ if ($name === 'end_date_filter') {
+ $day = (int)$ym->format('t');
+ }
+ $ym->setDate(
+ (int)$ym->format('Y'),
+ (int)$ym->format('m'),
+ $day
+ );
+ $this->$name = $ym->format('Y-m-d');
+ }
+
+ $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
+ if ($y === false && $ym === false && $d !== false) {
+ $this->$name = $d->format('Y-m-d');
+ }
+
+ if ($y === false && $ym === false && $d === false) {
+ $formats = array(
+ __("Y"),
+ __("Y-m"),
+ __("Y-m-d"),
+ );
+
+ $field = null;
+ if ($name === 'start_date_filter') {
+ $field = _T("start date filter");
+ }
+ if ($name === 'end_date_filter') {
+ $field = _T("end date filter");
+ }
+
+ throw new \Exception(
+ sprintf(
+ //TRANS: %1$s is field name, %2$s is list of known date formats
+ _T('Unknown date format for %1$s.<br/>Know formats are: %2$s'),
+ $field,
+ implode(', ', $formats)
+ )
+ );
+ }
+ } else {
+ $this->$name = null;
+ }
+ } catch (Throwable $e) {
+ Analog::log(
+ 'Wrong date format. field: ' . $name .
+ ', value: ' . $value . ', expected fmt: ' .
+ __("Y-m-d") . ' | ' . $e->getMessage(),
+ Analog::INFO
+ );
+ throw $e;
+ }
+ break;
+ default:
+ $this->$name = $value;
+ break;
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\IO;
+
+use ArrayObject;
+use DateTime;
+use Galette\Core\Db;
+use Galette\Core\Login;
+use Galette\Entity\Adherent;
+use Galette\Entity\ScheduledPayment;
+use Galette\Filters\ScheduledPaymentsList;
+use Galette\Repository\PaymentTypes;
+use Galette\Repository\ScheduledPayments;
+
+/**
+ * Contributions CSV exports
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ */
+
+class ScheduledPaymentsCsv extends CsvOut
+{
+ private string $filename;
+ private string $path;
+ private Db $zdb;
+ private Login $login;
+
+ /**
+ * Default constructor
+ *
+ * @param Db $zdb Db instance
+ * @param Login $login Login instance
+ */
+ public function __construct(Db $zdb, Login $login)
+ {
+ $this->filename = 'filtered_shceduledpaymentslist.csv';
+ $this->path = self::DEFAULT_DIRECTORY . $this->filename;
+ $this->zdb = $zdb;
+ $this->login = $login;
+ parent::__construct();
+ }
+
+ /**
+ * Export members CSV
+ *
+ * @param ScheduledPaymentsList $filters Current filters
+ *
+ * @return void
+ */
+ public function exportScheduledPayments(ScheduledPaymentsList $filters)
+ {
+ $scheduled = new ScheduledPayment($this->zdb);
+ $fields = $scheduled->getFields();
+ $labels = array();
+
+ foreach ($fields as $k => $f) {
+ $label = $f['label'];
+ $labels[] = $label;
+ }
+
+ $scheduleds = new ScheduledPayments($this->zdb, $this->login, $filters);
+ $scheduled_list = $scheduleds->getArrayList($filters->selected);
+ $ptypes = PaymentTypes::getAll(false);
+
+ foreach ($scheduled_list as &$scheduled) {
+ /** @var ArrayObject<string, int|string> $scheduled */
+ if (isset($scheduled->id_paymenttype)) {
+ //add textual payment type
+ $scheduled->id_paymenttype = $ptypes[$scheduled->id_paymenttype];
+ }
+
+ //handle dates
+ if (isset($scheduled->date)) {
+ if (
+ $scheduled->date != ''
+ && $scheduled->date != '1901-01-01'
+ ) {
+ $date = new DateTime($scheduled->date);
+ $scheduled->date = $date->format(__("Y-m-d"));
+ } else {
+ $scheduled->date = '';
+ }
+ }
+
+ if (isset($scheduled->scheduled_date)) {
+ if (
+ $scheduled->scheduled_date != ''
+ && $scheduled->scheduled_date != '1901-01-01'
+ ) {
+ $date = new DateTime($scheduled->scheduled_date);
+ $scheduled->scheduled_date = $date->format(__("Y-m-d"));
+ } else {
+ $scheduled->scheduled_date = '';
+ }
+ }
+
+ //member name
+ if (isset($scheduled->{Adherent::PK})) {
+ $scheduled->{Adherent::PK} = Adherent::getSName($this->zdb, $scheduled->{Adherent::PK});
+ }
+ }
+
+ $fp = fopen($this->path, 'w');
+ if ($fp) {
+ $this->export(
+ $scheduled_list,
+ self::DEFAULT_SEPARATOR,
+ self::DEFAULT_QUOTE,
+ $labels,
+ $fp
+ );
+ fclose($fp);
+ }
+ }
+
+ /**
+ * Get file path on disk
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Get file name
+ *
+ * @return string
+ */
+ public function getFileName()
+ {
+ return $this->filename;
+ }
+}
$list = array();
if (is_array($ids)) {
$list = $ids;
- } elseif (is_numeric($ids)) {
- $list = [(int)$ids];
} else {
- //not numeric and not an array: incorrect.
- Analog::log(
- 'Asking to remove contribution, but without providing an array or a single numeric value.',
- Analog::WARNING
- );
- return false;
+ $list = [$ids];
}
try {
break;
}
- //anyways, we want to order by firstname, lastname
+ //anyway, we want to order by firstname, lastname
if ($this->canOrderBy('nom_adh', $fields)) {
$order[] = 'nom_adh ' . $this->filters->getDirection();
}
/**
* Get payments types
*
+ * @param boolean $schedulable Types that can be used in schedules only
+ *
* @return array<int, PaymentType>
*/
- public static function getAll(): array
+ public static function getAll(bool $schedulable = true): array
{
global $zdb, $preferences, $login;
$ptypes = new self($zdb, $preferences, $login);
- return $ptypes->getList();
+ return $ptypes->getList($schedulable);
}
/**
* Get list
*
+ * @param boolean $schedulable Types that can be used in schedules only
+ *
* @return array<int, PaymentType>|ResultSet
*/
- public function getList(): array|ResultSet
+ public function getList(bool $schedulable = true): array|ResultSet
{
try {
$select = $this->zdb->select(PaymentType::TABLE, 'a');
$select->order(PaymentType::PK);
+ if ($schedulable === false) {
+ $select->where->notEqualTo('a.' . PaymentType::PK, PaymentType::SCHEDULED);
+ }
+
$types = array();
$results = $this->zdb->execute($select);
foreach ($results as $row) {
--- /dev/null
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\Repository;
+
+use Galette\Entity\ScheduledPayment;
+use Galette\Filters\ScheduledPaymentsList;
+use Laminas\Db\ResultSet\ResultSet;
+use Throwable;
+use Analog\Analog;
+use Laminas\Db\Sql\Expression;
+use Galette\Core\Db;
+use Galette\Core\Login;
+use Galette\Core\History;
+use Galette\Entity\Contribution;
+use Galette\Entity\Adherent;
+use Laminas\Db\Sql\Select;
+
+/**
+ * Scheduled payments class for galette
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ */
+class ScheduledPayments
+{
+ public const TABLE = ScheduledPayment::TABLE;
+ public const PK = ScheduledPayment::PK;
+
+ private ScheduledPaymentsList $filters;
+ private int $count = 0;
+
+ private Db $zdb;
+ private Login $login;
+ private float $sum = 0;
+ /** @var array<int> */
+ private array $current_selection;
+
+ /**
+ * Default constructor
+ *
+ * @param Db $zdb Database
+ * @param Login $login Login
+ * @param ?ScheduledPaymentsList $filters Filtering
+ */
+ public function __construct(Db $zdb, Login $login, ?ScheduledPaymentsList $filters = null)
+ {
+ $this->zdb = $zdb;
+ $this->login = $login;
+
+ if ($filters === null) {
+ $this->filters = new ScheduledPaymentsList();
+ } else {
+ $this->filters = $filters;
+ }
+ }
+
+ /**
+ * Get scheduled payments list for a specific contribution
+ *
+ * @param int $contrib_id Contribution identifier
+ *
+ * @return ScheduledPayment[]
+ */
+ public function getListFromContribution(int $contrib_id): array
+ {
+ $this->filters->from_contribution = $contrib_id;
+ /** @phpstan-ignore-next-line */
+ return $this->getList(true);
+ }
+
+ /**
+ * Get scheduled payments list for a specific contribution
+ *
+ * @param array<int> $ids an array of members id that has been selected
+ * @param bool $as_object return the results as an array of
+ * @param ?array<string> $fields field(s) name(s) to get. Should be a string or
+ * an array. If null, all fields will be returned
+ *
+ * @return array<int, Contribution>|false
+ */
+ public function getArrayList(array $ids, bool $as_object = false, ?array $fields = null): array|false
+ {
+ if (count($ids) < 1) {
+ Analog::log('No scheduled payment selected.', Analog::INFO);
+ return false;
+ }
+
+ $this->current_selection = $ids;
+ $list = $this->getList($as_object, $fields);
+ $array_list = [];
+ foreach ($list as $entry) {
+ $array_list[] = $entry;
+ }
+ return $array_list;
+ }
+
+ /**
+ * Get scheduled payments list
+ *
+ * @param bool $as_object return the results as an array of
+ * ScheduledPayment object.
+ * @param ?array<string> $fields field(s) name(s) to get. Should be a string or
+ * an array. If null, all fields will be returned
+ *
+ * @return array<int, Contribution>|ResultSet
+ */
+ public function getList(bool $as_object = true, ?array $fields = null): array|ResultSet
+ {
+ try {
+ $select = $this->buildSelect($fields);
+
+ $this->filters->setLimits($select);
+
+ $scheduleds = array();
+ $results = $this->zdb->execute($select);
+ if ($as_object) {
+ foreach ($results as $row) {
+ $scheduleds[] = new ScheduledPayment($this->zdb, $row);
+ }
+ } else {
+ $scheduleds = $results;
+ }
+ return $scheduleds;
+ } catch (Throwable $e) {
+ Analog::log(
+ 'Cannot list scheduled payments | ' . $e->getMessage(),
+ Analog::WARNING
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * Builds the SELECT statement
+ *
+ * @param ?array<string> $fields fields list to retrieve
+ *
+ * @return Select SELECT statement
+ */
+ private function buildSelect(?array $fields): Select
+ {
+ try {
+ $fieldsList = ['*'];
+ if (is_array($fields) && count($fields)) {
+ $fieldsList = $fields;
+ }
+
+ $select = $this->zdb->select(self::TABLE, 's');
+ $select->columns($fieldsList);
+
+ $select->join(
+ array('c' => PREFIX_DB . Contribution::TABLE),
+ 's.' . Contribution::PK . '= c.' . Contribution::PK,
+ array()
+ );
+
+ $select->join(
+ array('a' => PREFIX_DB . Adherent::TABLE),
+ 'c.' . Adherent::PK . '= a.' . Adherent::PK,
+ array()
+ );
+
+ $this->buildWhereClause($select);
+ $select->order(self::buildOrderClause());
+
+ $this->calculateSum($select);
+
+ $this->proceedCount($select);
+
+ return $select;
+ } catch (Throwable $e) {
+ Analog::log(
+ 'Cannot build SELECT clause for scheduled payments | ' . $e->getMessage(),
+ Analog::WARNING
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * Count scheduled payments from the query
+ *
+ * @param Select $select Original select
+ *
+ * @return void
+ */
+ private function proceedCount(Select $select): void
+ {
+ try {
+ $countSelect = clone $select;
+ $countSelect->reset($countSelect::COLUMNS);
+ $countSelect->reset($countSelect::ORDER);
+ $countSelect->columns(
+ array(
+ self::PK => new Expression('COUNT(' . self::PK . ')')
+ )
+ );
+
+ $results = $this->zdb->execute($countSelect);
+ $result = $results->current();
+
+ $k = self::PK;
+ $this->count = $result->$k;
+ $this->filters->setCounter($this->count);
+ } catch (Throwable $e) {
+ Analog::log(
+ 'Cannot count scheduled payments | ' . $e->getMessage(),
+ Analog::WARNING
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * Calculate sum of all selected scheduled payments
+ *
+ * @param Select $select Original select
+ *
+ * @return void
+ */
+ private function calculateSum(Select $select): void
+ {
+ try {
+ $sumSelect = clone $select;
+ $sumSelect->reset($sumSelect::COLUMNS);
+ $sumSelect->reset($sumSelect::ORDER);
+ $sumSelect->columns(
+ array(
+ 'scheduledsum' => new Expression('SUM(amount)')
+ )
+ );
+
+ $results = $this->zdb->execute($sumSelect);
+ $result = $results->current();
+ if ($result->scheduledsum) {
+ $this->sum = round($result->scheduledsum, 2);
+ }
+ } catch (Throwable $e) {
+ Analog::log(
+ 'Cannot calculate scheduled payments sum | ' . $e->getMessage(),
+ Analog::WARNING
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * Builds the order clause
+ *
+ * @return array<string> SQL ORDER clauses
+ */
+ private function buildOrderClause(): array
+ {
+ $order = array();
+
+ switch ($this->filters->orderby) {
+ case ScheduledPaymentsList::ORDERBY_ID:
+ $order[] = ScheduledPayment::PK . ' ' . $this->filters->ordered;
+ break;
+ case ScheduledPaymentsList::ORDERBY_MEMBER:
+ $order[] = 'a.nom_adh ' . $this->filters->getDirection();
+ $order[] = 'a.prenom_adh ' . $this->filters->getDirection();
+ break;
+ case ScheduledPaymentsList::ORDERBY_DATE:
+ $order[] = 'creation_date ' . $this->filters->ordered;
+ break;
+ case ScheduledPaymentsList::ORDERBY_SCHEDULED_DATE:
+ $order[] = 'scheduled_date ' . $this->filters->ordered;
+ break;
+ case ScheduledPaymentsList::ORDERBY_CONTRIBUTION:
+ $order[] = Contribution::PK . ' ' . $this->filters->ordered;
+ break;
+ case ScheduledPaymentsList::ORDERBY_AMOUNT:
+ $order[] = 'amount ' . $this->filters->ordered;
+ break;
+ case ScheduledPaymentsList::ORDERBY_PAYMENT_TYPE:
+ $order[] = 'id_paymenttype ' . $this->filters->ordered;
+ break;
+ default:
+ $order[] = $this->filters->orderby . ' ' . $this->filters->ordered;
+ break;
+ }
+
+ return $order;
+ }
+
+ /**
+ * Builds where clause, for filtering on simple list mode
+ *
+ * @param Select $select Original select
+ *
+ * @return void
+ */
+ private function buildWhereClause(Select $select): void
+ {
+ switch ($this->filters->date_field) {
+ case ScheduledPaymentsList::DATE_RECORD:
+ $field = 'creation_date';
+ break;
+ case ScheduledPaymentsList::DATE_SCHEDULED:
+ default:
+ $field = 'scheduled_date';
+ break;
+ }
+
+ if (isset($this->current_selection)) {
+ $select->where->in('s.' . self::PK, $this->current_selection);
+ }
+
+ try {
+ if ($this->filters->start_date_filter != null) {
+ $d = new \DateTime($this->filters->rstart_date_filter);
+ $select->where->greaterThanOrEqualTo(
+ $field,
+ $d->format('Y-m-d')
+ );
+ }
+
+ if ($this->filters->end_date_filter != null) {
+ $d = new \DateTime($this->filters->rend_date_filter);
+ $select->where->lessThanOrEqualTo(
+ $field,
+ $d->format('Y-m-d')
+ );
+ }
+
+ if ($this->filters->payment_type_filter !== null) {
+ $select->where->equalTo(
+ 'id_paymenttype',
+ $this->filters->payment_type_filter
+ );
+ }
+
+ if ($this->filters->from_contribution !== false) {
+ $select->where->equalTo(
+ 'c.' . Contribution::PK,
+ $this->filters->from_contribution
+ );
+ }
+
+ if (!$this->login->isAdmin() && !$this->login->isStaff()) {
+ $select->where(
+ array(
+ 'a.' . Adherent::PK => $this->login->id
+ )
+ );
+ }
+ } catch (Throwable $e) {
+ Analog::log(
+ __METHOD__ . ' | ' . $e->getMessage(),
+ Analog::WARNING
+ );
+ throw $e;
+ }
+ }
+
+ /**
+ * Get count for current query
+ *
+ * @return int
+ */
+ public function getCount(): int
+ {
+ return $this->count;
+ }
+
+ /**
+ * Get sum
+ *
+ * @return float
+ */
+ public function getSum(): float
+ {
+ return $this->sum;
+ }
+
+ /**
+ * Remove specified scheduled payments
+ *
+ * @param integer|array<int> $ids Scheduled payments identifiers to delete
+ * @param History $hist History
+ * @param boolean $transaction True to begin a database transaction
+ *
+ * @return boolean
+ */
+ public function remove(int|array $ids, History $hist, bool $transaction = true): bool
+ {
+ $list = array();
+ if (is_array($ids)) {
+ $list = $ids;
+ } else {
+ $list = [$ids];
+ }
+
+ try {
+ if ($transaction) {
+ $this->zdb->connection->beginTransaction();
+ }
+ $select = $this->zdb->select(self::TABLE);
+ $select->where->in(self::PK, $list);
+ $scheduleds = $this->zdb->execute($select);
+ foreach ($scheduleds as $scheduled) {
+ $c = new ScheduledPayment($this->zdb, $scheduled);
+ $res = $c->remove();
+ if ($res === false) {
+ throw new \Exception();
+ }
+ }
+ if ($transaction) {
+ $this->zdb->connection->commit();
+ }
+ $hist->add(
+ str_replace(
+ '%list',
+ print_r($list, true),
+ _T("Scheduled payments deleted (%list)")
+ )
+ );
+ return true;
+ } catch (Throwable $e) {
+ if ($transaction) {
+ $this->zdb->connection->rollBack();
+ }
+ Analog::log(
+ 'An error occurred trying to remove scheduled payments | ' .
+ $e->getMessage(),
+ Analog::ERROR
+ );
+ throw $e;
+ }
+ }
+}
* along with Galette. If not, see <http://www.gnu.org/licenses/>.
*/
#}
-{% if show_inline is not defined %}
-<div class="field">
-{% else %}
-<div class="inline field">
-{% endif %}
+<div class="{% if show_inline is defined %}inline {% endif %}field{% if required is defined and required == true %} required{% endif %}">
<label for="{{ varname }}">{% if label is defined %}{{ label }}{% else %}{{ _T("Payment type:") }}{% endif %}</label>
- <select name="{{ varname }}" id="{{ varname }}" class="ui search dropdown">
+ <select
+ name="{{ varname }}"
+ id="{{ varname }}"
+ {% if required is defined and required == true %} required="required"{% endif %}
+ {% if disabled is defined and disabled == true %} disabled="disabled"{% endif %}
+ class="ui search dropdown"
+ >
{% if empty is defined %}
<option value="{{ empty.value }}">{{ empty.label }}</option>
{% endif %}
-{% set ptypes = callstatic('\\Galette\\Repository\\PaymentTypes', 'getAll') %}
+{% if ptypes is not defined %}
+ {% set ptypes = callstatic('\\Galette\\Repository\\PaymentTypes', 'getAll') %}
+{% endif %}
{% for ptype in ptypes %}
<option value="{{ ptype.id }}"{% if current == ptype.id %} selected="selected"{% endif %}>{{ ptype.getName() }}</option>
{% endfor %}
{% endif %}
</th>
{% endfor %}
- {% if mode != 'ajax' and no_action is not defined or no_action == false %}
+ {% if mode != 'ajax' and (no_action is not defined or no_action == false) %}
<th class="actions_row">{{ _T('Actions') }}</th>
{% endif %}
{% endblock %}
<span class="ui teal horizontal label">
{{ _T("Transaction related") }}
</span>
+ {% endif %}
+ {% if contribution.hasSchedule() %}
+ <span class="ui teal horizontal label">
+ {{ _T("Has scheduled payments") }}
+ </span>
{% endif %}
</div>
<div class="active content">
<div class="ui mobile reversed stackable grid">
- <div class="{% if contribution.isTransactionPart() %}five wide {% endif %}column">
+ <div class="{% if contribution.isTransactionPart() %}six wide {% endif %}column">
{% if not require_mass %}
<div class="inline field">
<label for="id_adh">{{ _T("Contributor:") }}</label>
</div>
</div>
</div>
- {% if contribution.isTransactionPart() and contribution.transaction.getMissingAmount() %}
- <div class="inline field">
- <label>{{ _T("Dispatch type:") }}</label>
- <i class="circular inverted primary small icon info tooltip" title="{{ _T("Select a contribution type to create for dispatch transaction") }}" aria-hidden="true"></i>
- <input type="radio" name="contrib_type" id="contrib_type_fee" value="{{ constant('Galette\\Entity\\Contribution::TYPE_FEE') }}" checked="checked"/> <label for="contrib_type_fee">{{ _T("Membership fee") }}</label>
- <input type="radio" name="contrib_type" id="contrib_type_donation" value="donation"/> <label for="contrib_type_donation">{{ _T("Donation") }}</label>
- </div>
- {% endif %}
</div>
- {% if contribution.isTransactionPart() %}
- <div class="eleven wide column">
- {% set mid = contribution.transaction.member %}
- <div class="ui tiny header">{{ _T("Related transaction information") }}</div>
- <table id="transaction_detail" class="listing ui very compact yellow table">
- <thead>
- <tr>
- <th class="listing">#</th>
- <th class="listing">{{ _T("Description") }}</th>
- <th class="listing">{{ _T("Date") }}</th>
- <th class="listing">{{ _T("Member") }}</th>
- <th class="listing">{{ _T("Amount") }}</th>
- <th class="listing">{{ _T("Not dispatched amount") }}</th>
- </tr>
- </thead>
- {% if contribution.id and contribution.isTransactionPart() %}
- <tfoot>
- <tr>
- <td colspan="6">
- <div class="ui basic fitted right aligned segment">
- <a
- href="{{ url_for("editTransaction", {"id": contribution.transaction.id}) }}"
- class="ui icon blue compact button tooltip"
- >
- <i class="eye icon tooltip" aria-hidden="true"></i>
- <span class="ui special popup">{{ _T("View transaction") }}</span>
- </a>
- {% if contribution.transaction.getMissingAmount() > 0 %}
- <a
- href="{{ url_for("addContribution", {"type": constant('Galette\\Entity\\Contribution::TYPE_FEE')}) }}?trans_id={{ contribution.transaction.id }}"
- class="ui icon green compact button tooltip"
- title="{{ _T("Create a new fee that will be attached to the current transaction") }}"
- >
- <i class="plus tiny icon" aria-hidden="true"></i>
- <i class="user check icon" aria-hidden="true"></i>
- <span class="visually-hidden">{{ _T("Create a new fee that will be attached to the current transaction") }}</span>
- </a>
+ {% if contribution.isTransactionPart() or contribution.hasSchedule() %}
+ <div class="ten wide column">
+ {% if contribution.isTransactionPart() %}
+ <div class="ui yellow segment">
+ <div class="ui tiny header">
+ <a
+ href="{{ url_for("editTransaction", {"id": contribution.transaction.id}) }}"
+ class="ui mini icon blue compact button tooltip"
+ >
+ <i class="eye icon tooltip" aria-hidden="true"></i>
+ <span class="ui special popup">{{ _T("View transaction") }}</span>
+ </a>
+ {{ _T("Related transaction information") }}
+ </div>
+ <div class="content">
+ <div class="ui relaxed horizontal list">
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Date") }}</div>
+ {{ contribution.transaction.date }}
+ </div>
+ </div>
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Member") }}</div>
+ {{ memberName({'id': contribution.transaction.member}) }}
+ </div>
+ </div>
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Amount") }}</div>
+ {{ contribution.transaction.amount }}
+ </div>
+ </div>
+ {% if contribution.transaction.getMissingAmount() > 0 %}
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Not dispatched amount") }}</div>
+ {{ contribution.transaction.getMissingAmount() }}
+ {% if contribution.id != '' %}
+ <a
+ href="{{ url_for("addContribution", {"type": constant('Galette\\Entity\\Contribution::TYPE_FEE')}) }}?trans_id={{ contribution.transaction.id }}"
+ class="ui mini icon green compact button tooltip"
+ title="{{ _T("Create a new fee that will be attached to the current transaction") }}"
+ >
+ <i class="plus tiny icon" aria-hidden="true"></i>
+ <i class="user check icon" aria-hidden="true"></i>
+ <span class="visually-hidden">{{ _T("Create a new fee that will be attached to the current transaction") }}</span>
+ </a>
+ <a
+ href="{{ url_for("addContribution", {"type": constant('Galette\\Entity\\Contribution::TYPE_DONATION')}) }}?trans_id={{ contribution.transaction.id }}"
+ class="ui mini icon green compact button tooltip"
+ title="{{ _T("Create a new donation that will be attached to the current transaction") }}"
+ >
+ <i class="plus tiny icon" aria-hidden="true"></i>
+ <i class="gift icon" aria-hidden="true"></i>
+ <span class="visually-hidden">{{ _T("Create a new donation that will be attached to the current transaction") }}</span>
+ </a>
+ {% endif %}
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ {% if contribution.id == '' %}
+ <div class="inline field">
+ <label>{{ _T("Dispatch type:") }}</label>
+ <i class="circular inverted primary small icon info tooltip" title="{{ _T("Select a contribution type to create for dispatch transaction") }}" aria-hidden="true"></i>
+ <input type="radio" name="contrib_type" id="contrib_type_fee" value="{{ constant('Galette\\Entity\\Contribution::TYPE_FEE') }}"{% if type == constant('Galette\\Entity\\Contribution::TYPE_FEE') %} checked="checked"{% endif %}/> <label for="contrib_type_fee">{{ _T("Membership fee") }}</label>
+ <input type="radio" name="contrib_type" id="contrib_type_donation" value="{{ constant('Galette\\Entity\\Contribution::TYPE_DONATION') }}"{% if type == constant('Galette\\Entity\\Contribution::TYPE_DONATION') %} checked="checked"{% endif %}/> <label for="contrib_type_donation">{{ _T("Donation") }}</label>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ {% endif %}
+ {% if contribution.hasSchedule() %}
+ {% set scheduled_amount = scheduled.getAllocation(contribution.id) %}
+ <div class="ui yellow segment">
+ <div class="ui tiny header">
+ <a
+ href="#"
+ class="ui mini icon blue compact button tooltip"
+ id="scheduledslist"
+ >
+ <i class="eye icon tooltip" aria-hidden="true"></i>
+ <span class="ui special popup">{{ _T("View scheduled payments") }}</span>
+ </a>
+ {{ _T("Scheduled payments") }}
+ </div>
+ {% if not contribution.isScheduleFullyAllocated() %}
+ <div class="content">
+ <div class="ui relaxed horizontal list">
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Amount") }}</div>
+ {{ scheduled_amount }}
+ </div>
+ </div>
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Not dispatched amount") }}</div>
+ {{ contribution.amount - scheduled_amount }}
<a
- href="{{ url_for("addContribution", {"type": constant('Galette\\Entity\\Contribution::TYPE_DONATION')}) }}?trans_id={{ contribution.transaction.id }}"
- class="ui icon green compact button tooltip"
- title="{{ _T("Create a new donation that will be attached to the current transaction") }}"
+ href="{{ url_for("addScheduledPayment", {"id_cotis": contribution.id}) }}"
+ class="ui mini icon green compact button tooltip"
+ title="{{ _T("Create a new scheduled payment") }}"
>
<i class="plus tiny icon" aria-hidden="true"></i>
- <i class="gift icon" aria-hidden="true"></i>
- <span class="visually-hidden">{{ _T("Create a new donation that will be attached to the current transaction") }}</span>
+ <i class="money bill wave icon" aria-hidden="true"></i>
+ <span class="visually-hidden">{{ _T("Create a new scheduled payment") }}</span>
</a>
- {% endif %}
</div>
-
- </td>
- </tr>
- </tfoot>
- {% endif %}
- <tbody>
- <tr>
- <td>{{ contribution.transaction.id }}</td>
- <td>{{ contribution.transaction.description }}</td>
- <td>{{ contribution.transaction.date }}</td>
- <td>{{ memberName({'id': mid}) }}</td>
- <td class="right">{{ contribution.transaction.amount }}</td>
- <td class="right">{{ contribution.transaction.getMissingAmount() }}</td>
- </tr>
- </tbody>
- </table>
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ {% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% include 'components/forms/payment_types.html.twig' with {
'current': ptype,
- 'varname': 'type_paiement_cotis'
+ 'varname': 'type_paiement_cotis',
+ 'disabled': contribution.hasSchedule()
} %}
</div>
<div class="{% if type == constant('Galette\\Entity\\Contribution::TYPE_FEE') %}three{% else %}two{% endif %} fields">
{% endif %}
$(function() {
- {% if not contribution.id -%}
+ {% if not contribution.id -%}
var _types_amounts = {
{%- for key, values in type_cotis_options -%}
{%- if values.amount > 0 -%}
}
});
{% endif %}
+
+ {% if contribution.hasSchedule() %}
+ {# Contributions popup #}
+ var _btnuser_mapping = function(){
+ $('#scheduledslist').click(function(){
+ $.ajax({
+ url: '{{ url_for("scheduledPayments") }}',
+ type: "GET",
+ data: {
+ ajax: true,
+ id_cotis: '{{ contribution.id }}'
+ },
+ {% include "elements/js/loader.js.twig" with {
+ selector: '#scheduledslist',
+ loader: 'button'
+ } %},
+ success: function(res){
+ _contribs_dialog(res, '{{ contribution.id }}');
+ },
+ error: function() {
+ {% include "elements/js/modal.js.twig" with {
+ modal_title_twig: _T("An error occurred displaying scheduled payments :(")|e("js"),
+ modal_without_content: true,
+ modal_class: "mini",
+ modal_deny_only: true,
+ modal_cancel_text: _T("Close")|e("js"),
+ modal_classname: "redalert",
+ } %}
+ }
+ });
+ });
+ }
+ _btnuser_mapping();
+
+ var _contribs_dialog = function(res, id_cotis){
+ {% include "elements/js/modal.js.twig" with {
+ modal_title_twig: _T("Scheduled payments")|e("js"),
+ modal_content: "res",
+ modal_class: "scheduledpayments fullscreen",
+ modal_content_class: "scrolling",
+ modal_deny_only: true,
+ modal_cancel_text: _T("Close")|e('js')
+ } %}
+ _contribs_ajax_mapper(res, id_cotis);
+ }
+
+ var _contribs_ajax_mapper = function(res, id_cotis){
+ /*$('.scheduledpayments .filter.icon').remove();
+ $('.scheduledpayments .infoline .button').remove();
+ $('.scheduledpayments .contribution_row input[type=checkbox]').hide();
+
+ //Initialize Fomantic components
+ $('.scheduledpayments .dropdown').dropdown();
+ {% include "elements/js/calendar.js.twig" %}
+
+ //Deactivate contributions list links
+ $('.scheduledpayments tbody a').click(function(){
+ //for links in body (members links), we de nothing
+ return false;
+ });
+
+ //Use JS to send forms
+ $('.scheduledpayments form').on('submit', function(){
+ var _form = $(this);
+ $.ajax({
+ url: _form.attr('action'),
+ type: "POST",
+ data: _form.serialize(),
+ {% include "elements/js/loader.js.twig" with {
+ selector: '.scheduledpayments'
+ } %},
+ success: function(res){
+ $('#main-container').remove();
+ $('.scheduledpayments .content').append(res);
+ _contribs_ajax_mapper(res, max_amount);
+ },
+ error: function() {
+ {% include "elements/js/modal.js.twig" with {
+ modal_title_twig: _T("An error occurred displaying contributions :(")|e("js"),
+ modal_without_content: true,
+ modal_class: "mini",
+ modal_deny_only: true,
+ modal_cancel_text: _T("Close")|e("js"),
+ modal_classname: "redalert",
+ } %}
+ }
+ });
+ return false;
+ });
+
+ _bindDropdownsAutosubmit();*/
+
+ //Bind pagination and ordering links
+ /*$('.scheduledpayments .pagination a, .scheduledpayments thead a').click(function() {
+ $.ajax({
+ url: this.href,
+ type: "GET",
+ data: {
+ ajax: true,
+ max_amount: max_amount
+ },
+ {% include "elements/js/loader.js.twig" with {
+ selector: '.scheduledpayments'
+ } %},
+ success: function(res){
+ $('#main-container').remove();
+ $('.scheduledpayments .content').append(res);
+ _contribs_ajax_mapper(res, max_amount);
+ },
+ error: function() {
+ {% include "elements/js/modal.js.twig" with {
+ modal_title_twig: _T("An error occurred displaying contributions :(")|e("js"),
+ modal_without_content: true,
+ modal_class: "mini",
+ modal_deny_only: true,
+ modal_cancel_text: _T("Close")|e("js"),
+ modal_classname: "redalert",
+ } %}
+ },
+ });
+ return false;
+ });*/
+
+ //Bind reset filters button
+ /*$('#clear_filter').click(function(event) {
+ var _this = $(this);
+ _this.closest('form').submit(function(event) {
+ var _form = $(this);
+ $.ajax({
+ url: _form.attr('action'),
+ type: "POST",
+ data: {
+ clear_filter: true
+ },
+ {% include "elements/js/loader.js.twig" with {
+ selector: '.scheduledpayments'
+ } %},
+ success: function(res){
+ $('#main-container').remove();
+ $('.scheduledpayments .content').append(res);
+ _contribs_ajax_mapper(res, max_amount);
+ },
+ error: function() {
+ {% include "elements/js/modal.js.twig" with {
+ modal_title_twig: _T("An error occurred displaying contributions :(")|e("js"),
+ modal_without_content: true,
+ modal_class: "mini",
+ modal_deny_only: true,
+ modal_cancel_text: _T("Close")|e("js"),
+ modal_classname: "redalert",
+ } %}
+ }
+ });
+ });
+ });*/
+ }
+ {% endif %}
});
</script>
{% endblock %}
{% include "components/forms/payment_types.html.twig" with {
current: filters.payment_type_filter,
varname: "payment_type_filter",
- show_inline: "",
classname: "",
empty: {'value': -1, 'label': _T("Select")}
} %}
{% block footer %}
{% if nb != 0 %}
<tr>
- <th class="right" colspan="{% if (login.isAdmin() or login.isStaff()) and member is not defined %}10{% else %}9{% endif %}">
+ <th class="right aligned" colspan="{% if (login.isAdmin() or login.isStaff()) and member is not defined %}10{% else %}9{% endif %}">
{{ _T("Found contributions total %f")|replace({"%f": contribs.getSum()}) }}
</th>
</tr>
--- /dev/null
+{#
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+#}
+{% extends (mode == 'ajax') ? "ajax.html.twig" : "page.html.twig" %}
+
+{% block content %}
+ <form action="{% if scheduled.getId() is null %}{{ url_for('doAddScheduledPayment', {'id_cotis': scheduled.getContribution().id}) }}{% else %}{{ url_for('doEditScheduledPayment', {'id': scheduled.getId()}) }}{% endif %}" method="post" class="ui form">
+ <div class="ui styled fluid accordion field">
+ <div class="active title">
+ <i class="jsonly displaynone icon dropdown" aria-hidden="true"></i>
+ {{ _T("Scheduled payment") }}
+ </div>
+ <div class="active content">
+ <div class="ui mobile reversed stackable grid">
+ <div class="six wide column">
+ <div class="inline field required">
+ <label for="montant_cotis">{{ _T("Amount") }}</label>
+ <input type="text" name="amount" id="amount" value="{% if scheduled.getId() %}{{ scheduled.getAmount() }}{% else %}{{ scheduled.getMissingAmount() }}{% endif %}" maxlength="10" required="required"/>
+ </div>
+ {# payment type #}
+ {% set ptype = scheduled.getPaymentType().id %}
+ {% if ptype == null %}
+ {% set ptype = preferences.pref_default_paymenttype %}
+ {% endif %}
+
+ {% set ptypes = callstatic('\\Galette\\Repository\\PaymentTypes', 'getAll', false) %}
+ {% include 'components/forms/payment_types.html.twig' with {
+ 'current': ptype,
+ 'varname': 'id_paymenttype',
+ 'show_inline': true,
+ 'component_class': 'inline field',
+ 'required': true
+ } %}
+ <div class="inline field">
+ <label for="paid">{{ _T('Paid') }}</label>
+ <input type="checkbox" name="paid" id="paid" value="1"{% if scheduled.isPaid() %} checked="checked"{% endif %}/>
+ </div>
+ </div>
+ <div class="ten wide column">
+ {% set contribution = scheduled.getContribution() %}
+ <div class="ui yellow segment">
+ <div class="ui tiny header">
+ <a
+ href="{{ url_for("editContribution", {"type": contribution.isFee() ? constant('Galette\\Entity\\Contribution::TYPE_FEE') : constant('Galette\\Entity\\Contribution::TYPE_DONATION'), "id": contribution.id}) }}"
+ class="ui mini icon blue compact button tooltip"
+ >
+ <i class="eye icon tooltip" aria-hidden="true"></i>
+ <span class="ui special popup">{{ _T("View contribution") }}</span>
+ </a>
+ {{ _T("Related contribution information") }}
+ </div>
+ <div class="content">
+ <div class="ui relaxed horizontal list">
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Type") }}</div>
+ {{ contribution.type.libelle }}
+ </div>
+ </div>
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Begin") }}</div>
+ {{ contribution.begin_date }}
+ </div>
+ </div>
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("End") }}</div>
+ {{ contribution.end_date }}
+ </div>
+ </div>
+ {% if ((login.isAdmin() or login.isStaff()) and member is not defined) or pmember is defined %}
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Member") }}</div>
+ {{ memberName({'id': contribution.member}) }}
+ </div>
+ </div>
+ {% endif %}
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Amount") }}</div>
+ {{ contribution.amount }}
+ </div>
+ </div>
+ {% if not contribution.isScheduleFullyAllocated() %}
+ <div class="item">
+ <div class="content">
+ <div class="header">{{ _T("Not dispatched amount") }}</div>
+ {{ scheduled.getMissingAmount() }}
+ {% if scheduled.getId() != '' %}
+ <a
+ href="{{ url_for("addScheduledPayment", {"id_cotis": contribution.id}) }}"
+ class="ui mini icon green compact button tooltip"
+ title="{{ _T("Create a new scheduled payment") }}"
+ >
+ <i class="plus tiny icon" aria-hidden="true"></i>
+ <i class="money bill wave icon" aria-hidden="true"></i>
+ <span class="visually-hidden">{{ _T("Create a new scheduled payment") }}</span>
+ </a>
+ {% endif %}
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="sixteen wide column">
+ <div class="two fields">
+ <div class="inline field required">
+ <label for="creation_date">
+ {{ _T("Record date") }}
+ </label>
+ <div class="ui calendar" id="scheduled-creationdate">
+ <div class="ui input left icon">
+ <i class="calendar icon" aria-hidden="true"></i>
+ <input type="text" name="creation_date" id="creation_date" value="{{ scheduled.getCreationDate() }}" maxlength="10" required="required" placeholder="{{ _T('yyyy-mm-dd format') }}" />
+ </div>
+ </div>
+ </div>
+ <div class="inline field required">
+ <label for="scheduled_date">{{ _T("Scheduled date") }}</label>
+ <div class="ui calendar" id="scheduled-scheduleddate">
+ <div class="ui input left icon">
+ <i class="calendar icon" aria-hidden="true"></i>
+ <input type="text" name="scheduled_date" id="scheduled_date" value="{{ scheduled.getScheduledDate() }}" maxlength="10" required="required" placeholder="{{ _T('yyyy-mm-dd format') }}" />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="field">
+ <label for="comment">{{ _T("Comments") }}</label>
+ <textarea name="comment" id="comment" cols="61" rows="6">{{ scheduled.getComment() }}</textarea>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {% if mode != 'ajax' %}
+ <div class="ui basic center aligned segment">
+ <button type="submit" class="ui labeled icon primary button action">
+ <i class="save icon" aria-hidden="true"></i> {{ _T("Save") }}
+ </button>
+ <input type="submit" name="cancel" value="{{ _T("Cancel") }}" class="ui button"/>
+ <input type="hidden" name="id_cotis" id="id_cotis" value="{{ scheduled.getContribution().id }}"/>
+ {% endif %}
+ {% if scheduled.getId() %}
+ <input type="hidden" name="id" id="id" value="{{ scheduled.getId() }}"/>
+ {% endif %}
+
+ {% include 'components/forms/csrf.html.twig' %}
+ {% if mode != 'ajax' %}
+ </div>
+ {% endif %}
+ </form>
+{% endblock %}
--- /dev/null
+{#
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+#}
+{% extends 'elements/list.html.twig' %}
+
+{% set form = {
+ 'route': {
+ 'name': "batch-scheduledPaymentslist"
+ },
+ 'order': {
+ 'name': "scheduledPayments"
+ }
+} %}
+
+{% if nb != 0 and (login.isAdmin() or login.isStaff()) and mode != 'ajax' %}
+ {% set batch = {
+ 'route': {
+ 'name': 'batch-scheduledPaymentslist'
+ },
+ 'modal': {
+ 'title': _T("No entry selected"),
+ 'content': _T("Please make sure to select at least one entry from the list to perform this action.")
+ }
+ } %}
+ {% set batch_actions = [
+ {
+ 'name': 'delete',
+ 'label': _T("Delete"),
+ 'icon': 'trash red'
+ },
+ {
+ 'name': 'csv__directdownload',
+ 'label': _T("Export as CSV"),
+ 'icon': 'file csv'
+ }
+ ] %}
+{% endif %}
+
+{% block search %}
+ <form action="{{ url_for("filterScheduledPayments") }}" method="post" class="ui form filters">
+ <div class="ui secondary yellow segment">
+ <div class="four fields">
+ <div class="field">
+ <label for="date_field">{{ _T("Show contributions by") }}</label>
+ <select name="date_field" id="date_field" class="ui search dropdown">
+ <option value="{{ constant("Galette\\Filters\\ScheduledPaymentsList::DATE_RECORD") }}"{% if filters.date_field == constant('Galette\\Filters\\ScheduledPaymentsList::DATE_RECORD') %} selected="selected"{% endif %}>{{ _T("Date") }}</option>
+ <option value="{{ constant("Galette\\Filters\\ScheduledPaymentsList::DATE_SCHEDULED") }}"{% if filters.date_field == constant('Galette\\Filters\\ScheduledPaymentsList::DATE_SCHEDULED') %} selected="selected"{% endif %}>{{ _T("Scheduled date") }}</option>
+ </select>
+ </div>
+ <div class="two fields">
+ <div class="field">
+ <label for="start_date_filter">{{ _T("since") }}</label>
+ <div class="ui calendar" id="contrib-rangestart">
+ <div class="ui input left icon">
+ <i class="calendar icon" aria-hidden="true"></i>
+ <input placeholder="{{ _T("yyyy-mm-dd format") }}" type="text" name="start_date_filter" id="start_date_filter" maxlength="10" size="10" value="{{ filters.start_date_filter }}"/>
+ </div>
+ </div>
+ </div>
+ <div class="field">
+ <label for="end_date_filter">{{ _T("until") }}</label>
+ <div class="ui calendar" id="contrib-rangeend">
+ <div class="ui input left icon">
+ <i class="calendar icon" aria-hidden="true"></i>
+ <input placeholder="{{ _T("yyyy-mm-dd format") }}" type="text" name="end_date_filter" id="end_date_filter" maxlength="10" size="10" value="{{ filters.end_date_filter }}"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="field">
+ {% include "components/forms/payment_types.html.twig" with {
+ current: filters.payment_type_filter,
+ varname: "payment_type_filter",
+ classname: "",
+ empty: {'value': -1, 'label': _T("Select")}
+ } %}
+ </div>
+ <div class="ui right aligned basic fitted segment field flexend">
+ <button type="submit" class="tooltip ui labeled icon primary button" title="{{ _T("Apply filters") }}" name="filter">
+ <i class="search icon" aria-hidden="true"></i>
+ {{ _T("Filter") }}
+ </button>
+ <button type="submit" id="clear_filter" name="clear_filter" class="tooltip ui labeled icon button" title="{{ _T("Reset all filters to defaults") }}">
+ <i class="trash alt red icon" aria-hidden="true"></i>
+ {{ _T("Clear filter") }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ {% if mode == 'ajax'%}
+ <input type="hidden" name="ajax" value="true"/>
+ <input type="hidden" name="from_contribution" value="{{ filters.from_contribution }}"/>
+ {% else %}
+ {% include "components/forms/csrf.html.twig" %}
+ {% endif %}
+ </form>
+{% endblock %}
+
+{% block infoline %}
+ {% set infoline = {
+ 'label': _Tn("%count shceduled payment", "%count scheduled payments", nb)|replace({"%count": nb}),
+ 'route': {
+ 'name': 'filterScheduledPayments'
+ }
+ } %}
+ {{ parent() }}
+{% endblock %}
+
+{% block header %}
+ {% set columns = [
+ {'label': '#', 'order': constant("Galette\\Filters\\ScheduledPaymentsList::ORDERBY_ID")},
+ {'label': _T("Member"), 'order': constant("Galette\\Filters\\ScheduledPaymentsList::ORDERBY_MEMBER")},
+ {'label': _T("Date"), 'order': constant("Galette\\Filters\\ScheduledPaymentsList::ORDERBY_DATE")},
+ {'label': _T("Scheduled date"), 'order': constant("Galette\\Filters\\ScheduledPaymentsList::ORDERBY_SCHEDULED_DATE")},
+ {'label': _T('Amount'), 'order': constant("Galette\\Filters\\ScheduledPaymentsList::ORDERBY_AMOUNT")},
+ {'label': _T("Payment type"), 'order': constant("Galette\\Filters\\ScheduledPaymentsList::ORDERBY_PAYMENT_TYPE")}
+ ] %}
+
+ {% if (not login.isAdmin() and not login.isStaff()) or mode == 'ajax' %}
+ {% set no_action = true %}
+ {% endif %}
+
+ {{ parent() }}
+{% endblock %}
+
+{% block footer %}
+ {% if nb != 0 %}
+ <tr>
+ <th class="right aligned" colspan="{% if (login.isAdmin() or login.isStaff()) and member is not defined %}7{% else %}6{% endif %}">
+ {{ _T("Found total scheduled %f")|replace({"%f": scheduled.getSum()}) }}
+ </th>
+ </tr>
+ {% endif %}
+{% endblock %}
+
+{% block body %}
+ {% for ordre, scheduled in list %}
+ {% set contribution = scheduled.getContribution() %}
+ {% if contribution.isFee() %}
+ {% set ctype = constant('Galette\\Entity\\Contribution::TYPE_FEE') %}
+ {% else %}
+ {% set ctype = constant('Galette\\Entity\\Contribution::TYPE_DONATION') %}
+ {% endif %}
+
+ <tr class="{% if mode == 'ajax' %}schedule_row {% endif %}" id="row_{{ scheduled.getId() }}">
+ <td data-scope="row">
+ <input type="checkbox" name="entries_sel[]" value="{{ scheduled.getId() }}"/>
+ {% if preferences.pref_show_id %}
+ {{ scheduled.getId() }}
+ {% else %}
+ {{ ordre + 1 + (filters.current_page - 1) * numrows }}
+ {% endif %}
+ {% if (login.isAdmin() or login.isStaff()) and mode != 'ajax' %}
+ <span>
+ <a href="{{ url_for("editContribution", {"type": ctype, "id": contribution.id}) }}">
+ <i
+ class="ui {% if contribution.isFee() %}user check{% else %}gift{% endif %} grey icon tooltip"
+ title="{% if contribution.isFee() %}{{ _T('Contribution') }}{% else %}{{ _T('Donation') }}{% endif %}"
+ ></i>
+ <span class="visually-hidden">{{ _T("Contribution %id")|replace({"%id": contribution.id}) }}</span>
+ </a>
+ </span>
+ {% endif %}
+ </td>
+ <td data-col-label="{{ _T("Member") }}">{{ memberName({id: scheduled.getContribution().member}) }}</td>
+ <td data-col-label="{{ _T("Date") }}">{{ scheduled.getCreationDate() }}</td>
+ <td data-col-label="{{ _T("Scheduled date") }}">{{ scheduled.getScheduledDate() }}</td>
+ <td data-col-label="{{ _T("Amount") }}">{{ scheduled.getAmount() }}</td>
+ <td data-col-label="{{ _T("Payment type") }}">{{ scheduled.getPaymentType() }}</td>
+ {% if (login.isAdmin() or login.isStaff()) and mode != 'ajax' %}
+ <td class="actions_row center">
+ {% if (login.isAdmin() or login.isStaff()) %}
+ <a
+ href="{{ url_for("editScheduledPayment", {"id": scheduled.getId()}) }}"
+ class="action"
+ >
+ <i class="ui edit icon tooltip" aria-hidden="true"></i>
+ <span class="ui special popup">{{ _T("Edit scheduled payment") }}</span>
+ </a>
+ <a
+ href="{{ url_for("removeScheduledPayment", {"id": scheduled.getId()}) }}"
+ class="delete"
+ >
+ <i class="ui trash red icon tooltip" aria-hidden="true"></i>
+ <span class="ui special popup">{{ _T("Delete scheduled payment") }}</span>
+ </a>
+ {% endif %}
+ </td>
+ {% endif %}
+ </tr>
+ {% else %}
+ <tr><td colspan="{% if (login.isAdmin() or login.isStaff()) and member is not defined %}6{% elseif login.isAdmin() or login.isStaff() %}6{% else %}5{% endif %}" class="emptylist">{{ _T("No scheduled payment") }}</td></tr>
+ {% endfor %}
+{% endblock %}
},
error: function() {
{% include "elements/js/modal.js.twig" with {
- modal_title_twig: _T("An error occurred displaying members interface :(")|e("js"),
+ modal_title_twig: _T("An error occurred displaying contributions :(")|e("js"),
modal_without_content: true,
modal_class: "mini",
modal_deny_only: true,
'galette_field_types',
'galette_fields_categories',
'galette_mailing_history',
+ 'galette_payments_schedules',
'galette_pdfmodels',
'galette_preferences',
'galette_searches',
--- /dev/null
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\Entity\test\units;
+
+use Galette\GaletteTestCase;
+
+/**
+ * Scheduled payment tests
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ */
+class ScheduledPayment extends GaletteTestCase
+{
+ protected int $seed = 20240321210526;
+
+ /**
+ * Tear down tests
+ *
+ * @return void
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ $this->deleteScheduledPayments();
+ }
+
+ /**
+ * Delete scheduled payments
+ *
+ * @return void
+ */
+ private function deleteScheduledPayments()
+ {
+ $delete = $this->zdb->delete(\Galette\Entity\ScheduledPayment::TABLE);
+ $delete->where(['comment' => 'FAKER' . $this->seed]);
+ $this->zdb->execute($delete);
+
+ $delete = $this->zdb->delete(\Galette\Entity\Contribution::TABLE);
+ $delete->where(['info_cotis' => 'FAKER' . $this->seed]);
+ $this->zdb->execute($delete);
+
+ $delete = $this->zdb->delete(\Galette\Entity\Adherent::TABLE);
+ $delete->where(['fingerprint' => 'FAKER' . $this->seed]);
+ $this->zdb->execute($delete);
+ }
+
+ /**
+ * Test add
+ *
+ * @return void
+ */
+ public function testAdd(): void
+ {
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $this->assertFalse($this->contrib->hasSchedule());
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $this->assertFalse($scheduledPayment->isContributionHandled($this->contrib->id));
+ $now = new \DateTime();
+
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'amount' => 10.0,
+ 'comment' => 'FAKER' . $this->seed
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $pid = $scheduledPayment->getId();
+ $this->assertTrue($this->contrib->hasSchedule());
+ $this->assertTrue($scheduledPayment->isContributionHandled($this->contrib->id));
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb, $pid);
+ $this->assertSame($data[\Galette\Entity\Contribution::PK], $scheduledPayment->getContribution()->id);
+ $this->assertSame($data['id_paymenttype'], $scheduledPayment->getPaymentType()->id);
+ $this->assertSame($data['scheduled_date'], $scheduledPayment->getScheduledDate());
+ $this->assertSame($data['scheduled_date'], $scheduledPayment->getScheduledDate(false)->format('Y-m-d'));
+ $this->assertSame($data['amount'], $scheduledPayment->getAmount());
+ $this->assertSame($data['comment'], $scheduledPayment->getComment());
+ }
+
+ /**
+ * Test update
+ *
+ * @return void
+ */
+ public function testUpdate(): void
+ {
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ //no amount, will take contribution amount
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $pid = $scheduledPayment->getId();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb, $pid);
+ $this->assertSame($data[\Galette\Entity\Contribution::PK], $scheduledPayment->getContribution()->id);
+ $this->assertSame($data['id_paymenttype'], $scheduledPayment->getPaymentType()->id);
+ $this->assertSame($data['scheduled_date'], $scheduledPayment->getScheduledDate());
+ $this->assertSame($data['scheduled_date'], $scheduledPayment->getScheduledDate(false)->format('Y-m-d'));
+ $this->assertSame($this->contrib->amount, $scheduledPayment->getAmount());
+ $this->assertSame($data['comment'], $scheduledPayment->getComment());
+
+ $data['amount'] = 20.0;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb, $pid);
+ $this->assertSame($data[\Galette\Entity\Contribution::PK], $scheduledPayment->getContribution()->id);
+ $this->assertSame($data['id_paymenttype'], $scheduledPayment->getPaymentType()->id);
+ $this->assertSame($data['scheduled_date'], $scheduledPayment->getScheduledDate());
+ $this->assertSame($data['scheduled_date'], $scheduledPayment->getScheduledDate(false)->format('Y-m-d'));
+ $this->assertSame($data['amount'], $scheduledPayment->getAmount());
+ $this->assertSame($data['comment'], $scheduledPayment->getComment());
+ }
+
+ /**
+ * Test update
+ *
+ * @return void
+ */
+ public function testCheck(): void
+ {
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ $data = [];
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(
+ [
+ 'Contribution is required',
+ 'Payment type is required',
+ 'Scheduled date is required'
+ ],
+ $scheduledPayment->getErrors()
+ );
+
+ $data = [
+ 'scheduled_date' => $now->format('Y-m-d')
+ ];
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(
+ [
+ 'Contribution is required',
+ 'Payment type is required'
+ ],
+ $scheduledPayment->getErrors()
+ );
+
+ $data += [
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CREDITCARD
+ ];
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(
+ [
+ 'Contribution is required'
+ ],
+ $scheduledPayment->getErrors()
+ );
+
+ $data += [
+ \Galette\Entity\Contribution::PK => $this->contrib->id
+ ];
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(
+ [
+ 'Payment type for contribution must be set to scheduled'
+ ],
+ $scheduledPayment->getErrors()
+ );
+
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+
+ $data += [
+ 'amount' => -1
+ ];
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(
+ [
+ 'Amount must be a positive number'
+ ],
+ $scheduledPayment->getErrors()
+ );
+
+ $data['amount'] = 0;
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(
+ [
+ 'Amount must be a positive number'
+ ],
+ $scheduledPayment->getErrors()
+ );
+
+ $data['amount'] = 200.0;
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(
+ [
+ 'Amount cannot be greater than non allocated amount'
+ ],
+ $scheduledPayment->getErrors()
+ );
+
+ $data['amount'] = 10.0;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+
+ $data['id_paymenttype'] = \Galette\Entity\PaymentType::SCHEDULED;
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(
+ [
+ 'Cannot schedule a scheduled payment!'
+ ],
+ $scheduledPayment->getErrors()
+ );
+ }
+
+ /**
+ * Test delete
+ *
+ * @return void
+ */
+ public function testDelete(): void
+ {
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ //no amount, will take contribution amount
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $pid = $scheduledPayment->getId();
+
+ $this->assertTrue($scheduledPayment->load($pid));
+ $this->assertTrue($scheduledPayment->remove());
+ $this->assertFalse($scheduledPayment->load($pid));
+ }
+
+ /**
+ * Test restrictions on contributions with a scheduled payment
+ *
+ * @return void
+ */
+ public function testContributionRestriction(): void
+ {
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ //no amount, will take contribution amount
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+ $this->assertSame([], $this->contrib->getErrors());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ //test it's not possible to change payment type if there is a scheduled payment
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::CASH;
+ $this->expectException('RuntimeException');
+ $this->expectExceptionMessage('Existing errors prevents storing contribution: Array
+(
+ [0] => Cannot change payment type if there is an attached scheduled payment
+)
+');
+ $this->assertFalse($this->contrib->store());
+ $this->assertSame(
+ ['Cannot change payment type if there is an attached scheduled payment'],
+ $this->contrib->getErrors()
+ );
+ }
+
+ /**
+ * Test getNotFullyAllocated
+ *
+ * @return void
+ */
+ public function testGetNotFullyAllocated(): void
+ {
+ // retrieve contributions with schedule as payment type and that are not allocated, or not fully allocated
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ $nonfulls = $scheduledPayment->getNotFullyAllocated();
+ $this->assertCount(0, $nonfulls); //no contributiopn with SCHEDULED payment type
+
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+ $this->assertSame([], $this->contrib->getErrors());
+
+ $nonfulls = $scheduledPayment->getNotFullyAllocated();
+ $this->assertCount(1, $nonfulls);
+ $test = array_pop($nonfulls);
+ $this->assertSame(
+ [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'montant_cotis' => '92.00',
+ 'allocated' => null,
+ ],
+ $test
+ );
+
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed,
+ 'amount' => 10.0
+ ];
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $data['amount'] = 24.5;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $nonfulls = $scheduledPayment->getNotFullyAllocated();
+ $this->assertCount(1, $nonfulls);
+ $test = array_pop($nonfulls);
+ $this->assertSame(
+ [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'montant_cotis' => '92.00',
+ 'allocated' => '34.50',
+ ],
+ $test
+ );
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $data['amount'] = 92 - 34.5;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $nonfulls = $scheduledPayment->getNotFullyAllocated();
+ $this->assertCount(0, $nonfulls);
+ }
+
+ /**
+ * Test getAllocation
+ *
+ * @return void
+ */
+ public function testGetAllocation(): void
+ {
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed,
+ 'amount' => 10.0
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+ $this->assertSame([], $this->contrib->getErrors());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $data['amount'] = 25.0;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $this->assertSame(35.0, $scheduledPayment->getAllocation($this->contrib->id));
+ }
+
+ /**
+ * Test isFullyAllocated
+ *
+ * @return void
+ */
+ public function testIsFullyAllocated(): void
+ {
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed,
+ 'amount' => 10.0
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+ $this->assertSame([], $this->contrib->getErrors());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $data['amount'] = 25.0;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $this->assertSame(35.0, $scheduledPayment->getAllocation($this->contrib->id));
+ $this->assertSame(92.0 - 35.0, $scheduledPayment->getMissingAmount());
+ $this->assertFalse($scheduledPayment->isFullyAllocated($this->contrib));
+
+ //contribution amount is 92
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $data['amount'] = 92 - 35 + 1;
+ $check = $scheduledPayment->check($data);
+ $this->assertFalse($check);
+ $this->assertSame(['Amount cannot be greater than non allocated amount'], $scheduledPayment->getErrors());
+
+ $data['amount'] = 92 - 35;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $this->assertSame(92.0, $scheduledPayment->getAllocation($this->contrib->id));
+ $this->assertSame(92.0, $scheduledPayment->getAllocated());
+ $this->assertSame(0.0, $scheduledPayment->getMissingAmount());
+ $this->assertTrue($scheduledPayment->isFullyAllocated($this->contrib));
+ }
+}
$types = new \Galette\Repository\PaymentTypes($this->zdb, $this->preferences, $this->login);
$list = $types->getList();
- $this->assertCount(6, $list);
+ $this->assertCount(7, $list);
if ($this->zdb->isPostgres()) {
$select = $this->zdb->select(\Galette\Entity\PaymentType::TABLE . '_id_seq');
$types->installInit();
$list = $types->getList();
- $this->assertCount(6, $list);
+ $this->assertCount(7, $list);
if ($this->zdb->isPostgres()) {
$select = $this->zdb->select(\Galette\Entity\PaymentType::TABLE . '_id_seq');
--- /dev/null
+<?php
+
+/**
+ * Copyright © 2003-2024 The Galette Team
+ *
+ * This file is part of Galette (https://galette.eu).
+ *
+ * Galette 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.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Galette\Repository\test\units;
+
+use Galette\GaletteTestCase;
+
+/**
+ * Scheduled payments repository tests
+ *
+ * @author Johan Cwiklinski <johan@x-tnd.be>
+ */
+class ScheduledPayments extends GaletteTestCase
+{
+ protected int $seed = 20240407181603;
+
+ /**
+ * Tear down tests
+ *
+ * @return void
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ $this->deleteScheduledPayments();
+ }
+
+ /**
+ * Delete scheduled payments
+ *
+ * @return void
+ */
+ private function deleteScheduledPayments()
+ {
+ $delete = $this->zdb->delete(\Galette\Entity\ScheduledPayment::TABLE);
+ $delete->where(['comment' => 'FAKER' . $this->seed]);
+ $this->zdb->execute($delete);
+
+ $delete = $this->zdb->delete(\Galette\Entity\Contribution::TABLE);
+ $delete->where(['info_cotis' => 'FAKER' . $this->seed]);
+ $this->zdb->execute($delete);
+
+ $delete = $this->zdb->delete(\Galette\Entity\Adherent::TABLE);
+ $delete->where(['fingerprint' => 'FAKER' . $this->seed]);
+ $this->zdb->execute($delete);
+ }
+
+ /**
+ * Test getList
+ *
+ * @return void
+ */
+ public function testGetList()
+ {
+ $this->logSuperAdmin();
+ $scheduledPayments = new \Galette\Repository\ScheduledPayments($this->zdb, $this->login);
+
+ $list = $scheduledPayments->getList(true, null);
+ $this->assertIsArray($list);
+ $this->assertCount(0, $list);
+ $this->assertSame(0, $scheduledPayments->getCount());
+ $this->assertSame(0.0, $scheduledPayments->getSum());
+
+ //create contribution, and associated scheduled payments
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed,
+ 'amount' => 10.0
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+ $this->assertSame([], $this->contrib->getErrors());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $scheduled_date = $now->modify('+1 month');
+ $data['scheduled_date'] = $scheduled_date->format('Y-m-d');
+ $data['amount'] = 25.0;
+ $data['id_paymenttype'] = \Galette\Entity\PaymentType::CREDITCARD;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+
+ $list = $scheduledPayments->getList(true);
+ $this->assertIsArray($list);
+ $this->assertCount(2, $list);
+ $this->assertSame(35.0, $scheduledPayments->getSum());
+
+ //filters
+ $filters = new \Galette\Filters\ScheduledPaymentsList();
+ $filters->from_contribution = $this->contrib->id + 1;
+ $scheduledPayments = new \Galette\Repository\ScheduledPayments($this->zdb, $this->login, $filters);
+ $list = $scheduledPayments->getList(true);
+ $this->assertCount(0, $list);
+
+ $filters = new \Galette\Filters\ScheduledPaymentsList();
+ $filters->date_field = \Galette\Filters\ScheduledPaymentsList::DATE_SCHEDULED;
+ $filters->start_date_filter = $scheduled_date->modify('-1 day')->format('Y-m-d');
+ $filters->end_date_filter = $scheduled_date->modify('+1 day')->format('Y-m-d');
+ $scheduledPayments = new \Galette\Repository\ScheduledPayments($this->zdb, $this->login, $filters);
+ $list = $scheduledPayments->getList(true);
+ $this->assertCount(1, $list);
+ $this->assertSame(25.0, $scheduledPayments->getSum());
+
+ $filters = new \Galette\Filters\ScheduledPaymentsList();
+ $filters->payment_type_filter = \Galette\Entity\PaymentType::CASH;
+ $scheduledPayments = new \Galette\Repository\ScheduledPayments($this->zdb, $this->login, $filters);
+ $list = $scheduledPayments->getList(true);
+ $this->assertCount(1, $list);
+ $this->assertSame(10.0, $scheduledPayments->getSum());
+ }
+
+ /**
+ * Test getArrayList
+ *
+ * @return void
+ */
+ public function testGetArrayList()
+ {
+ $this->logSuperAdmin();
+ $scheduledPayments = new \Galette\Repository\ScheduledPayments($this->zdb, $this->login);
+
+ $list = $scheduledPayments->getList(true, null);
+ $this->assertIsArray($list);
+ $this->assertCount(0, $list);
+ $this->assertSame(0, $scheduledPayments->getCount());
+ $this->assertSame(0.0, $scheduledPayments->getSum());
+
+ //create contribution, and associated scheduled payments
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed,
+ 'amount' => 10.0
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+ $this->assertSame([], $this->contrib->getErrors());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+ $id_1 = $scheduledPayment->getId();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $scheduled_date = $now->modify('+1 month');
+ $data['scheduled_date'] = $scheduled_date->format('Y-m-d');
+ $data['amount'] = 25.0;
+ $data['id_paymenttype'] = \Galette\Entity\PaymentType::CREDITCARD;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+ $id_2 = $scheduledPayment->getId();
+
+ $list = $scheduledPayments->getArrayList([$id_1, $id_2], true);
+ $this->assertIsArray($list);
+ $this->assertCount(2, $list);
+ $contrib = array_pop($list);
+ $this->assertTrue($contrib instanceof \Galette\Entity\ScheduledPayment);
+
+ $list = $scheduledPayments->getArrayList([$id_1, $id_2], false);
+ $this->assertIsArray($list);
+ $this->assertCount(2, $list);
+ $contrib = array_pop($list);
+ $this->assertFalse($contrib instanceof \Galette\Entity\ScheduledPayment);
+ }
+
+ /**
+ * Test remove
+ *
+ * @return void
+ */
+ public function testRemove()
+ {
+ $this->logSuperAdmin();
+ $scheduledPayments = new \Galette\Repository\ScheduledPayments($this->zdb, $this->login);
+
+ $list = $scheduledPayments->getList(true, null);
+ $this->assertIsArray($list);
+ $this->assertCount(0, $list);
+ $this->assertSame(0, $scheduledPayments->getCount());
+ $this->assertSame(0.0, $scheduledPayments->getSum());
+
+ //create contribution, and associated scheduled payments
+ $this->logSuperAdmin();
+ $this->getMemberOne();
+ //create contribution for member
+ $this->createContribution();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $now = new \DateTime();
+
+ $data = [
+ \Galette\Entity\Contribution::PK => $this->contrib->id,
+ 'id_paymenttype' => \Galette\Entity\PaymentType::CASH,
+ 'scheduled_date' => $now->format('Y-m-d'),
+ 'comment' => 'FAKER' . $this->seed,
+ 'amount' => 10.0
+ ];
+ $this->contrib->payment_type = \Galette\Entity\PaymentType::SCHEDULED;
+ $this->assertTrue($this->contrib->store());
+ $this->assertSame([], $this->contrib->getErrors());
+
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+ $id_1 = $scheduledPayment->getId();
+
+ $scheduledPayment = new \Galette\Entity\ScheduledPayment($this->zdb);
+ $scheduled_date = $now->modify('+1 month');
+ $data['scheduled_date'] = $scheduled_date->format('Y-m-d');
+ $data['amount'] = 25.0;
+ $data['id_paymenttype'] = \Galette\Entity\PaymentType::CREDITCARD;
+ $check = $scheduledPayment->check($data);
+ if (count($scheduledPayment->getErrors())) {
+ var_dump($scheduledPayment->getErrors());
+ }
+ $this->assertTrue($check);
+ $this->assertTrue($scheduledPayment->store());
+ $id_2 = $scheduledPayment->getId();
+
+ $list = $scheduledPayments->getList(true);
+ $this->assertIsArray($list);
+ $this->assertCount(2, $list);
+
+ $this->assertTrue($scheduledPayments->remove([$id_1, $id_2], $this->history));
+
+ $list = $scheduledPayments->getList(true);
+ $this->assertCount(0, $list);
+ }
+}
{
$ct = new \Galette\Entity\ContributionsTypes($this->zdb);
if (count($ct->getCompleteList()) === 0) {
- //status are not yet instanciated.
+ //contributions types are not yet instanciated.
$res = $ct->installInit();
$this->assertTrue($res);
}
$types = new \Galette\Repository\PaymentTypes($this->zdb, $this->preferences, $this->login);
if (count($types->getList()) === 0) {
//payment types are not yet instanciated.
- $res = $types->installInit(false);
+ $res = $types->installInit();
$this->assertTrue($res);
}
}
protected function initTitles(): void
{
$titles = new \Galette\Repository\Titles($this->zdb);
- if (count($titles->getList($this->zdb)) === 0) {
+ if (count($titles->getList()) === 0) {
$res = $titles->installInit();
$this->assertTrue($res);
}