]> git.agnieray.net Git - galette.git/blobdiff - galette/lib/Galette/Controllers/Crud/ContributionsController.php
Disable events from mass changes; closes #1733
[galette.git] / galette / lib / Galette / Controllers / Crud / ContributionsController.php
index 61367f2df40870df1ba38b73549818929bb2fcd2..ebee1834eb064bbc4f0be95c21d57fe0960c5c1b 100644 (file)
@@ -7,7 +7,7 @@
  *
  * PHP version 5
  *
- * Copyright © 2020 The Galette Team
+ * Copyright © 2020-2023 The Galette Team
  *
  * This file is part of Galette (http://galette.tuxfamily.org).
  *
@@ -28,7 +28,7 @@
  * @package   Galette
  *
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2020 The Galette Team
+ * @copyright 2020-2023 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.9.4dev - 2020-05-08
 
 namespace Galette\Controllers\Crud;
 
-use Throwable;
+use Galette\Features\BatchList;
+use Analog\Analog;
 use Galette\Controllers\CrudController;
-use Slim\Http\Request;
-use Slim\Http\Response;
+use Slim\Psr7\Request;
+use Slim\Psr7\Response;
 use Galette\Entity\Adherent;
 use Galette\Entity\Contribution;
 use Galette\Entity\Transaction;
-use Galette\Repository\Contributions;
-use Galette\Repository\Transactions;
 use Galette\Repository\Members;
 use Galette\Entity\ContributionsTypes;
-use Galette\Core\GaletteMail;
-use Galette\Entity\Texts;
-use Galette\IO\PdfMembersCards;
 use Galette\Repository\PaymentTypes;
-use Analog\Analog;
 
 /**
  * Galette contributions controller
@@ -60,7 +55,7 @@ use Analog\Analog;
  * @name      ContributionsController
  * @package   Galette
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2020 The Galette Team
+ * @copyright 2020-2023 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.9.4dev - 2020-05-02
@@ -68,6 +63,8 @@ use Analog\Analog;
 
 class ContributionsController extends CrudController
 {
+    use BatchList;
+
     // CRUD - Create
 
     /**
@@ -78,7 +75,7 @@ class ContributionsController extends CrudController
      *
      * @param Request      $request  PSR Request
      * @param Response     $response PSR Response
-     * @param array        $args     Request arguments
+     * @param string       $type     Contribution type
      * @param Contribution $contrib  Contribution instance
      *
      * @return Response
@@ -86,23 +83,28 @@ class ContributionsController extends CrudController
     public function addEditPage(
         Request $request,
         Response $response,
-        array $args,
+        string $type,
         Contribution $contrib
     ): Response {
-        // contribution types
-        $ct = new ContributionsTypes($this->zdb);
-        $contributions_types = $ct->getList($args['type'] === 'fee');
-
-        $disabled = array();
+        $post = $request->getParsedBody();
 
-        if (!is_int($contrib->id)) {
-            // initialiser la structure contribution à vide (nouvelle contribution)
-            $contribution['duree_mois_cotis'] = $this->preferences->pref_membership_ext;
+        // check for ajax mode
+        $ajax = false;
+        if (
+            ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')
+            || isset($post['ajax'])
+            && $post['ajax'] == 'true'
+        ) {
+            $ajax = true;
         }
 
+        // contribution types
+        $ct = new ContributionsTypes($this->zdb);
+        $contributions_types = $ct->getList($type === Contribution::TYPE_FEE);
+
         // template variable declaration
         $title = null;
-        if ($args['type'] === 'fee') {
+        if ($type === Contribution::TYPE_FEE) {
             $title = _T("Membership fee");
         } else {
             $title = _T("Donation");
@@ -114,23 +116,12 @@ class ContributionsController extends CrudController
             $title .= ' (' . _T("creation") . ')';
         }
 
-        // required fields
-        $required = [
-            'id_type_cotis'     => 1,
-            'id_adh'            => 1,
-            'date_enreg'        => 1,
-            'date_debut_cotis'  => 1,
-            'date_fin_cotis'    => $contrib->isCotis(),
-            'montant_cotis'     => $contrib->isCotis() ? 1 : 0
-        ];
-
         $params = [
             'page_title'        => $title,
-            'required'          => $required,
-            'disabled'          => $disabled,
+            'required'          => $contrib->getRequired(),
             'contribution'      => $contrib,
             'adh_selected'      => $contrib->member,
-            'type'              => $args['type']
+            'type'              => $type
         ];
 
         // contribution types
@@ -138,9 +129,10 @@ class ContributionsController extends CrudController
 
         // members
         $m = new Members();
-        $members = $m->getSelectizedMembers(
+        $members = $m->getDropdownMembers(
             $this->zdb,
-            isset($contrib) && $contrib->member > 0 ? $contrib->member : null
+            $this->login,
+            $contrib->member > 0 ? $contrib->member : null
         );
 
         $params['members'] = [
@@ -153,16 +145,17 @@ class ContributionsController extends CrudController
         }
 
         $ext_membership = '';
-        if (isset($contrib) && $contrib->isCotis() || !isset($contrib) && $args['type'] === 'fee') {
+        if ($contrib->isFee() || $type === Contribution::TYPE_FEE) {
             $ext_membership = $this->preferences->pref_membership_ext;
         }
         $params['pref_membership_ext'] = $ext_membership;
         $params['autocomplete'] = true;
+        $params['mode'] = ($ajax ? 'ajax' : '');
 
         // display page
         $this->view->render(
             $response,
-            'ajouter_contribution.tpl',
+            'pages/contribution_form.html.twig',
             $params
         );
         return $response;
@@ -171,13 +164,13 @@ class ContributionsController extends CrudController
     /**
      * Add page
      *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param Request     $request  PSR Request
+     * @param Response    $response PSR Response
+     * @param string|null $type     Contribution type
      *
      * @return Response
      */
-    public function add(Request $request, Response $response, array $args = []): Response
+    public function add(Request $request, Response $response, string $type = null): Response
     {
         if ($this->session->contribution !== null) {
             $contrib = $this->session->contribution;
@@ -186,7 +179,7 @@ class ContributionsController extends CrudController
             $get = $request->getQueryParams();
 
             $ct = new ContributionsTypes($this->zdb);
-            $contributions_types = $ct->getList($args['type'] === 'fee');
+            $contributions_types = $ct->getList($type === Contribution::TYPE_FEE);
 
             $cparams = ['type' => array_keys($contributions_types)[0]];
 
@@ -203,7 +196,7 @@ class ContributionsController extends CrudController
             $contrib = new Contribution(
                 $this->zdb,
                 $this->login,
-                (count($cparams) > 0 ? $cparams : null)
+                $cparams
             );
 
             if (isset($cparams['adh'])) {
@@ -215,22 +208,177 @@ class ContributionsController extends CrudController
             }
         }
 
-        return $this->addEditPage($request, $response, $args, $contrib);
+        return $this->addEditPage($request, $response, $type, $contrib);
     }
 
     /**
      * Add action
      *
+     * @param Request     $request  PSR Request
+     * @param Response    $response PSR Response
+     * @param string|null $type     Contribution type
+     *
+     * @return Response
+     */
+    public function doAdd(Request $request, Response $response, string $type = null): Response
+    {
+        return $this->store($request, $response, 'add', $type);
+    }
+
+    /**
+     * Choose contribution type to mass add contribution
+     *
+     * @param Request  $request  PSR Request
+     * @param Response $response PSR Response
+     *
+     * @return Response
+     */
+    public function massAddChooseType(Request $request, Response $response): Response
+    {
+        $filters = $this->session->filter_members;
+        $data = [
+            'id'            => $filters->selected,
+            'redirect_uri'  => $this->routeparser->urlFor('members')
+        ];
+
+        // display page
+        $this->view->render(
+            $response,
+            'modals/mass_choose_contributions_type.html.twig',
+            array(
+                'mode'          => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ? 'ajax' : '',
+                'page_title'    => str_replace(
+                    '%count',
+                    count($data['id']),
+                    _T('Mass add contribution on %count members')
+                ),
+                'data'          => $data,
+                'form_url'      => $this->routeparser->urlFor('massAddContributions'),
+                'cancel_uri'    => $this->routeparser->urlFor('members')
+            )
+        );
+        return $response;
+    }
+
+    /**
+     * Massive change page
+     *
+     * @param Request  $request  PSR Request
+     * @param Response $response PSR Response
+     *
+     * @return Response
+     */
+    public function massAddContributions(Request $request, Response $response): Response
+    {
+        $post = $request->getParsedBody();
+        $filters = $this->session->filter_members;
+        $type = $post['type'];
+
+        $ct = new ContributionsTypes($this->zdb);
+        $contributions_types = $ct->getList($type === Contribution::TYPE_FEE);
+
+        $contribution = new Contribution(
+            $this->zdb,
+            $this->login,
+            ['type' => array_keys($contributions_types)[0]]
+        );
+
+        $data = [
+            'id'            => $filters->selected,
+            'redirect_uri'  => $this->routeparser->urlFor('members'),
+            'type'          => $type
+        ];
+
+        // display page
+        $this->view->render(
+            $response,
+            'modals/mass_add_contributions.html.twig',
+            array(
+                'mode'          => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ? 'ajax' : '',
+                'page_title'    => str_replace(
+                    '%count',
+                    count($data['id']),
+                    _T('Mass add contribution on %count members')
+                ),
+                'form_url'      => $this->routeparser->urlFor('doMassAddContributions'),
+                'cancel_uri'    => $this->routeparser->urlFor('members'),
+                'data'          => $data,
+                'contribution'  => $contribution,
+                'type'          => $type,
+                'require_mass'  => true,
+                'required'      => $contribution->getRequired(),
+                'type_cotis_options' => $contributions_types
+            )
+        );
+        return $response;
+    }
+
+    /**
+     * Do massive contribution add
+     *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
      *
      * @return Response
      */
-    public function doAdd(Request $request, Response $response, array $args = []): Response
+    public function doMassAddContributions(Request $request, Response $response): Response
     {
-        $args['action'] = 'add';
-        return $this->store($request, $response, $args);
+        $post = $request->getParsedBody();
+        $members_ids = $post['id'];
+        unset($post['id']);
+
+        $error_detected = [];
+
+        // flagging required fields for first step only
+        $disabled = [];
+        $success = 0;
+        $errors = 0;
+
+        foreach ($members_ids as $member_id) {
+            $post[Adherent::PK] = (int)$member_id;
+            $contrib = new Contribution($this->zdb, $this->login);
+            $contrib->disableEvents();
+
+            // regular fields
+            $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
+            if ($valid !== true) {
+                $error_detected = array_merge($error_detected, $valid);
+            }
+
+            //all goes well, we can proceed
+            if (count($error_detected) == 0) {
+                $store = $contrib->store();
+                if ($store === true) {
+                    ++$success;
+                    $files_res = $contrib->handleFiles($_FILES);
+                    if (is_array($files_res)) {
+                        $error_detected = array_merge($error_detected, $files_res);
+                    }
+                } else {
+                    ++$errors;
+                }
+            }
+        }
+
+        if (count($error_detected) == 0) {
+            $redirect_url = $this->routeparser->urlFor('members');
+        } else {
+            //something went wrong.
+            //store entity in session
+            $redirect_url = $this->routeparser->urlFor('massAddContributions');
+            //report errors
+            foreach ($error_detected as $error) {
+                $this->flash->addMessage(
+                    'error_detected',
+                    $error
+                );
+            }
+        }
+
+        //redirect to calling action
+        return $response
+            ->withStatus(301)
+            ->withHeader('Location', $redirect_url);
     }
 
     // /CRUD - Create
@@ -239,40 +387,50 @@ class ContributionsController extends CrudController
     /**
      * List page
      *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param Request        $request  PSR Request
+     * @param Response       $response PSR Response
+     * @param string         $option   One of 'page' or 'order'
+     * @param string|integer $value    Value of the option
+     * @param string         $type     One of 'transactions' or 'contributions'
      *
      * @return Response
      */
-    public function list(Request $request, Response $response, array $args = []): Response
+    public function list(Request $request, Response $response, $option = null, $value = null, $type = null): Response
     {
         $ajax = false;
-        if (
-            $request->isXhr()
-            || isset($request->getQueryParams()['ajax'])
-            && $request->getQueryParams()['ajax'] == 'true'
-        ) {
-            $ajax = true;
-        }
         $get = $request->getQueryParams();
 
-        $option = $args['option'] ?? null;
-        $value = $args['value'] ?? null;
-        $raw_type = null;
-
-        switch ($args['type']) {
+        switch ($type) {
             case 'transactions':
                 $raw_type = 'transactions';
                 break;
             case 'contributions':
                 $raw_type = 'contributions';
                 break;
+            default:
+                Analog::log(
+                    'Trying to load unknown contribution type ' . $type,
+                    Analog::WARNING
+                );
+                return $response
+                    ->withStatus(301)
+                    ->withHeader(
+                        'Location',
+                        $this->routeparser->urlFor('me')
+                    );
         }
 
         $filter_name = 'filter_' . $raw_type;
+        if (
+            ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')
+            || isset($get['ajax'])
+            && $get['ajax'] == 'true'
+        ) {
+            $ajax = true;
+            $filter_name .= '_ajax';
+        }
 
-        if (isset($this->session->$filter_name) && $ajax === false) {
+        if (isset($this->session->$filter_name)) {
             $filters = $this->session->$filter_name;
         } else {
             $filter_class = '\\Galette\\Filters\\' . ucwords($raw_type . 'List');
@@ -284,10 +442,11 @@ class ContributionsController extends CrudController
             $filters->filtre_cotis_adh = (int)$get[Adherent::PK];
         }
 
-        $filters->filtre_transactions = false;
-        if (isset($request->getQueryParams()['max_amount'])) {
-            $filters->filtre_transactions = true;
-            $filters->max_amount = (int)$request->getQueryParams()['max_amount'];
+        if ($type === 'contributions') {
+            if (isset($request->getQueryParams()['max_amount'])) {
+                $filters->filtre_transactions = true;
+                $filters->max_amount = (int)$request->getQueryParams()['max_amount'];
+            }
         }
 
         if ($option !== null) {
@@ -299,22 +458,54 @@ class ContributionsController extends CrudController
                     $filters->orderby = $value;
                     break;
                 case 'member':
-                    if (
-                        ($this->login->isAdmin()
-                        || $this->login->isStaff())
-                    ) {
-                        if ($value == 'all') {
-                            $filters->filtre_cotis_adh = null;
-                        } else {
-                            $filters->filtre_cotis_adh = $value;
-                        }
-                    }
+                    $filters->filtre_cotis_adh = ($value === 'all' ? null : $value);
                     break;
             }
         }
 
-        if (!$this->login->isAdmin() && !$this->login->isStaff()) {
-            $filters->filtre_cotis_adh = $this->login->id;
+        if (!$this->login->isAdmin() && !$this->login->isStaff() && $value != $this->login->id) {
+            if ($value === 'all' || empty($value)) {
+                $value = $this->login->id;
+            } else {
+                $member = new Adherent(
+                    $this->zdb,
+                    (int)$value,
+                    [
+                        'picture' => false,
+                        'groups' => false,
+                        'dues' => false,
+                        'parent' => true
+                    ]
+                );
+                if (
+                    !$member->hasParent() ||
+                    $member->parent->id != $this->login->id
+                ) {
+                    $value = $this->login->id;
+                    Analog::log(
+                        'Trying to display contributions for member #' . $value .
+                        ' without appropriate ACLs',
+                        Analog::WARNING
+                    );
+                }
+            }
+            $filters->filtre_cotis_children = $value;
+        }
+
+        $class = '\\Galette\\Entity\\' . ucwords(trim($raw_type, 's'));
+        $contrib = new $class($this->zdb, $this->login);
+
+        if (!$contrib->canShow($this->login)) {
+            Analog::log(
+                'Trying to display contributions without appropriate ACLs',
+                Analog::WARNING
+            );
+            return $response
+                ->withStatus(301)
+                ->withHeader(
+                    'Location',
+                    $this->routeparser->urlFor('me')
+                );
         }
 
         $class = '\\Galette\\Repository\\' . ucwords($raw_type);
@@ -327,7 +518,7 @@ class ContributionsController extends CrudController
         }
 
         //assign pagination variables to the template and add pagination links
-        $filters->setSmartyPagination($this->router, $this->view->getSmarty());
+        $filters->setSmartyPagination($this->routeparser, $this->view);
 
         $tpl_vars = [
             'page_title'        => $raw_type === 'contributions' ?
@@ -341,55 +532,95 @@ class ContributionsController extends CrudController
 
         if ($filters->filtre_cotis_adh != null) {
             $member = new Adherent($this->zdb);
+            $member->enableDep('children');
             $member->load($filters->filtre_cotis_adh);
             $tpl_vars['member'] = $member;
         }
 
+        if ($filters->filtre_cotis_children != false) {
+            $member = new Adherent(
+                $this->zdb,
+                $filters->filtre_cotis_children,
+                [
+                    'picture'   => false,
+                    'groups'    => false,
+                    'dues'      => false,
+                    'parent'    => true
+                ]
+            );
+            $tpl_vars['pmember'] = $member;
+        }
+
+        // hide column action in ajax mode
+        if ($ajax === true) {
+            $tpl_vars['no_action'] = true;
+        }
+
         // display page
         $this->view->render(
             $response,
-            'gestion_' . $raw_type . '.tpl',
+            'pages/' . $raw_type . '_list.html.twig',
             $tpl_vars
         );
         return $response;
     }
 
     /**
-     * Filtering
+     * List page for logged-in member
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param string   $type     One of 'transactions' or 'contributions'
      *
      * @return Response
      */
-    public function filter(Request $request, Response $response, array $args = []): Response
+    public function myList(Request $request, Response $response, string $type = null): Response
     {
-        $raw_type = null;
-        switch ($args['type']) {
-            case 'transactions':
-                $raw_type = 'transactions';
-                break;
-            case 'contributions':
-                $raw_type = 'contributions';
-                break;
+        return $this->list(
+            $request->withQueryParams(
+                $request->getQueryParams() + [
+                    Adherent::PK => $this->login->id
+                ]
+            ),
+            $response,
+            null,
+            null,
+            $type
+        );
+    }
+
+    /**
+     * Filtering
+     *
+     * @param Request     $request  PSR Request
+     * @param Response    $response PSR Response
+     * @param string|null $type     One of 'transactions' or 'contributions'
+     *
+     * @return Response
+     */
+    public function filter(Request $request, Response $response, string $type = null): Response
+    {
+        $ajax = false;
+        $filter_name = 'filter_' . $type;
+        if ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') {
+            $ajax = true;
+            $filter_name .= '_ajax';
         }
 
-        $type = 'filter_' . $raw_type;
         $post = $request->getParsedBody();
         $error_detected = [];
 
-        if ($this->session->$type !== null) {
-            $filters = $this->session->$type;
+        if ($this->session->$filter_name !== null) {
+            $filters = $this->session->$filter_name;
         } else {
-            $filter_class = '\\Galette\\Filters\\' . ucwords($raw_type) . 'List';
+            $filter_class = '\\Galette\\Filters\\' . ucwords($type) . 'List';
             $filters = new $filter_class();
         }
 
         if (isset($post['clear_filter'])) {
-            $filters->reinit();
+            $filters->reinit($ajax);
         } else {
-            if (isset($post['max_amount'])) {
+            if (!isset($post['max_amount'])) {
                 $filters->max_amount = null;
             }
 
@@ -399,16 +630,16 @@ class ContributionsController extends CrudController
                 $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'])) {
-                try {
-                    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'];
-                    }
-                } catch (Throwable $e) {
-                    $error_detected[] = $e->getMessage();
+                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'];
                 }
             }
 
@@ -430,7 +661,7 @@ class ContributionsController extends CrudController
             }
         }
 
-        $this->session->$type = $filters;
+        $this->session->$filter_name = $filters;
 
         if (count($error_detected) > 0) {
             //report errors
@@ -444,7 +675,52 @@ class ContributionsController extends CrudController
 
         return $response
             ->withStatus(301)
-            ->withHeader('Location', $this->router->pathFor('contributions', ['type' => $raw_type]));
+            ->withHeader('Location', $this->routeparser->urlFor('contributions', ['type' => $type]));
+    }
+
+    /**
+     * Batch actions handler
+     *
+     * @param Request  $request  PSR Request
+     * @param Response $response PSR Response
+     * @param string   $type     One of 'transactions' or 'contributions'
+     *
+     * @return Response
+     */
+    public function handleBatch(Request $request, Response $response, string $type): Response
+    {
+        $filter_name = 'filter_' . $type;
+        $post = $request->getParsedBody();
+
+        if (isset($post['entries_sel'])) {
+            $filter_class = '\\Galette\\Filters\\' . ucwords($type . 'List');
+            $filters = $this->session->$filter_name ?? new $filter_class();
+            $filters->selected = $post['entries_sel'];
+            $this->session->$filter_name = $filters;
+
+            if (isset($post['csv'])) {
+                return $response
+                    ->withStatus(301)
+                    ->withHeader('Location', $this->routeparser->urlFor('csv-contributionslist', ['type' => $type]));
+            }
+
+            if (isset($post['delete'])) {
+                return $response
+                    ->withStatus(301)
+                    ->withHeader('Location', $this->routeparser->urlFor('removeContributions', ['type' => $type]));
+            }
+
+            throw new \RuntimeException('Does not know what to batch :(');
+        } else {
+            $this->flash->addMessage(
+                'error_detected',
+                _T("No contribution was selected, please check at least one.")
+            );
+
+            return $response
+                ->withStatus(301)
+                ->withHeader('Location', $this->routeparser->urlFor('contributions', ['type' => $type]));
+        }
     }
 
     // /CRUD - Read
@@ -453,54 +729,55 @@ class ContributionsController extends CrudController
     /**
      * Edit page
      *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param Request     $request  PSR Request
+     * @param Response    $response PSR Response
+     * @param int         $id       Contribution id
+     * @param string|null $type     Contribution type
      *
      * @return Response
      */
-    public function edit(Request $request, Response $response, array $args = []): Response
+    public function edit(Request $request, Response $response, int $id, string $type = null): Response
     {
         if ($this->session->contribution !== null) {
             $contrib = $this->session->contribution;
             $this->session->contribution = null;
         } else {
-            $contrib = new Contribution($this->zdb, $this->login, (int)$args['id']);
+            $contrib = new Contribution($this->zdb, $this->login, $id);
             if ($contrib->id == '') {
                 //not possible to load contribution, exit
                 $this->flash->addMessage(
                     'error_detected',
                     str_replace(
                         '%id',
-                        $args['id'],
+                        $id,
                         _T("Unable to load contribution #%id!")
                     )
                 );
                 return $response
                     ->withStatus(301)
-                    ->withHeader('Location', $this->router->pathFor(
+                    ->withHeader('Location', $this->routeparser->urlFor(
                         'contributions',
                         ['type' => 'contributions']
                     ));
             }
         }
 
-        return $this->addEditPage($request, $response, $args, $contrib);
+        return $this->addEditPage($request, $response, $type, $contrib);
     }
 
     /**
      * Edit action
      *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param Request     $request  PSR Request
+     * @param Response    $response PSR Response
+     * @param integer     $id       Contribution id
+     * @param string|null $type     Contribution type
      *
      * @return Response
      */
-    public function doEdit(Request $request, Response $response, array $args = []): Response
+    public function doEdit(Request $request, Response $response, int $id, string $type = null): Response
     {
-        $args['action'] = 'edit';
-        return $this->store($request, $response, $args);
+        return $this->store($request, $response, 'edit', $type, $id);
     }
 
     /**
@@ -508,17 +785,25 @@ class ContributionsController extends CrudController
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
-     * @param array    $args     Request arguments
+     * @param string   $action   Action ('edit' or 'add')
+     * @param string   $type     Contribution type
+     * @param integer  $id       Contribution id
      *
      * @return Response
      */
-    public function store(Request $request, Response $response, array $args = []): Response
+    public function store(Request $request, Response $response, $action, string $type, $id = null): Response
     {
         $post = $request->getParsedBody();
-        $action = $args['action'];
+        $url_args = [
+            'action'    => $action,
+            'type'      => $type
+        ];
+        if ($id !== null) {
+            $url_args['id'] = $id;
+        }
 
         if ($action == 'edit' && isset($post['btnreload'])) {
-            $redirect_url = $this->router->pathFor($action . 'Contribution', $args);
+            $redirect_url = $this->routeparser->urlFor($action . 'Contribution', $url_args);
             $redirect_url .= '?' . Adherent::PK . '=' . $post[Adherent::PK] . '&' .
                 ContributionsTypes::PK . '=' . $post[ContributionsTypes::PK] . '&' .
                 'montant_cotis=' . $post['montant_cotis'];
@@ -528,59 +813,49 @@ class ContributionsController extends CrudController
         }
 
         $error_detected = [];
-        $redirect_url = null;
-
-        $id_cotis = null;
-        if (isset($args['id'])) {
-            $id_cotis = $args['id'];
-        }
 
         if ($this->session->contribution !== null) {
             $contrib = $this->session->contribution;
             $this->session->contribution = null;
         } else {
-            if ($id_cotis === null) {
+            if ($id === null) {
                 $contrib = new Contribution($this->zdb, $this->login);
             } else {
-                $contrib = new Contribution($this->zdb, $this->login, (int)$id_cotis);
+                $contrib = new Contribution($this->zdb, $this->login, $id);
             }
         }
 
-        // flagging required fields for first step only
-        $required = [
-            'id_type_cotis'     => 1,
-            'id_adh'            => 1,
-            'date_enreg'        => 1,
-            'montant_cotis'     => 1, //TODO: not always required, see #196
-            'date_debut_cotis'  => 1,
-            'date_fin_cotis'    => ($args['type'] === 'fee')
-        ];
         $disabled = [];
 
         // regular fields
-        $valid = $contrib->check($post, $required, $disabled);
+        $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
         if ($valid !== true) {
             $error_detected = array_merge($error_detected, $valid);
         }
 
+        // send email to member
+        if (isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
+            $contrib->setSendmail(); //flag to send creation email
+        }
+
+        //all goes well, we can proceed
         if (count($error_detected) == 0) {
-            //all goes well, we can proceed
-            if (count($error_detected) == 0) {
-                // send email to member
-                if (isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
-                    $contrib->setSendmail(); //flag to send creation email
-                }
+            $store = $contrib->store();
+            if ($store === true) {
+                $this->flash->addMessage(
+                    'success_detected',
+                    _T('Contribution has been successfully stored')
+                );
+            } else {
+                //something went wrong :'(
+                $error_detected[] = _T("An error occurred while storing the contribution.");
+            }
+        }
 
-                $store = $contrib->store();
-                if ($store === true) {
-                    $this->flash->addMessage(
-                        'success_detected',
-                        _T('Contribution has been successfully stored')
-                    );
-                } else {
-                    //something went wrong :'(
-                    $error_detected[] = _T("An error occurred while storing the contribution.");
-                }
+        if (count($error_detected) === 0) {
+            $files_res = $contrib->handleFiles($_FILES);
+            if (is_array($files_res)) {
+                $error_detected = array_merge($error_detected, $files_res);
             }
         }
 
@@ -588,16 +863,16 @@ class ContributionsController extends CrudController
             $this->session->contribution = null;
             if ($contrib->isTransactionPart() && $contrib->transaction->getMissingAmount() > 0) {
                 //new contribution
-                $redirect_url = $this->router->pathFor(
+                $redirect_url = $this->routeparser->urlFor(
                     'addContribution',
                     [
-                        'type'      => $post['contrib_type']
+                        'type'      => $post['contrib_type'] ?? $type
                     ]
                 ) . '?' . Transaction::PK . '=' . $contrib->transaction->id .
                 '&' . Adherent::PK . '=' . $contrib->member;
             } else {
                 //contributions list for member
-                $redirect_url = $this->router->pathFor(
+                $redirect_url = $this->routeparser->urlFor(
                     'contributions',
                     [
                         'type'      => 'contributions'
@@ -608,7 +883,7 @@ class ContributionsController extends CrudController
             //something went wrong.
             //store entity in session
             $this->session->contribution = $contrib;
-            $redirect_url = $this->router->pathFor($args['action'] . 'Contribution', $args);
+            $redirect_url = $this->routeparser->urlFor($action . 'Contribution', $url_args);
 
             //report errors
             foreach ($error_detected as $error) {
@@ -635,9 +910,9 @@ class ContributionsController extends CrudController
      *
      * @return string
      */
-    public function redirectUri(array $args = [])
+    public function redirectUri(array $args)
     {
-        return $this->router->pathFor('contributions', ['type' => $args['type']]);
+        return $this->routeparser->urlFor('contributions', ['type' => $args['type']]);
     }
 
     /**
@@ -647,9 +922,9 @@ class ContributionsController extends CrudController
      *
      * @return string
      */
-    public function formUri(array $args = [])
+    public function formUri(array $args)
     {
-        return $this->router->pathFor(
+        return $this->routeparser->urlFor(
             'doRemoveContribution',
             $args
         );
@@ -662,7 +937,7 @@ class ContributionsController extends CrudController
      *
      * @return string
      */
-    public function confirmRemoveTitle(array $args = [])
+    public function confirmRemoveTitle(array $args)
     {
         $raw_type = null;
 
@@ -718,4 +993,16 @@ class ContributionsController extends CrudController
 
     // /CRUD - Delete
     // /CRUD
+
+    /**
+     * Get filter name in session
+     *
+     * @param array|null $args Route arguments
+     *
+     * @return string
+     */
+    public function getFilterName(array $args = null): string
+    {
+        return 'filter_' . $args['type'];
+    }
 }