From 2f87cc7f097837055644c92d81a5a5042e7ffe92 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Tue, 20 Aug 2024 12:33:55 +0200 Subject: [PATCH 01/18] Refactor mandate contact tab queries to use APIv4 and check permissions (effectively respecting financialacls) --- CRM/Sepa/Logic/Settings.php | 10 + CRM/Sepa/Page/MandateTab.php | 320 ++++++++++++++----------- l10n/de_DE/LC_MESSAGES/sepa.po | 2 +- templates/CRM/Sepa/Page/MandateTab.tpl | 39 ++- xml/Menu/sepa.xml | 2 +- 5 files changed, 217 insertions(+), 156 deletions(-) diff --git a/CRM/Sepa/Logic/Settings.php b/CRM/Sepa/Logic/Settings.php index f59a815a..b5a6ad84 100644 --- a/CRM/Sepa/Logic/Settings.php +++ b/CRM/Sepa/Logic/Settings.php @@ -260,6 +260,16 @@ public static function isLittleBicExtensionAccessible() { } } + /** + * Whether the "Financial ACLs" Core extension is installed. The extension introduces financial type-specific + * permissions for CRUD actions on contributions, which CiviSEPA is respecting for displaying mandates, etc. + */ + public static function isFinancialaclsInstalled(): bool { + return \CRM_Extension_System::singleton() + ->getManager() + ->getStatus('financialacls') === \CRM_Extension_Manager::STATUS_INSTALLED; + } + /** * Return the ID of the contributions' 'In Progress' status. * diff --git a/CRM/Sepa/Page/MandateTab.php b/CRM/Sepa/Page/MandateTab.php index 81a89345..6e758764 100644 --- a/CRM/Sepa/Page/MandateTab.php +++ b/CRM/Sepa/Page/MandateTab.php @@ -15,6 +15,8 @@ +--------------------------------------------------------*/ use CRM_Sepa_ExtensionUtil as E; +use Civi\Api4\SepaMandate; +use Civi\Api4\Contribution; class CRM_Sepa_Page_MandateTab extends CRM_Core_Page { @@ -23,156 +25,190 @@ class CRM_Sepa_Page_MandateTab extends CRM_Core_Page { */ public function run() { CRM_Utils_System::setTitle(E::ts('SEPA Mandates')); - $contact_id = CRM_Utils_Request::retrieve('cid', 'Integer'); + $contactId = CRM_Utils_Request::retrieve('cid', 'Integer'); $this->assign('date_format', '%Y-%m-%d'); - $this->assign('contact_id', $contact_id); - - - // ============================== - // == OOFF == - // ============================== - $ooff_list = array(); - $ooff_query = " - SELECT - civicrm_sdd_mandate.id AS mandate_id, - civicrm_contribution.id AS contribution_id, - civicrm_contribution.receive_date AS receive_date, - civicrm_sdd_mandate.status AS status, - civicrm_sdd_mandate.reference AS reference, - civicrm_financial_type.name AS financial_type, - civicrm_campaign.title AS campaign, - civicrm_contribution.total_amount AS total_amount, - civicrm_contribution.currency AS currency, - civicrm_contribution.cancel_reason AS cancel_reason, - IF(civicrm_sdd_mandate.status IN ('OOFF'), 'sepa-active', 'sepa-inactive') - AS class - FROM civicrm_sdd_mandate - LEFT JOIN civicrm_contribution ON civicrm_contribution.id = civicrm_sdd_mandate.entity_id - LEFT JOIN civicrm_financial_type ON civicrm_financial_type.id = civicrm_contribution.financial_type_id - LEFT JOIN civicrm_campaign ON civicrm_campaign.id = civicrm_contribution.campaign_id - WHERE civicrm_sdd_mandate.contact_id = %1 - AND civicrm_sdd_mandate.type = 'OOFF' - AND civicrm_sdd_mandate.entity_table = 'civicrm_contribution'"; - - $ooff_mandates = CRM_Core_DAO::executeQuery($ooff_query, - array( 1 => array($contact_id, 'Integer'))); - - while ($ooff_mandates->fetch()) { - $ooff = array( - 'receive_date' => $ooff_mandates->receive_date, - 'status_raw' => $ooff_mandates->status, - 'status' => CRM_Sepa_Logic_Status::translateMandateStatus($ooff_mandates->status, TRUE), - 'reference' => $ooff_mandates->reference, - 'financial_type' => $ooff_mandates->financial_type, - 'campaign' => $ooff_mandates->campaign, - 'total_amount' => $ooff_mandates->total_amount, - 'currency' => $ooff_mandates->currency, - 'cancel_reason' => $ooff_mandates->cancel_reason, - 'class' => $ooff_mandates->class, - ); - - // add links - $ooff['view_link'] = CRM_Utils_System::url('civicrm/contact/view/contribution', "reset=1&id={$ooff_mandates->contribution_id}&cid={$contact_id}&action=view&context=contribution"); - if (CRM_Core_Permission::check('edit sepa mandates')) { - $ooff['edit_link'] = CRM_Utils_System::url('civicrm/sepa/xmandate', "mid={$ooff_mandates->mandate_id}"); - } + $this->assign('contact_id', $contactId); + $this->assign( + 'financialacls', + \CRM_Extension_System::singleton() + ->getManager() + ->getStatus('financialacls') === \CRM_Extension_Manager::STATUS_INSTALLED + ); + $this->assign( + 'permissions', + [ + 'create' => CRM_Core_Permission::check('create sepa mandates'), + 'view' => CRM_Core_Permission::check('view sepa mandates'), + 'edit' => CRM_Core_Permission::check('edit sepa mandates'), + 'delete' => CRM_Core_Permission::check('delete sepa mandates'), + ] + ); - $ooff_list[] = $ooff; + // Retrieve OOFF mandates. + $ooffList = []; + $ooffMandates = SepaMandate::get() + ->addSelect( + 'id', + 'contribution.id', + 'contribution.receive_date', + 'status', + 'reference', + 'contribution.financial_type_id:name', + 'campaign.title', + 'contribution.total_amount', + 'contribution.currency', + 'contribution.cancel_reason' + ) + // Use INNER JOIN for Financial ACLs to correctly restrict the result. + ->addJoin( + 'Contribution AS contribution', + 'INNER', + ['entity_table', '=', '"civicrm_contribution"'], + ['entity_id', '=', 'contribution.id'] + ) + ->addJoin( + 'Campaign AS campaign', + 'LEFT', + ['campaign.id', '=', 'contribution.campaign_id'] + ) + ->addWhere('contact_id', '=', $contactId) + ->addWhere('type', '=', 'OOFF') + ->execute(); + foreach ($ooffMandates as $ooffMandate) { + $ooffList[] = [ + 'receive_date' => $ooffMandate['contribution.receive_date'], + 'status_raw' => $ooffMandate['status'], + 'status' => CRM_Sepa_Logic_Status::translateMandateStatus($ooffMandate['status'], TRUE), + 'reference' => $ooffMandate['reference'], + 'financial_type' => $ooffMandate['contribution.financial_type_id:name'], + 'campaign' => $ooffMandate['campaign.title'], + 'total_amount' => $ooffMandate['contribution.total_amount'], + 'currency' => $ooffMandate['contribution.currency'], + 'cancel_reason' => $ooffMandate['contribution.cancel_reason'], + 'class' => 'OOFF' === $ooffMandate['status'] ? 'sepa-active' : 'sepa-inactive', + 'view_link' => CRM_Utils_System::url( + 'civicrm/contact/view/contribution', + "reset=1&id={$ooffMandate['contribution.id']}&cid={$contactId}&action=view&context=contribution" + ), + 'edit_link' => CRM_Utils_System::url( + 'civicrm/sepa/xmandate', + "mid={$ooffMandate['id']}" + ), + ]; } - $this->assign('ooffs', $ooff_list); - - - // ============================== - // == RCUR == - // ============================== - $rcur_list = array(); - $rcur_query = " - SELECT - civicrm_sdd_mandate.id AS mandate_id, - civicrm_contribution_recur.id AS rcur_id, - civicrm_contribution_recur.start_date AS start_date, - civicrm_contribution_recur.end_date AS end_date, - civicrm_contribution_recur.next_sched_contribution_date AS next_collection_date, - last.receive_date AS last_collection_date, - last.cancel_reason AS last_cancel_reason, - civicrm_sdd_mandate.status AS status, - civicrm_sdd_mandate.reference AS reference, - cancel_reason.note AS cancel_reason, - civicrm_financial_type.name AS financial_type, - civicrm_campaign.title AS campaign, - civicrm_sdd_mandate.reference AS reference, - civicrm_contribution_recur.frequency_interval AS frequency_interval, - civicrm_contribution_recur.frequency_unit AS frequency_unit, - civicrm_contribution_recur.cycle_day AS cycle_day, - civicrm_contribution_recur.currency AS currency, - civicrm_contribution_recur.amount AS amount, - IF(civicrm_sdd_mandate.status IN ('FRST', 'RCUR'), 'sepa-active', 'sepa-inactive') - AS class - FROM civicrm_sdd_mandate - LEFT JOIN civicrm_contribution_recur ON civicrm_contribution_recur.id = civicrm_sdd_mandate.entity_id - LEFT JOIN civicrm_financial_type ON civicrm_financial_type.id = civicrm_contribution_recur.financial_type_id - LEFT JOIN civicrm_campaign ON civicrm_campaign.id = civicrm_contribution_recur.campaign_id - LEFT JOIN civicrm_contribution last ON last.receive_date = (SELECT MAX(receive_date) FROM civicrm_contribution - WHERE contribution_recur_id = civicrm_contribution_recur.id - AND contribution_status_id != 2) - LEFT JOIN civicrm_note cancel_reason ON cancel_reason.entity_id = civicrm_contribution_recur.id - AND cancel_reason.entity_table = 'civicrm_contribution_recur' - AND cancel_reason.subject = 'cancel_reason' - WHERE civicrm_sdd_mandate.contact_id = %1 - AND civicrm_sdd_mandate.type = 'RCUR' - AND civicrm_sdd_mandate.entity_table = 'civicrm_contribution_recur' - GROUP BY civicrm_sdd_mandate.id - ORDER BY civicrm_contribution_recur.start_date DESC, civicrm_sdd_mandate.id DESC;"; - - $mandate_ids = array(); - - CRM_Core_DAO::disableFullGroupByMode(); - $rcur_mandates = CRM_Core_DAO::executeQuery($rcur_query, - array(1 => array($contact_id, 'Integer')) - ); - CRM_Core_DAO::reenableFullGroupByMode(); - - while ($rcur_mandates->fetch()) { - $rcur = array( - 'mandate_id' => $rcur_mandates->mandate_id, - 'start_date' => $rcur_mandates->start_date, - 'cycle_day' => $rcur_mandates->cycle_day, - 'status_raw' => $rcur_mandates->status, - 'reference' => $rcur_mandates->reference, - 'financial_type' => $rcur_mandates->financial_type, - 'campaign' => $rcur_mandates->campaign, - 'status' => CRM_Sepa_Logic_Status::translateMandateStatus($rcur_mandates->status, TRUE), - 'frequency' => CRM_Utils_SepaOptionGroupTools::getFrequencyText($rcur_mandates->frequency_interval, $rcur_mandates->frequency_unit, TRUE), - 'next_collection_date' => $rcur_mandates->next_collection_date, - 'last_collection_date' => $rcur_mandates->last_collection_date, - 'cancel_reason' => $rcur_mandates->cancel_reason, - 'last_cancel_reason' => $rcur_mandates->last_cancel_reason, - 'reference' => $rcur_mandates->reference, - 'end_date' => $rcur_mandates->end_date, - 'currency' => $rcur_mandates->currency, - 'amount' => $rcur_mandates->amount, - 'class' => $rcur_mandates->class - ); - - // calculate annual amount - if ($rcur_mandates->frequency_unit == 'year') { - $rcur['total_amount'] = $rcur_mandates->amount / $rcur_mandates->frequency_interval; - } elseif ($rcur_mandates->frequency_unit == 'month') { - $rcur['total_amount'] = $rcur_mandates->amount * 12.0 / $rcur_mandates->frequency_interval; + $this->assign('ooffs', $ooffList); + + + // Retrieve RCUR mandates. + $rcurList = []; + $rcurMandates = SepaMandate::get() + ->addSelect( + 'id', + 'contribution_recur.id', + 'contribution_recur.start_date', + 'contribution_recur.end_date', + 'contribution_recur.next_sched_contribution_date', + 'last_contribution.cancel_reason', + 'status', + 'reference', + 'GROUP_FIRST(cancel_reason.note) AS cancel_reason', + 'contribution_recur.financial_type_id:name', + 'campaign.title', + 'contribution_recur.frequency_interval', + 'contribution_recur.frequency_unit', + 'contribution_recur.cycle_day', + 'contribution_recur.currency', + 'contribution_recur.amount' + ) + // Use INNER JOIN for Financial ACLs to correctly restrict the result. + ->addJoin( + 'ContributionRecur AS contribution_recur', + 'INNER', + ['entity_id', '=', 'contribution_recur.id'], + ['entity_table', '=', '"civicrm_contribution_recur"'] + ) + ->addJoin( + 'Note AS cancel_reason', + 'LEFT', + ['cancel_reason.entity_id', '=', 'contribution_recur.id'], + ['cancel_reason.entity_table', '=', '"civicrm_contribution_recur"'], + ['cancel_reason.subject', '=', '"cancel_reason"'] + ) + ->addJoin( + 'Campaign AS campaign', + 'LEFT', + ['campaign.id', '=', 'contribution_recur.campaign_id'] + ) + ->addWhere('contact_id', '=', $contactId) + ->addWhere('type', '=', 'RCUR') + ->addWhere('entity_table', '=', 'civicrm_contribution_recur') + ->addOrderBy('contribution_recur.start_date', 'DESC') + ->addOrderBy('id', 'DESC') + ->addGroupBy('id') + ->execute(); + + foreach ($rcurMandates as $rcurMandate) { + $lastInstallment = Contribution::get() + ->addSelect('receive_date', 'cancel_reason') + ->addWhere('contribution_recur_id', '=', $rcurMandate['contribution_recur.id']) + ->addWhere('contribution_status_id:name', '!=', 'Pending') + ->addOrderBy('receive_date', 'DESC') + ->setLimit(1) + ->execute(); + $rcurRow = [ + 'mandate_id' => $rcurMandate['id'], + 'start_date' => $rcurMandate['contribution_recur.start_date'], + 'cycle_day' => $rcurMandate['contribution_recur.cycle_day'], + 'status_raw' => $rcurMandate['status'], + 'reference' => $rcurMandate['reference'], + 'financial_type' => $rcurMandate['contribution_recur.financial_type_id:name'], + 'campaign' => $rcurMandate['campaign.title'], + 'status' => CRM_Sepa_Logic_Status::translateMandateStatus($rcurMandate['status'], TRUE), + 'frequency' => CRM_Utils_SepaOptionGroupTools::getFrequencyText( + $rcurMandate['contribution_recur.frequency_interval'], + $rcurMandate['contribution_recur.frequency_unit'], + TRUE + ), + 'next_collection_date' => $rcurMandate['contribution_recur.next_sched_contribution_date'], + 'last_collection_date' => $lastInstallment->first()['receive_date'] ?? NULL, + 'cancel_reason' => $rcur_mandates['cancel_reason'], + 'last_cancel_reason' => $lastInstallment->first()['cancel_reason'] ?? NULL, + 'end_date' => $rcurMandate['contribution_recur.end_date'], + 'currency' => $rcurMandate['contribution_recur.currency'], + 'amount' => $rcurMandate['contribution_recur.amount'], + 'class' => in_array($rcurMandate['status'], ['FRST', 'RCUR']) + ? 'sepa-active' + : 'sepa-inactive', + ]; + + // Calculate annual amount. + if ('year' === $rcurMandate['contribution_recur.frequency_unit']) { + $rcurRow['total_amount'] = + $rcurMandate['contribution_recur.amount'] / $rcurMandate['contribution_recur.frequency_interval']; + } elseif ('month' === $rcurMandate['contribution_recur.frequency_unit']) { + $rcurRow['total_amount'] = + $rcurMandate['contribution_recur.amount'] * 12.0 / $rcurMandate['contribution_recur.frequency_interval']; } - // add links - $rcur['view_link'] = CRM_Utils_System::url('civicrm/contact/view/contributionrecur', "reset=1&id={$rcur_mandates->rcur_id}&cid={$contact_id}&context=contribution"); + // Add links. + $rcurRow['view_link'] = CRM_Utils_System::url( + 'civicrm/contact/view/contributionrecur', + "reset=1&id={$rcurMandate['contribution_recur.id']}&cid={$contactId}&context=contribution"); if (CRM_Core_Permission::check('edit sepa mandates')) { - $rcur['edit_link'] = CRM_Utils_System::url('civicrm/sepa/xmandate', "mid={$rcur_mandates->mandate_id}"); + $rcurRow['edit_link'] = CRM_Utils_System::url('civicrm/sepa/xmandate', "mid={$rcurMandate['id']}"); } - $rcur_list[$rcur_mandates->mandate_id] = $rcur; + $rcurList[$rcurMandate['id']] = $rcurRow; } - // add cancellation info - if (!empty($rcur_list)) { - $mandate_id_list = implode(',', array_keys($rcur_list)); + // Add cancellation info. + // TODO: Transform into APIv4 query. + // This currently generates a string of "0" (for pending/in progress/completed contributions) and "1" for + // other status and counts trailing "1"s, passing the number of failed last installments to the template. + // As this does not disclose contribution information that has not yet been fetched via the API, no additional + // ACL bypassing is being done here. + if (!empty($rcurList)) { + $mandate_id_list = implode(',', array_keys($rcurList)); $fail_sequence = " SELECT civicrm_sdd_mandate.id AS mandate_id, @@ -196,12 +232,12 @@ public function run() { while ($fail_query->fetch()) { if (preg_match("#(?1+)$#", $fail_query->fail_sequence, $match)) { $last_sequence = $match['last_fails']; - $rcur_list[$fail_query->mandate_id]['fail_sequence'] = strlen($last_sequence); + $rcurList[$fail_query->mandate_id]['fail_sequence'] = strlen($last_sequence); } } } - $this->assign('rcurs', $rcur_list); + $this->assign('rcurs', $rcurList); parent::run(); } diff --git a/l10n/de_DE/LC_MESSAGES/sepa.po b/l10n/de_DE/LC_MESSAGES/sepa.po index 73303356..6b25d169 100644 --- a/l10n/de_DE/LC_MESSAGES/sepa.po +++ b/l10n/de_DE/LC_MESSAGES/sepa.po @@ -1964,7 +1964,7 @@ msgstr "Lege SEPA-Mandate an" #: sepa.php msgid "View SEPA mandates" -msgstr "SEPA-Mandtae ansehen" +msgstr "SEPA-Mandate ansehen" #: sepa.php msgid "Edit SEPA mandates" diff --git a/templates/CRM/Sepa/Page/MandateTab.tpl b/templates/CRM/Sepa/Page/MandateTab.tpl index d5d60af3..451a53b4 100644 --- a/templates/CRM/Sepa/Page/MandateTab.tpl +++ b/templates/CRM/Sepa/Page/MandateTab.tpl @@ -21,11 +21,13 @@ {/literal} {* add new mandate button *} -
-
{ts domain="org.project60.sepa"}Add new SEPA Mandate{/ts}
-
-
-
+{if $permissions.create} +
+
{ts domain="org.project60.sepa"}Add new SEPA Mandate{/ts}
+
+
+
+{/if} {if $rcurs}

{ts domain="org.project60.sepa"}Recurring SEPA Mandates{/ts}

@@ -56,15 +58,20 @@ {$rcur.total_amount|crmMoney:$rcur.currency} {$rcur.last_collection_date|crmDate:$date_format} - {foreach from=$rcur.fail_sequence item=fail} -
- {/foreach} + {* Show as many warnings as last installments have failed. *} + {if $rcur.fail_sequence} + {for $i=1 to $rcur.fail_sequence} +
+ {/for} + {/if} {$rcur.next_collection_date|crmDate:$date_format} {$rcur.end_date|crmDate:$date_format} - {ts domain="org.project60.sepa"}View{/ts} - {if $rcur.edit_link} + {if $permissions.view} + {ts domain="org.project60.sepa"}View{/ts} + {/if} + {if $permissions.edit && $rcur.edit_link} {ts domain="org.project60.sepa"}Edit{/ts} {/if} @@ -76,6 +83,9 @@ {else}
{ts domain="org.project60.sepa"}This contact has no recorded recurring mandates.{/ts} +{if $financialacls} + {ts domain="org.project60.sepa"}Note that only mandates associated with contributions of authorized financial types are being displayed.{/ts} +{/if}
{/if} @@ -103,8 +113,10 @@ {$ooff.total_amount|crmMoney:$ooff.currency} - {ts domain="org.project60.sepa"}View{/ts} - {if $ooff.edit_link} + {if $permissions.view} + {ts domain="org.project60.sepa"}View{/ts} + {/if} + {if $permissions.edit && $ooff.edit_link} {ts domain="org.project60.sepa"}Edit{/ts} {/if} @@ -116,6 +128,9 @@ {else}
{ts domain="org.project60.sepa"}This contact has no recorded one-off mandates.{/ts} +{if $financialacls} + {ts domain="org.project60.sepa"}Note that only mandates associated with contributions of authorized financial types are being displayed.{/ts} +{/if}
{/if} diff --git a/xml/Menu/sepa.xml b/xml/Menu/sepa.xml index b2e561f3..4b835d0e 100644 --- a/xml/Menu/sepa.xml +++ b/xml/Menu/sepa.xml @@ -79,7 +79,7 @@ civicrm/sepa/tab CRM_Sepa_Page_MandateTab SEPA Mandates - access CiviCRM + view sepa mandates civicrm/sepa/retry From c06afc07f2eb3dde304e172b06a928e92f10c5e8 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Tue, 20 Aug 2024 13:26:52 +0200 Subject: [PATCH 02/18] Validate financial types when creating mandates --- CRM/Sepa/Form/CreateMandate.php | 37 ++---------------- Civi/Sepa/Util/ContributionUtil.php | 58 +++++++++++++++++++++++++++++ api/v3/SepaMandate.php | 8 ++++ 3 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 Civi/Sepa/Util/ContributionUtil.php diff --git a/CRM/Sepa/Form/CreateMandate.php b/CRM/Sepa/Form/CreateMandate.php index 77395528..cc448f9a 100644 --- a/CRM/Sepa/Form/CreateMandate.php +++ b/CRM/Sepa/Form/CreateMandate.php @@ -13,6 +13,8 @@ | copyright header is strictly prohibited without | | written permission from the original author(s). | +--------------------------------------------------------*/ + +use Civi\Sepa\Util\ContributionUtil; use CRM_Sepa_ExtensionUtil as E; /** @@ -111,7 +113,7 @@ public function buildQuickForm() { 'select', 'payment_instrument_id', E::ts('Payment Method'), - $this->getPaymentInstrumentList(), + ContributionUtil::getPaymentInstrumentList(), TRUE, array('class' => 'crm-select2') ); @@ -121,7 +123,7 @@ public function buildQuickForm() { 'select', 'financial_type_id', E::ts('Financial Type'), - $this->getFinancialTypeList(), + ContributionUtil::getFinancialTypeList(), TRUE, array('class' => 'crm-select2') ); @@ -572,37 +574,6 @@ protected function getCreditorList($creditors) { return $creditor_list; } - /** - * Get the list of (active) financial types - */ - protected function getFinancialTypeList() { - $list = array(); - $query = civicrm_api3('FinancialType', 'get',array( - 'is_active' => 1, - 'option.limit' => 0, - 'return' => 'id,name' - )); - - foreach ($query['values'] as $value) { - $list[$value['id']] = $value['name']; - } - - return $list; - } - - /** - * Get the list of (CiviSEPA) payment instruments - */ - protected function getPaymentInstrumentList() { - $list = array(); - $payment_instruments = CRM_Sepa_Logic_PaymentInstruments::getAllSddPaymentInstruments(); - foreach ($payment_instruments as $payment_instrument) { - $list[$payment_instrument['id']] = $payment_instrument['label']; - } - - return $list; - } - /** * Get the list of (active) financial types */ diff --git a/Civi/Sepa/Util/ContributionUtil.php b/Civi/Sepa/Util/ContributionUtil.php new file mode 100644 index 00000000..887dbaec --- /dev/null +++ b/Civi/Sepa/Util/ContributionUtil.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Sepa\Util; + +use Civi\Api4\FinancialType; + +class ContributionUtil { + + /** + * Retrieves the list of (CiviSEPA) payment instruments. + * + * @return array + * An array of payment instrument labels, keyed by their ID. + */ + public static function getPaymentInstrumentList(): array { + $list = []; + $payment_instruments = \CRM_Sepa_Logic_PaymentInstruments::getAllSddPaymentInstruments(); + foreach ($payment_instruments as $payment_instrument) { + $list[$payment_instrument['id']] = $payment_instrument['label']; + } + + return $list; + } + + /** + * Retrieves the list of (active) financial types. + * + * @return array + * An array of financial type names, keyed by their ID. + */ + public static function getFinancialTypeList(): array { + // Check permissions for financial types for evaluating Financial ACLs. + return FinancialType::get() + ->addSelect('id', 'name') + ->addWhere('is_active', '=', TRUE) + ->execute() + ->indexBy('id') + ->column('name'); +} + +} diff --git a/api/v3/SepaMandate.php b/api/v3/SepaMandate.php index 5a29ec52..84f17cb5 100644 --- a/api/v3/SepaMandate.php +++ b/api/v3/SepaMandate.php @@ -111,6 +111,14 @@ function civicrm_api3_sepa_mandate_createfull($params) { } } + // Validate financial type. + if ( + is_numeric($params['financial_type_id'] ?? NULL) + && !array_key_exists($params['financial_type_id'], \Civi\Sepa\Util\ContributionUtil::getFinancialTypeList()) + ) { + throw new CRM_Core_Exception("No permission for creating SEPA mandates with financial type [{$params['financial_type_id']}]."); + } + // if BIC is used for this creditor, it is required (see #245) if (empty($params['bic'])) { if ($creditor['uses_bic']) { From 165af167a84e4c885a27ad2a1d7e440ae0f55ed7 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Tue, 20 Aug 2024 15:03:08 +0200 Subject: [PATCH 03/18] Add API4 Get action for SEPA mandates checking Financial ACLs --- CRM/Sepa/Form/CreateMandate.php | 18 +++++-- CRM/Sepa/Page/MandateTab.php | 2 - Civi/Api4/SepaMandate.php | 11 +++- .../Api4/Action/SepaMandate/GetAction.php | 54 +++++++++++++++++++ 4 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 Civi/Sepa/Api4/Action/SepaMandate/GetAction.php diff --git a/CRM/Sepa/Form/CreateMandate.php b/CRM/Sepa/Form/CreateMandate.php index cc448f9a..dbe80035 100644 --- a/CRM/Sepa/Form/CreateMandate.php +++ b/CRM/Sepa/Form/CreateMandate.php @@ -54,9 +54,21 @@ public function buildQuickForm() { } // load mandate - $this->old_mandate = civicrm_api3('SepaMandate', 'getsingle', array('id' => $mandate_id)); + try { + // API4 SepaMandate.get checks Financial ACLs for corresponding (recurring) contribution. + $this->old_mandate = \Civi\Api4\SepaMandate::get() + ->addWhere('id', '=', $mandate_id) + ->execute() + ->single(); + } + catch (\Exception $e) { + Civi::log()->error($e->getMessage()); + CRM_Core_Error::statusBounce( + E::ts('The mandate to clone/replace from does not exist or you do not have permission for it.'), + ); + } if ($this->old_mandate['type'] != 'RCUR') { - CRM_Core_Error::fatal(E::ts("You can only replace RCUR mandates")); + CRM_Core_Error::statusBounce(E::ts('You can only replace RCUR mandates')); } $this->contact_id = (int) $this->old_mandate['contact_id']; @@ -64,7 +76,7 @@ public function buildQuickForm() { } if (empty($this->contact_id)) { - CRM_Core_Error::fatal(E::ts("No contact ID (cid) given.")); + CRM_Core_Error::statusBounce(E::ts("No contact ID (cid) given.")); } // load the contact and set the title diff --git a/CRM/Sepa/Page/MandateTab.php b/CRM/Sepa/Page/MandateTab.php index 6e758764..1abc72fb 100644 --- a/CRM/Sepa/Page/MandateTab.php +++ b/CRM/Sepa/Page/MandateTab.php @@ -59,7 +59,6 @@ public function run() { 'contribution.currency', 'contribution.cancel_reason' ) - // Use INNER JOIN for Financial ACLs to correctly restrict the result. ->addJoin( 'Contribution AS contribution', 'INNER', @@ -120,7 +119,6 @@ public function run() { 'contribution_recur.currency', 'contribution_recur.amount' ) - // Use INNER JOIN for Financial ACLs to correctly restrict the result. ->addJoin( 'ContributionRecur AS contribution_recur', 'INNER', diff --git a/Civi/Api4/SepaMandate.php b/Civi/Api4/SepaMandate.php index 8f483fe0..74db2efe 100644 --- a/Civi/Api4/SepaMandate.php +++ b/Civi/Api4/SepaMandate.php @@ -1,6 +1,8 @@ setCheckPermissions($checkPermissions); + } + /** * @see \Civi\Api4\Generic\AbstractEntity::permissions() * @return array[] @@ -20,5 +27,5 @@ public static function permissions(): array { 'update' => ['edit sepa mandates'], ]; } - + } diff --git a/Civi/Sepa/Api4/Action/SepaMandate/GetAction.php b/Civi/Sepa/Api4/Action/SepaMandate/GetAction.php new file mode 100644 index 00000000..bbb207a6 --- /dev/null +++ b/Civi/Sepa/Api4/Action/SepaMandate/GetAction.php @@ -0,0 +1,54 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Sepa\Api4\Action\SepaMandate; + +use Civi\Api4\Generic\DAOGetAction; +use Civi\Api4\Generic\Result; + +class GetAction extends DAOGetAction { + + public function _run(Result $result) { + // Add unique joins for permission checks of Financial ACLs. + if ($this->getCheckPermissions()) { + $contributionAlias = uniqid('contribution_'); + $contributionRecurAlias = uniqid('contribution_recur_'); + $this + ->addJoin( + 'Contribution AS ' . $contributionAlias, + 'LEFT', + ['entity_table', '=', '"civicrm_contribution"'], + ['entity_id', '=', $contributionAlias . '.id'] + ) + ->addJoin( + 'ContributionRecur AS ' . $contributionRecurAlias, + 'LEFT', + ['entity_table', '=', '"civicrm_contribution_recur"'], + ['entity_id', '=', $contributionRecurAlias . '.id'] + ) + ->addClause( + 'OR', + ['AND', [['type', '=', 'OOFF'], [$contributionAlias . '.id', 'IS NOT NULL']]], + ['AND', [['type', '=', 'RCUR'], [$contributionRecurAlias . '.id', 'IS NOT NULL']]] + ); + } + return parent::_run($result); + } + +} From 0d1de98a941674977fa0ae9394e9669dc6693d8f Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Thu, 22 Aug 2024 12:49:42 +0200 Subject: [PATCH 04/18] Do not evaluate $_REQUEST superglobal directly in CiviSEPA dashboard --- CRM/Sepa/Page/DashBoard.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CRM/Sepa/Page/DashBoard.php b/CRM/Sepa/Page/DashBoard.php index 84a3b073..33f775f1 100644 --- a/CRM/Sepa/Page/DashBoard.php +++ b/CRM/Sepa/Page/DashBoard.php @@ -31,13 +31,8 @@ class CRM_Sepa_Page_DashBoard extends CRM_Core_Page { function run() { CRM_Utils_System::setTitle(ts('CiviSEPA Dashboard', array('domain' => 'org.project60.sepa'))); // get requested group status - if (isset($_REQUEST['status'])) { - if ($_REQUEST['status'] != 'open' && $_REQUEST['status'] != 'closed') { - $status = 'open'; - } else { - $status = $_REQUEST['status']; - } - } else { + $status = CRM_Utils_Request::retrieve('status', 'String'); + if ('open' !== $status && 'closed' !== $status) { $status = 'open'; } From d67ce178fa378026c9916e7a835ce9a342af7868 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Thu, 22 Aug 2024 15:08:29 +0200 Subject: [PATCH 05/18] Add and use API4 Get action for SEPA transaction groups checking Financial ACLs --- CRM/Sepa/Page/DashBoard.php | 105 ++- Civi/Api4/SepaContributionGroup.php | 33 + Civi/Api4/SepaCreditor.php | 4 +- Civi/Api4/SepaMandate.php | 4 +- Civi/Api4/SepaSddFile.php | 4 +- Civi/Api4/SepaTransactionGroup.php | 11 +- .../Action/SepaTransactionGroup/GetAction.php | 41 ++ api/v3/SepaTransactionGroup.php | 3 + l10n/de_DE/LC_MESSAGES/sepa.mo | Bin 66079 -> 72598 bytes l10n/de_DE/LC_MESSAGES/sepa.po | 690 ++++++++++++------ l10n/org.project60.sepa.pot | 10 +- templates/CRM/Sepa/Page/DashBoard.tpl | 6 + 12 files changed, 666 insertions(+), 245 deletions(-) create mode 100644 Civi/Api4/SepaContributionGroup.php create mode 100644 Civi/Sepa/Api4/Action/SepaTransactionGroup/GetAction.php diff --git a/CRM/Sepa/Page/DashBoard.php b/CRM/Sepa/Page/DashBoard.php index 33f775f1..7129f98d 100644 --- a/CRM/Sepa/Page/DashBoard.php +++ b/CRM/Sepa/Page/DashBoard.php @@ -21,7 +21,7 @@ * */ -require_once 'CRM/Core/Page.php'; +use CRM_Sepa_ExtensionUtil as E; class CRM_Sepa_Page_DashBoard extends CRM_Core_Page { @@ -47,6 +47,12 @@ function run() { // check permissions $this->assign('can_delete', CRM_Core_Permission::check('delete sepa groups')); $this->assign('can_batch', CRM_Core_Permission::check('batch sepa groups')); + $this->assign( + 'financialacls', + \CRM_Extension_System::singleton() + ->getManager() + ->getStatus('financialacls') === \CRM_Extension_Manager::STATUS_INSTALLED + ); if (isset($_REQUEST['update'])) { $this->callBatcher($_REQUEST['update']); @@ -83,38 +89,81 @@ function run() { $this->assign('closed_status_id', CRM_Core_PseudoConstant::getKey('CRM_Batch_BAO_Batch', 'status_id', 'Closed')); // now read the details - $result = civicrm_api("SepaTransactionGroup", "getdetail", array( - "version" => 3, - "sequential" => 1, - "status_ids" => implode(',', $status_list[$status]), - "order_by" => (($status=='open')?'latest_submission_date':'file.created_date'), - )); - if (isset($result['is_error']) && $result['is_error']) { - CRM_Core_Session::setStatus(sprintf(ts("Couldn't read transaction groups. Error was: '%s'", array('domain' => 'org.project60.sepa')), $result['error_message']), ts('Error', array('domain' => 'org.project60.sepa')), 'error'); - } else { + try { + // API4 SepaTransactionGroup.get checks Financial ACLs for corresponding contributions. + $sepaTransactionGroups = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->selectRowCount() + ->addSelect( + 'id', + 'reference', + 'sdd_file_id', + 'type', + 'collection_date', + 'latest_submission_date', + 'status_id', + 'sdd_creditor_id', + 'sepa_sdd_file.created_date', + 'SUM(contribution.total_amount) AS total', + 'COUNT(*) AS nb_contrib', + 'contribution.currency', + 'sepa_sdd_file.reference' + ) + ->addJoin( + 'Contribution AS contribution', + 'LEFT', + 'SepaContributionGroup' + ) + ->addJoin( + 'SepaSddFile AS sepa_sdd_file', + 'LEFT', + ['sdd_file_id', '=', 'sepa_sdd_file.id'] + ) + ->addWhere('status_id', 'IN', $status_list[$status]) + ->addOrderBy('open' === $status ? 'latest_submission_date' : 'sepa_sdd_file.created_date') + ->addGroupBy('id') + ->addGroupBy('reference') + ->addGroupBy('sdd_file_id') + ->addGroupBy('type') + ->addGroupBy('collection_date') + ->addGroupBy('latest_submission_date') + ->addGroupBy('status_id') + ->addGroupBy('sdd_creditor_id') + ->addGroupBy('sepa_sdd_file.created_date') + ->addGroupBy('contribution.currency') + ->addGroupBy('sepa_sdd_file.reference') + ->execute(); $groups = []; $now = date('Y-m-d'); - foreach ($result["values"] as $id => $group) { - // 'beautify' + foreach ($sepaTransactionGroups as $group) { $group['latest_submission_date'] = date('Y-m-d', strtotime($group['latest_submission_date'])); $group['collection_date'] = date('Y-m-d', strtotime($group['collection_date'])); $group['collection_date_in_future'] = ($group['collection_date'] > $now) ? 1 : 0; $group['status'] = $status_2_title[$group['status_id']]; + $group['currency'] = $group['contribution.currency']; + $group['file_id'] = $group['sdd_file_id']; + $group['file_created_date'] = $group['sepa_sdd_file.created_date']; + $group['creditor_id'] = $group['sdd_creditor_id']; + $group['file'] = $group['sepa_sdd_file.reference']; $group['file'] = $this->getFormatFilename($group); $group['status_label'] = $status2label[$group['status_id']]; - $remaining_days = (strtotime($group['latest_submission_date']) - strtotime("now")) / (60*60*24); - if ($group['status']=='closed') { + $remaining_days = (strtotime($group['latest_submission_date']) - strtotime("now")) / (60 * 60 * 24); + if ('closed' === $group['status']) { $group['submit'] = 'closed'; - } elseif ($group['type'] == 'OOFF') { + } + elseif ('OOFF' === $group['type']) { $group['submit'] = 'soon'; - } else { + } + else { if ($remaining_days <= -1) { $group['submit'] = 'missed'; - } elseif ($remaining_days <= 1) { + } + elseif ($remaining_days <= 1) { $group['submit'] = 'urgently'; - } elseif ($remaining_days <= 6) { + } + elseif ($remaining_days <= 6) { $group['submit'] = 'soon'; - } else { + } + else { $group['submit'] = 'later'; } } @@ -122,18 +171,24 @@ function run() { $group['transaction_message'] = CRM_Sepa_BAO_SEPATransactionGroup::getCustomGroupTransactionMessage($group['id']); $group['transaction_note'] = CRM_Sepa_BAO_SEPATransactionGroup::getNote($group['id']); - array_push($groups, $group); + $groups[] = $group; } - $this->assign("groups", $groups); + $this->assign('groups', $groups); + } + catch (CRM_Core_Exception $exception) { + CRM_Core_Session::setStatus( + E::ts( + "Couldn't read transaction groups. Error was: %1", + [1 => $result['error_message']] + ), + E::ts('Error'), + 'error' + ); } parent::run(); } - function getTemplateFileName() { - return "CRM/Sepa/Page/DashBoard.tpl"; - } - /** * call the batching API */ diff --git a/Civi/Api4/SepaContributionGroup.php b/Civi/Api4/SepaContributionGroup.php new file mode 100644 index 00000000..58253f2d --- /dev/null +++ b/Civi/Api4/SepaContributionGroup.php @@ -0,0 +1,33 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Api4; + +use Civi\Api4\Generic\DAOEntity; + +/** + * SepaContributionGroup entity. + * + * Provided by the CiviSEPA extension. + * + * @package Civi\Api4 + */ +class SepaContributionGroup extends Generic\DAOEntity { + use Generic\Traits\EntityBridge; +} diff --git a/Civi/Api4/SepaCreditor.php b/Civi/Api4/SepaCreditor.php index c0424e65..f9f7f486 100644 --- a/Civi/Api4/SepaCreditor.php +++ b/Civi/Api4/SepaCreditor.php @@ -2,9 +2,9 @@ namespace Civi\Api4; /** - * Resource entity. + * SepaCreditor entity. * - * Provided by the CiviCRM Resource Management extension. + * Provided by the CiviSEPA extension. * * @package Civi\Api4 */ diff --git a/Civi/Api4/SepaMandate.php b/Civi/Api4/SepaMandate.php index 74db2efe..386f0500 100644 --- a/Civi/Api4/SepaMandate.php +++ b/Civi/Api4/SepaMandate.php @@ -4,9 +4,9 @@ use Civi\Sepa\Api4\Action\SepaMandate\GetAction; /** - * Resource entity. + * SepaMandate entity. * - * Provided by the CiviCRM Resource Management extension. + * Provided by the CiviSEPA extension. * * @package Civi\Api4 */ diff --git a/Civi/Api4/SepaSddFile.php b/Civi/Api4/SepaSddFile.php index 0940b1b0..c681f15d 100644 --- a/Civi/Api4/SepaSddFile.php +++ b/Civi/Api4/SepaSddFile.php @@ -2,9 +2,9 @@ namespace Civi\Api4; /** - * Resource entity. + * SepaSddFile entity. * - * Provided by the CiviCRM Resource Management extension. + * Provided by the CiviSEPA extension. * * @package Civi\Api4 */ diff --git a/Civi/Api4/SepaTransactionGroup.php b/Civi/Api4/SepaTransactionGroup.php index f97c08db..483ebb5a 100644 --- a/Civi/Api4/SepaTransactionGroup.php +++ b/Civi/Api4/SepaTransactionGroup.php @@ -1,13 +1,20 @@ setCheckPermissions($checkPermissions); + } + } diff --git a/Civi/Sepa/Api4/Action/SepaTransactionGroup/GetAction.php b/Civi/Sepa/Api4/Action/SepaTransactionGroup/GetAction.php new file mode 100644 index 00000000..282ce680 --- /dev/null +++ b/Civi/Sepa/Api4/Action/SepaTransactionGroup/GetAction.php @@ -0,0 +1,41 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Sepa\Api4\Action\SepaTransactionGroup; + +use Civi\Api4\Generic\DAOGetAction; +use Civi\Api4\Generic\Result; + +class GetAction extends DAOGetAction { + + public function _run(Result $result) { + // Add unique joins for permission checks of Financial ACLs. + if ($this->getCheckPermissions()) { + $contributionAlias = uniqid('contribution_'); + $this + ->addJoin( + 'Contribution AS ' . $contributionAlias, + 'INNER', + 'SepaContributionGroup' + ); + } + return parent::_run($result); + } + +} diff --git a/api/v3/SepaTransactionGroup.php b/api/v3/SepaTransactionGroup.php index 59790a71..4914b864 100644 --- a/api/v3/SepaTransactionGroup.php +++ b/api/v3/SepaTransactionGroup.php @@ -84,6 +84,9 @@ function civicrm_api3_sepa_transaction_group_get($params) { return _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params); } +/** + * @deprecated This action will not be ported to APIv4. Use the "Get" action with custom joins and filters instead. + */ function civicrm_api3_sepa_transaction_group_getdetail($params) { // $where = "txgroup.id= txgroup_contrib.txgroup_id AND txgroup_contrib.contribution_id = contrib.id"; $where = "1"; diff --git a/l10n/de_DE/LC_MESSAGES/sepa.mo b/l10n/de_DE/LC_MESSAGES/sepa.mo index d28b7f2b87016c37fb221739733c86d54b0d0098..215cd11c62a14acc80233c7e07b72787e1cbd5eb 100644 GIT binary patch delta 20037 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 H8t?jFtx!6i delta 14009 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\n" "Language-Team: Project60 \n" @@ -10,7 +10,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: utf-8\n" -"X-Generator: Poedit 2.0.6\n" +"X-Generator: Poedit 3.0.1\n" #: CRM/Admin/Form/Setting/SepaSettings.php #: templates/CRM/Admin/Form/Setting/SepaSettings.tpl @@ -60,6 +60,7 @@ msgid "Please enter the %s as number (integers only)." msgstr "Bitte geben Sie %s als ganze Zahl an." #: CRM/Admin/Form/Setting/SepaSettings.php +#: Civi/Sepa/DataProcessor/Join/AbstractMandateJoin.php msgid "- select -" msgstr "- wählen -" @@ -98,12 +99,28 @@ msgstr "Adresse" msgid "Country" msgstr "Land" +#: CRM/Admin/Form/Setting/SepaSettings.php CRM/Sepa/DAO/SEPAMandate.php +#: CRM/Sepa/Form/CreateMandate.php CRM/Sepa/Form/Report/SepaMandateGeneric.php +#: CRM/Sepa/Form/Search/SepaContactSearch.php CRM/Utils/SepaTokens.php +#: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php +#: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php +#: Civi/Sepa/ActionProvider/Action/FindMandate.php +#: templates/CRM/Contribute/Form/ContributionConfirm.sepa.tpl +#: templates/CRM/Contribute/Form/ContributionThankYou.sepa.tpl +#: templates/CRM/Event/Form/RegistrationConfirm.sepa.tpl +#: templates/CRM/Event/Form/RegistrationThankYou.sepa.tpl +#: templates/CRM/Sepa/Page/EditMandate.tpl +#: templates/Sepa/Contribute/Form/ContributionView.tpl +#: templates/Sepa/Contribute/Page/ContributionRecur.tpl +msgid "Account Holder" +msgstr "Kontoinhaber" + #: CRM/Admin/Form/Setting/SepaSettings.php CRM/Sepa/Form/CreateMandate.php #: CRM/Sepa/Form/Report/SepaMandateGeneric.php -#: CRM/Sepa/Form/Search/SepaContactSearch.php +#: CRM/Sepa/Form/Search/SepaContactSearch.php CRM/Utils/SepaTokens.php #: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php #: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php -#: Civi/Sepa/ActionProvider/Action/FindMandate.php js/CreateMandate.js sepa.php +#: Civi/Sepa/ActionProvider/Action/FindMandate.php js/CreateMandate.js #: templates/CRM/Admin/Form/Setting/SepaSettings.tpl #: templates/CRM/Contribute/Form/ContributionConfirm.sepa.tpl #: templates/CRM/Contribute/Form/ContributionThankYou.sepa.tpl @@ -117,10 +134,10 @@ msgstr "BIC" #: CRM/Admin/Form/Setting/SepaSettings.php CRM/Sepa/Form/CreateMandate.php #: CRM/Sepa/Form/Report/SepaMandateGeneric.php -#: CRM/Sepa/Form/Search/SepaContactSearch.php +#: CRM/Sepa/Form/Search/SepaContactSearch.php CRM/Utils/SepaTokens.php #: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php #: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php -#: Civi/Sepa/ActionProvider/Action/FindMandate.php js/CreateMandate.js sepa.php +#: Civi/Sepa/ActionProvider/Action/FindMandate.php js/CreateMandate.js #: templates/CRM/Admin/Form/Setting/SepaSettings.tpl #: templates/CRM/Contribute/Form/ContributionConfirm.sepa.tpl #: templates/CRM/Contribute/Form/ContributionThankYou.sepa.tpl @@ -132,16 +149,21 @@ msgstr "BIC" msgid "IBAN" msgstr "IBAN" +#: CRM/Admin/Form/Setting/SepaSettings.php +msgid "CUC (only from CBIBdySDDReq)" +msgstr "CUC (nur aus CBIBdySDDReq)" + #: CRM/Admin/Form/Setting/SepaSettings.php CRM/Sepa/DAO/SEPACreditor.php #: CRM/Sepa/Form/CreateMandate.php CRM/Sepa/Form/Report/SepaMandateOOFF.php -#: CRM/Sepa/Form/Report/SepaMandateRCUR.php sepa.php +#: CRM/Sepa/Form/Report/SepaMandateRCUR.php CRM/Utils/SepaTokens.php msgid "Currency" msgstr "Währung" #: CRM/Admin/Form/Setting/SepaSettings.php CRM/Sepa/DAO/SEPAMandate.php #: CRM/Sepa/DAO/SEPATransactionGroup.php #: CRM/Sepa/Form/Report/SepaMandateGeneric.php -#: CRM/Sepa/Form/Search/SepaContactSearch.php sepa.php +#: CRM/Sepa/Form/Search/SepaContactSearch.php CRM/Utils/SepaTokens.php +#: Civi/Sepa/DataProcessor/Join/AbstractMandateJoin.php #: templates/CRM/Sepa/Page/DashBoard.tpl templates/CRM/Sepa/Page/MandateTab.tpl msgid "Type" msgstr "Art" @@ -318,6 +340,10 @@ msgstr "Mandat [%1] kann nicht angepasst werden, das Batching läuft gerade!" msgid "You can only modify RCUR mandates." msgstr "Sie können nur Mandate mit wiederkehrenden Zahlungen (RCUR) ersetzen." +#: CRM/Sepa/BAO/SEPAMandate.php +msgid "Account Holder changed from '%1' to '%2'" +msgstr "Kontoinhaber geändert von '%1' auf '%2'" + #: CRM/Sepa/BAO/SEPAMandate.php msgid "IBAN changed from '%1' to '%2'" msgstr "IBAN geändert von '%1' auf '%2'" @@ -331,10 +357,8 @@ msgid "Bank details changed" msgstr "Bankverbindung geändert" #: CRM/Sepa/BAO/SEPAMandate.php -#, fuzzy -#| msgid "Amount has to be positive." msgid "The amount has to be positive." -msgstr "Betrag muss positiv sein." +msgstr "Der Betrag muss positiv sein." #: CRM/Sepa/BAO/SEPAMandate.php msgid "Amount increased" @@ -372,6 +396,14 @@ msgstr "Kampagne geändert" msgid "Campaign changed from '%1' [%2] to '%3' [%4]." msgstr "Kampagne von '%1' [%2] auf '%3' [%4] geändert." +#: CRM/Sepa/BAO/SEPAMandate.php +msgid "Cycle day changed" +msgstr "Einzugstag geändert" + +#: CRM/Sepa/BAO/SEPAMandate.php +msgid "Cycle day changed from '%1' to '%2'." +msgstr "Einzugstag geändert von '%1' auf '%2'." + #: CRM/Sepa/BAO/SEPAMandate.php msgid "%1 pending contributions were adjusted as well." msgstr "%1 pending contributions were adjusted as well." @@ -430,38 +462,42 @@ msgstr "SEPA Zuwendungsgruppen" msgid "SEPAContribution Group" msgstr "SEPA Zuwendungsgruppe" +#: CRM/Sepa/DAO/SEPAContributionGroup.php CRM/Sepa/DAO/SEPACreditor.php +#: CRM/Sepa/DAO/SEPAMandate.php CRM/Sepa/DAO/SEPASddFile.php +#: CRM/Sepa/DAO/SEPATransactionGroup.php +#: templates/CRM/Admin/Form/Setting/SepaSettings.tpl +#: templates/CRM/Sepa/Page/ListGroup.tpl +msgid "ID" +msgstr "ID" + #: CRM/Sepa/DAO/SEPAContributionGroup.php msgid "primary key" -msgstr "" +msgstr "Primärschlüssel" #: CRM/Sepa/DAO/SEPAContributionGroup.php -#, fuzzy -#| msgid "Contribution ID" -msgid "FK to Contribution ID" +#: CRM/Sepa/Form/Report/SepaMandateOOFF.php +msgid "Contribution ID" msgstr "Zuwendungs-ID" +#: CRM/Sepa/DAO/SEPAContributionGroup.php +msgid "FK to Contribution ID" +msgstr "Fremdschlüssel zu Zuwendungs-ID" + +#: CRM/Sepa/DAO/SEPAContributionGroup.php +msgid "Txgroup ID" +msgstr "Transaktionsgruppen-ID" + #: CRM/Sepa/DAO/SEPAContributionGroup.php msgid "FK to civicrm_sdd_txgroup" -msgstr "" +msgstr "Fremdschlüssel zu civicrm_sdd_txgroup" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "SEPA Creditor" msgid "SEPACreditors" -msgstr "SEPA Gläubiger" +msgstr "SEPA-Gläubiger" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "SEPA Creditor" msgid "SEPACreditor" -msgstr "SEPA Gläubiger" - -#: CRM/Sepa/DAO/SEPACreditor.php CRM/Sepa/DAO/SEPAMandate.php -#: CRM/Sepa/DAO/SEPASddFile.php CRM/Sepa/DAO/SEPATransactionGroup.php -#: templates/CRM/Admin/Form/Setting/SepaSettings.tpl -#: templates/CRM/Sepa/Page/ListGroup.tpl -msgid "ID" -msgstr "ID" +msgstr "SEPA-Gläubiger" #: CRM/Sepa/DAO/SEPACreditor.php msgid "Creditor Contact ID" @@ -469,7 +505,7 @@ msgstr "Gläubiger-Kontakt" #: CRM/Sepa/DAO/SEPACreditor.php msgid "FK to Contact ID that owns that account" -msgstr "" +msgstr "Fremdschlüssel zu Kontakt-ID des Kontoinhabers" #: CRM/Sepa/DAO/SEPACreditor.php msgid "SEPA Creditor identifier" @@ -480,6 +516,8 @@ msgid "" "Provided by the bank. ISO country code+check digit+ZZZ+country specific " "identifier" msgstr "" +"Von der Bank bereitgestellt. ISO-Ländercode + Prüfziffer + ZZZ + " +"länderspezifischer Identifikator" #: CRM/Sepa/DAO/SEPACreditor.php #: templates/CRM/Admin/Form/Setting/SepaSettings.tpl @@ -488,7 +526,7 @@ msgstr "Name des Gläubigers" #: CRM/Sepa/DAO/SEPACreditor.php msgid "official creditor name, passed to exported files" -msgstr "" +msgstr "offizieller Gläubigername, wird exportierten Dateien übergeben" #: CRM/Sepa/DAO/SEPACreditor.php #: templates/CRM/Admin/Form/Setting/SepaSettings.tpl @@ -496,8 +534,6 @@ msgid "Creditor Label" msgstr "Bezeichnung des Gläubigers" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "internal label of the creditor" msgid "internally used label for the creditor" msgstr "Interner Name des Gläubigers" @@ -511,59 +547,49 @@ msgstr "" #: CRM/Sepa/DAO/SEPACreditor.php msgid "Which Country does this address belong to." -msgstr "" +msgstr "Zu welchem Land gehört diese Adresse" #: CRM/Sepa/DAO/SEPACreditor.php CRM/Sepa/DAO/SEPAMandate.php msgid "Iban" msgstr "IBAN" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "name of the creditor" msgid "Iban of the creditor" -msgstr "Öffentlicher Name des Gläubigers" +msgstr "IBAN des Gläubigers" #: CRM/Sepa/DAO/SEPACreditor.php CRM/Sepa/DAO/SEPAMandate.php msgid "Bic" msgstr "BIC" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "name of the creditor" msgid "BIC of the creditor" -msgstr "Öffentlicher Name des Gläubigers" +msgstr "BIC des Gläubigers" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "Mandate numbering prefix" msgid "Mandate numering prefix" -msgstr "Mandats-Prefix" +msgstr "Mandats-Präfix" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "Creditor Identifier" msgid "prefix for mandate identifiers" -msgstr "SEPA Gläubiger-Identifikationsnummer" +msgstr "Präfix für Mandatsreferenzen" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "Currency used by this creditor" msgid "currency used by this creditor" -msgstr "Währung dieses Kreditors" +msgstr "Währung dieses Gläubigers" + +#: CRM/Sepa/DAO/SEPACreditor.php +msgid "Payment Processor ID" +msgstr "Zahlungsprozessor-ID" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "First Contribution (to be deprecated)" msgid "Payment processor link (to be deprecated)" -msgstr "Ersteinzug" +msgstr "Zahlungsprozessor-Link (wird veralten)" #: CRM/Sepa/DAO/SEPACreditor.php msgid "Category purpose of the collection" msgstr "" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "Default Creditor" msgid "Default value" msgstr "Standard-Gläubiger" @@ -607,10 +633,8 @@ msgid "Creditor Type" msgstr "Art des Kreditors" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "Creditor Type: SEPA (default) or PSP" msgid "Type of the creditor, values are SEPA (default) and PSP" -msgstr "Art des Kreditors: SEPA (Standard) oder PSP " +msgstr "Art des Gläubigers: SEPA (Standard) oder PSP" #: CRM/Sepa/DAO/SEPACreditor.php msgid "OOFF Payment Instruments" @@ -631,26 +655,29 @@ msgid "" msgstr "" #: CRM/Sepa/DAO/SEPACreditor.php -#, fuzzy -#| msgid "Currency used by this creditor" msgid "If true, BICs are not used for this creditor" -msgstr "Währung dieses Kreditors" +msgstr "Falls aktiviert, werden BICs nicht für diesen Gläubiger verwendet" + +#: CRM/Sepa/DAO/SEPACreditor.php +#: templates/CRM/Admin/Form/Setting/SepaSettings.tpl +msgid "CUC" +msgstr "" + +#: CRM/Sepa/DAO/SEPACreditor.php +msgid "CUC of the creditor" +msgstr "CUC des Gläubigers" #: CRM/Sepa/DAO/SEPAMandate.php -#, fuzzy -#| msgid "SEPA Mandates" msgid "SEPAMandates" -msgstr "SEPA Mandate" +msgstr "SEPA-Mandate" #: CRM/Sepa/DAO/SEPAMandate.php -#, fuzzy -#| msgid "SEPA Mandate" msgid "SEPAMandate" msgstr "SEPA-Mandat" #: CRM/Sepa/DAO/SEPAMandate.php CRM/Sepa/DAO/SEPASddFile.php #: CRM/Sepa/DAO/SEPATransactionGroup.php CRM/Sepa/Form/CreateMandate.php -#: CRM/Sepa/Form/Search/SepaContactSearch.php sepa.php +#: CRM/Sepa/Form/Search/SepaContactSearch.php CRM/Utils/SepaTokens.php #: templates/CRM/Sepa/Page/EditMandate.tpl #: templates/CRM/Sepa/Page/MandateTab.tpl #: templates/Sepa/Contribute/Form/ContributionView.tpl @@ -659,14 +686,14 @@ msgid "Reference" msgstr "Mandatsreferenz" #: CRM/Sepa/DAO/SEPAMandate.php -#, fuzzy -#| msgid "Mandate reference" msgid "A unique mandate reference" -msgstr "Mandatsreferenz" +msgstr "eine eindeutige Mandatsreferenz" #: CRM/Sepa/DAO/SEPAMandate.php CRM/Sepa/Form/CreateMandate.php #: CRM/Sepa/Form/Report/SepaMandateGeneric.php -#: CRM/Sepa/Form/Report/SepaMandateOOFF.php sepa.php +#: CRM/Sepa/Form/Report/SepaMandateOOFF.php CRM/Utils/SepaTokens.php +#: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php +#: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php #: templates/CRM/Sepa/Page/CreateMandate.tpl #: templates/CRM/Sepa/Page/EditMandate.tpl #: templates/Sepa/Contribute/Form/ContributionView.tpl @@ -714,6 +741,10 @@ msgstr "Gläubiger-ID" msgid "FK to ssd_creditor" msgstr "" +#: CRM/Sepa/DAO/SEPAMandate.php +msgid "Creator" +msgstr "Ersteller" + #: CRM/Sepa/DAO/SEPAMandate.php CRM/Sepa/Form/Report/SepaMandateGeneric.php #: CRM/Sepa/Form/Search/SepaContactSearch.php #: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php @@ -726,24 +757,30 @@ msgstr "Kontakt ID" msgid "FK to Contact ID of the debtor" msgstr "" +#: CRM/Sepa/DAO/SEPAMandate.php templates/CRM/Sepa/Page/CreateMandate.tpl +#: templates/CRM/Sepa/Page/EditMandate.tpl +#: templates/CRM/Sepa/Page/ListGroup.tpl +msgid "Contact" +msgstr "Kontakt" + +#: CRM/Sepa/DAO/SEPAMandate.php +msgid "Name of the account holder" +msgstr "Name des Kontoinhabers" + #: CRM/Sepa/DAO/SEPAMandate.php -#, fuzzy -#| msgid "name of the creditor" msgid "Iban of the debtor" -msgstr "Öffentlicher Name des Gläubigers" +msgstr "IBAN des Schuldners" #: CRM/Sepa/DAO/SEPAMandate.php -#, fuzzy -#| msgid "name of the creditor" msgid "BIC of the debtor" -msgstr "Öffentlicher Name des Gläubigers" +msgstr "BIC des Schuldners" #: CRM/Sepa/DAO/SEPAMandate.php msgid "RCUR for recurrent (default), OOFF for one-shot" msgstr "" #: CRM/Sepa/DAO/SEPAMandate.php CRM/Sepa/Form/Search/SepaContactSearch.php -#: Civi/Sepa/ActionProvider/Action/FindMandate.php sepa.php +#: CRM/Utils/SepaTokens.php Civi/Sepa/ActionProvider/Action/FindMandate.php #: templates/CRM/Sepa/Page/DashBoard.tpl #: templates/CRM/Sepa/Page/EditMandate.tpl #: templates/CRM/Sepa/Page/ListGroup.tpl templates/CRM/Sepa/Page/MandateTab.tpl @@ -762,31 +799,30 @@ msgstr "" msgid "creation date" msgstr "Erstellungsdatum" +#: CRM/Sepa/DAO/SEPAMandate.php CRM/Sepa/DAO/SEPASddFile.php +#: CRM/Sepa/DAO/SEPATransactionGroup.php +msgid "Created Date" +msgstr "Erstelldatum" + #: CRM/Sepa/DAO/SEPAMandate.php msgid "First Contribution (to be deprecated)" msgstr "Ersteinzug" #: CRM/Sepa/DAO/SEPAMandate.php -#, fuzzy -#| msgid "1st contribution" msgid "FK to civicrm_contribution" -msgstr "Erstzuwendung" +msgstr "Fremdschlüssel zu civicrm_contribution" #: CRM/Sepa/DAO/SEPAMandate.php msgid "validation date" msgstr "Validierungsdatum" #: CRM/Sepa/DAO/SEPASddFile.php -#, fuzzy -#| msgid "SEPA File Format" msgid "SEPASdd Files" -msgstr "SEPA Dateiformat (PAIN)" +msgstr "SEPASdd-Dateien" #: CRM/Sepa/DAO/SEPASddFile.php -#, fuzzy -#| msgid "SEPA File Format" msgid "SEPASdd File" -msgstr "SEPA Dateiformat (PAIN)" +msgstr "SEPASdd-Datei" #: CRM/Sepa/DAO/SEPASddFile.php msgid "End-to-end reference for this sdd file." @@ -797,33 +833,33 @@ msgid "Filename" msgstr "Dateiname" #: CRM/Sepa/DAO/SEPASddFile.php -#, fuzzy -#| msgid "name of the creditor" msgid "Name of the generated file" -msgstr "Öffentlicher Name des Gläubigers" +msgstr "Name der erzeugten Datei" #: CRM/Sepa/DAO/SEPASddFile.php CRM/Sepa/DAO/SEPATransactionGroup.php msgid "Latest Submission Date" msgstr "Spätestes Übertragungsdatum" #: CRM/Sepa/DAO/SEPASddFile.php CRM/Sepa/DAO/SEPATransactionGroup.php -#, fuzzy -#| msgid "Latest Submission Date" msgid "Latest submission date" msgstr "Spätestes Übertragungsdatum" -#: CRM/Sepa/DAO/SEPASddFile.php CRM/Sepa/DAO/SEPATransactionGroup.php -msgid "Created Date" -msgstr "Erstelldatum" - #: CRM/Sepa/DAO/SEPASddFile.php CRM/Sepa/DAO/SEPATransactionGroup.php msgid "When was this item created" msgstr "" +#: CRM/Sepa/DAO/SEPASddFile.php +msgid "Created ID" +msgstr "Ersteller-ID" + #: CRM/Sepa/DAO/SEPASddFile.php msgid "FK to Contact ID of creator" msgstr "" +#: CRM/Sepa/DAO/SEPASddFile.php CRM/Sepa/DAO/SEPATransactionGroup.php +msgid "Status ID" +msgstr "Status-ID" + #: CRM/Sepa/DAO/SEPASddFile.php msgid "fk to Batch Status options in civicrm_option_values" msgstr "" @@ -841,16 +877,12 @@ msgid "Tag used to group multiple creditors in this XML file." msgstr "" #: CRM/Sepa/DAO/SEPATransactionGroup.php -#, fuzzy -#| msgid "View SEPA transaction groups" msgid "SEPATransaction Groups" -msgstr "SEPA-Gruppen ansehen" +msgstr "SEPATransaction-Gruppen" #: CRM/Sepa/DAO/SEPATransactionGroup.php -#, fuzzy -#| msgid "View SEPA transaction groups" msgid "SEPATransaction Group" -msgstr "SEPA-Gruppen ansehen" +msgstr "SEPATransaction-Gruppe" #: CRM/Sepa/DAO/SEPATransactionGroup.php msgid "End-to-end reference for this tx group." @@ -869,21 +901,25 @@ msgid "Collection Date" msgstr "Einzugsdatum" #: CRM/Sepa/DAO/SEPATransactionGroup.php -#, fuzzy -#| msgid "Earliest Collection Date" msgid "Target collection date" -msgstr "Frühester Einzugstermin" +msgstr "Zieleinzugsdatum" #: CRM/Sepa/DAO/SEPATransactionGroup.php msgid "fk sepa group Status options in civicrm_option_values" msgstr "" #: CRM/Sepa/DAO/SEPATransactionGroup.php -#, fuzzy -#| msgid "Creditor ID" -msgid "fk to SDD Creditor Id" +msgid "Sdd Creditor ID" msgstr "Gläubiger-ID" +#: CRM/Sepa/DAO/SEPATransactionGroup.php +msgid "fk to SDD Creditor Id" +msgstr "Fremdschlüssel zu Gläubiger-ID" + +#: CRM/Sepa/DAO/SEPATransactionGroup.php +msgid "Sdd File ID" +msgstr "Sdd-Datei-ID" + #: CRM/Sepa/DAO/SEPATransactionGroup.php msgid "fk to SDD File Id" msgstr "" @@ -893,6 +929,8 @@ msgid "SepaMandate ID" msgstr "Mandats-ID" #: CRM/Sepa/DAO/SepaMandateLink.php CRM/Sepa/Form/Report/SepaMandateGeneric.php +#: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php +#: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php msgid "Creation Date" msgstr "Erstellungsdatum" @@ -914,6 +952,12 @@ msgstr "Startdatum" msgid "End Date" msgstr "Enddatum" +#: CRM/Sepa/Form/CreateMandate.php +msgid "" +"The mandate to clone/replace from does not exist or you do not have " +"permission for it." +msgstr "" + #: CRM/Sepa/Form/CreateMandate.php msgid "You can only replace RCUR mandates" msgstr "Sie können nur Mandate mit wiederkehrenden Zahlungen (RCUR) ersetzen" @@ -942,10 +986,8 @@ msgid "Creditor" msgstr "Kreditor" #: CRM/Sepa/Form/CreateMandate.php -#, fuzzy -#| msgid "Payment Details" msgid "Payment Method" -msgstr "Zahlungsdetails" +msgstr "Zahlungsmethode" #: CRM/Sepa/Form/CreateMandate.php CRM/Sepa/Form/Report/SepaMandateOOFF.php #: CRM/Sepa/Form/Report/SepaMandateRCUR.php @@ -975,15 +1017,20 @@ msgstr "nicht benötigt" msgid "Account" msgstr "Konto" +#: CRM/Sepa/Form/CreateMandate.php +msgid "not required if same as contact" +msgstr "nicht benötigt wenn gleich mit Kontakt" + #: CRM/Sepa/Form/CreateMandate.php msgid "required" msgstr "erforderlich" #: CRM/Sepa/Form/CreateMandate.php CRM/Sepa/Form/Report/SepaMandateGeneric.php #: CRM/Sepa/Form/Report/SepaMandateOOFF.php CRM/Sepa/Page/CreateMandate.php +#: CRM/Utils/SepaTokens.php #: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php #: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php -#: Civi/Sepa/ActionProvider/Action/FindMandate.php sepa.php +#: Civi/Sepa/ActionProvider/Action/FindMandate.php #: templates/CRM/Contribute/Form/ContributionThankYou.sepa.tpl #: templates/CRM/Event/Form/RegistrationThankYou.sepa.tpl #: templates/CRM/Sepa/Page/CreateMandate.tpl @@ -1077,6 +1124,7 @@ msgstr "Generic SEPA Mandate Report (org.project60.sepa)" #: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php #: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php #: Civi/Sepa/ActionProvider/Action/FindMandate.php +#: Civi/Sepa/ActionProvider/Action/TerminateMandate.php #: templates/CRM/Contribute/Form/ContributionThankYou.sepa.tpl #: templates/CRM/Event/Form/RegistrationThankYou.sepa.tpl #: templates/CRM/Sepa/Page/CreateMandate.tpl @@ -1087,6 +1135,7 @@ msgstr "Mandatsreferenz" #: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php #: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php #: Civi/Sepa/ActionProvider/Action/FindMandate.php +#: Civi/Sepa/ActionProvider/Action/TerminateMandate.php msgid "Mandate ID" msgstr "Mandats-ID" @@ -1094,10 +1143,10 @@ msgstr "Mandats-ID" msgid "Mandate Status" msgstr "Status des Mandats" -#: CRM/Sepa/Form/Report/SepaMandateGeneric.php +#: CRM/Sepa/Form/Report/SepaMandateGeneric.php CRM/Utils/SepaTokens.php #: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php #: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php -#: Civi/Sepa/ActionProvider/Action/FindMandate.php sepa.php +#: Civi/Sepa/ActionProvider/Action/FindMandate.php msgid "Signature Date" msgstr "Unterschriftsdatum" @@ -1128,6 +1177,10 @@ msgstr "Einmaleinzug" msgid "Recurring" msgstr "Dauereinzug" +#: CRM/Sepa/Form/Report/SepaMandateGeneric.php +msgid "account_holder" +msgstr "" + #: CRM/Sepa/Form/Report/SepaMandateGeneric.php msgid "Contact Name" msgstr "Kontaktname" @@ -1148,11 +1201,8 @@ msgstr "SEPA Mandate (Einzeleinzug)" msgid "SEPA One-Off Mandate Report (org.project60.sepa)" msgstr "SEPA Einzeleinzugs-Mandate (org.project60.sepa)" -#: CRM/Sepa/Form/Report/SepaMandateOOFF.php -msgid "Contribution ID" -msgstr "Zuwendungs-ID" - #: CRM/Sepa/Form/Report/SepaMandateOOFF.php CRM/Sepa/Form/RetryCollection.php +#: Civi/Sepa/ActionProvider/Action/TerminateMandate.php #: templates/CRM/Sepa/Page/EditMandate.tpl msgid "Cancel Reason" msgstr "Änderungsgrund" @@ -1190,7 +1240,7 @@ msgstr "SEPA Mandate (Dauereinzüge)" msgid "SEPA Recurring Mandate Report (org.project60.sepa)" msgstr "SEPA Dauereinzugs-Mandate (org.project60.sepa)" -#: CRM/Sepa/Form/Report/SepaMandateRCUR.php sepa.php +#: CRM/Sepa/Form/Report/SepaMandateRCUR.php CRM/Utils/SepaTokens.php msgid "Cycle Day" msgstr "Monatstag" @@ -1251,10 +1301,8 @@ msgid "SDD Groups" msgstr "SEPA-Gruppen" #: CRM/Sepa/Form/RetryCollection.php -#, fuzzy -#| msgid "Transaction Message" msgid "Custom Transaction Message" -msgstr "Verwendungszweck" +msgstr "Benutzerdefinierte Transaktionsnachricht" #: CRM/Sepa/Form/RetryCollection.php templates/CRM/Sepa/Page/CreateMandate.tpl msgid "Note" @@ -1373,6 +1421,52 @@ msgstr "Dateiformat [%1] nicht vorhanden!" msgid "Cannot close transaction group! Error was: '%s'" msgstr "Die Gruppe konnte nicht geschlossen werden. Fehler ist: '%s'" +#: CRM/Sepa/Logic/MandateRepairs.php +msgid "" +"%1 orphaned open (pending) SEPA contributions were found in the system, i.e. " +"they are not part of a SEPA transaction group, and will not be collected any " +"more. You should delete them by searching for contributions in status " +"'Pending' with payment instruments RCUR and FRST." +msgstr "" + +#: CRM/Sepa/Logic/MandateRepairs.php +msgid "" +"WARNING: %1 orphaned active (in Progress) SEPA contributions detected. These " +"may cause irregularities in the generation of the SEPA collection groups, " +"and in particular might cause the same installment to be collected multiple " +"times. You should find them by searching for contributions in status 'in " +"Progress' with the SEPA payment instruments (e.g. RCUR and FRST), and then " +"export (to be safe) and delete them." +msgstr "" + +#: CRM/Sepa/Logic/MandateRepairs.php +msgid "" +"Warning: had to adjusted the status of %1 contribution(s) to 'Pending', as " +"they are part of an open transaction group." +msgstr "" + +#: CRM/Sepa/Logic/MandateRepairs.php +msgid "Adjusted the payment instruments of %1 recurring mandate(s)." +msgstr "Zahlungsmethode von %1 wiederkehrenden Mandaten wurden angepasst." + +#: CRM/Sepa/Logic/MandateRepairs.php +msgid "" +"The following irregularities have been detected and fixed in your database:" +msgstr "" +"Die folgenden Unregelmäßigkeiten wurden erkannt und in der Datenbank behoben:" + +#: CRM/Sepa/Logic/MandateRepairs.php +msgid "You can find a detailed log of the changes here: %1" +msgstr "" + +#: CRM/Sepa/Logic/MandateRepairs.php +msgid "CiviSEPA Health Check" +msgstr "CiviSEPA-Selbstkontrolle" + +#: CRM/Sepa/Logic/Queue/Close.php +msgid "Marking SDD Group(s) Received: [%1]" +msgstr "Markiere SDD-Gruppe(n) als erhalten: [%1]" + #: CRM/Sepa/Logic/Queue/Close.php msgid "Closing SDD Group(s) [%1]" msgstr "Schließe SEPA-Gruppe(n) [%1]" @@ -1406,6 +1500,14 @@ msgstr "Bereinigung beendeter Mandate wird vorbereitet" msgid "Cleaning up ended mandates" msgstr "Beendet Mandate werden bereinigt" +#: CRM/Sepa/Logic/Queue/Update.php +msgid "Process %1 mandates (%2-%3)" +msgstr "" + +#: CRM/Sepa/Logic/Queue/Update.php +msgid "Cleaning up %1 groups" +msgstr "" + #: CRM/Sepa/Logic/Queue/Update.php msgid "Lock released" msgstr "Sperre aufgehoben" @@ -1414,6 +1516,10 @@ msgstr "Sperre aufgehoben" msgid "Thank you" msgstr "Vielen Dank" +#: CRM/Sepa/Logic/Settings.php +msgid "In Progress" +msgstr "" + #: CRM/Sepa/Logic/Status.php msgid "Not activated" msgstr "Nicht aktiv" @@ -1575,8 +1681,8 @@ msgid "CiviSEPA Dashboard" msgstr "CiviSEPA Dashboard" #: CRM/Sepa/Page/DashBoard.php -msgid "Couldn't read transaction groups. Error was: '%s'" -msgstr "Die Gruppen konnten nicht geladen werden. Fehler ist: '%s'" +msgid "Couldn't read transaction groups. Error was: %1" +msgstr "Die Gruppen konnten nicht geladen werden. Fehler war: %1" #: CRM/Sepa/Page/DashBoard.php msgid "Unknown batcher mode '%s'. No batching triggered." @@ -1690,6 +1796,22 @@ msgstr "Die SEPA Gruppe [%s] konnte nicht geladen werden. Fehler ist: '%s'" msgid "SEPA Mandates" msgstr "SEPA Mandate" +#: CRM/Sepa/Page/MarkGroupReceived.php +msgid "Mark SEPA group received" +msgstr "SEPA-Gruppen als erhalten markieren" + +#: CRM/Sepa/Page/MarkGroupReceived.php templates/CRM/Sepa/Page/DeleteGroup.tpl +msgid "No group_id given!" +msgstr "Keine Gruppen-ID (group_id) angegeben." + +#: CRM/Sepa/Page/MarkGroupReceived.php +msgid "Cannot mark TEST groups as received." +msgstr "" + +#: CRM/Sepa/Page/MarkGroupReceived.php +msgid "Couldn't close SDD group #%1.
Error was: %2" +msgstr "SDD-Gruppe #%1 konnte nicht geschlossen werden. Fehler: %2" + #: CRM/Sepa/Page/SepaFile.php msgid "Generate XML File" msgstr "Erzeuge XML-Datei" @@ -1714,10 +1836,6 @@ msgstr "SEPA Standardnachrichtenvorlage PDF" msgid "SEPA Direct Debit Payment Information" msgstr "SEPA Zahlungsinformationen" -#: CRM/Sepa/Upgrader/Base.php -msgid "Upgrade %1 to revision %2" -msgstr "" - #: CRM/Sepa/Upgrader.php msgid "" "Your CiviSEPA payment processors have been disabled, the code was moved into " @@ -1733,10 +1851,8 @@ msgstr "" "releases\">CiviSEPA Payment Processor Erweiterung." #: CRM/Sepa/Upgrader.php -#, fuzzy -#| msgid "Payment Processor Settings" msgid "%1 Payment Processor(s) Disabled!" -msgstr "Einstellungen Zahlungsprozessor" +msgstr "%1 Zahlungsprzessor(en) deaktiviert!" #: CRM/Sepa/Upgrader.php msgid "" @@ -1767,6 +1883,40 @@ msgstr "unregelmäßig" msgid "SEPA Standard (FRST/RCUR)" msgstr "" +#: CRM/Utils/SepaTokens.php +msgid "Signature Date (raw)" +msgstr "Unterschriftsdatum (unformatiert)" + +#: CRM/Utils/SepaTokens.php +msgid "IBAN (anonymised)" +msgstr "IBAN (anonymisiert)" + +#: CRM/Utils/SepaTokens.php +msgid "Amount (raw)" +msgstr "Betrag (unformatiert)" + +#: CRM/Utils/SepaTokens.php +msgid "First Collection Date (raw)" +msgstr "Datum des ersten Einzugs (unformatiert)" + +#: CRM/Utils/SepaTokens.php +#: templates/CRM/Contribute/Form/ContributionThankYou.sepa.tpl +#: templates/CRM/Event/Form/RegistrationThankYou.sepa.tpl +msgid "First Collection Date" +msgstr "Erster Einzugstermin" + +#: CRM/Utils/SepaTokens.php +msgid "Interval Multiplier" +msgstr "Intervallänge" + +#: CRM/Utils/SepaTokens.php +msgid "Interval Unit" +msgstr "Intervalleinheit" + +#: CRM/Utils/SepaTokens.php templates/CRM/Sepa/Page/CreateMandate.tpl +msgid "Interval" +msgstr "Turnus" + #: Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php #: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php msgid "Creditor (default)" @@ -1809,6 +1959,22 @@ msgstr "Einzugstag (Standard)" msgid "Collection Day" msgstr "Einzugstag" +#: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php +msgid "Creditor (Leave empty to use default)" +msgstr "Gläubiger (Leer lassen für Standardwert)" + +#: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php +msgid "Financial Type (Leave empty to use default)" +msgstr "Zuwendungsart (Leer lassen für Standardwert)" + +#: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php +msgid "Campaign (Leave empty to use default)" +msgstr "Kampagne (Leer lassen für Standardwert)" + +#: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php +msgid "Recurring Contribution ID" +msgstr "ID der wiederkehrenden Zuwendung" + #: Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php msgid "bi-monthly" msgstr "alle 2 Monate" @@ -1818,8 +1984,6 @@ msgid "as soon as possible" msgstr "sobald wie möglich" #: Civi/Sepa/ActionProvider/Action/FindMandate.php -#, fuzzy -#| msgid "One-off" msgid "One-Off" msgstr "Einmaleinzug" @@ -1857,6 +2021,10 @@ msgstr "Auswahl (falls mehrere gefunden)" msgid "Annual Amount" msgstr "Jahresbetrag" +#: Civi/Sepa/ActionProvider/Action/TerminateMandate.php +msgid "Cancel Reason (if no parameter)" +msgstr "Änderungsgrund (wenn keine Parameter)" + #: Civi/Sepa/ContainerSpecs.php msgid "Create SEPA Mandate (One-Off)" msgstr "SEPA Mandat (Einzel-Lastschrift) erstellen" @@ -1869,6 +2037,55 @@ msgstr "SEPA Mandat (Dauerlastschrift) erstellen" msgid "Find SEPA Mandate" msgstr "SEPA-Mandate Finden" +#: Civi/Sepa/ContainerSpecs.php +msgid "Terminate SEPA Mandate" +msgstr "SEPA-Mandat beenden" + +#: Civi/Sepa/ContainerSpecs.php +#: templates/CRM/Contact/Page/View/Summary.sepa.tpl +msgid "SEPA Mandate" +msgstr "SEPA-Mandat" + +#: Civi/Sepa/ContainerSpecs.php +msgid "SEPA Creditor" +msgstr "SEPA-Gläubiger" + +#: Civi/Sepa/ContainerSpecs.php +msgid "SEPA Transaction Group" +msgstr "SEPA-Transaktionsgruppe" + +#: Civi/Sepa/ContainerSpecs.php +msgid "SEPA SDD File" +msgstr "SEPA-SDD-Datei" + +#: Civi/Sepa/ContainerSpecs.php +msgid "SEPA Contribution Group" +msgstr "SEPA-Zuwendungsgruppe" + +#: Civi/Sepa/ContainerSpecs.php +msgid "SEPA Mandate Link" +msgstr "SEPA-Mandatsverweis" + +#: Civi/Sepa/ContainerSpecs.php +msgid "Join Sepa Mandate on Contribution" +msgstr "SEPA-Mandat mit Zuwendung verbinden (join)" + +#: Civi/Sepa/ContainerSpecs.php +msgid "Join Sepa Mandate on Contribution Recur" +msgstr "SEPA-Mandat mit wiederkehrender Zuwendung verbinden (join)" + +#: Civi/Sepa/DataProcessor/Join/AbstractMandateJoin.php +msgid "Select field" +msgstr "" + +#: Civi/Sepa/DataProcessor/Join/AbstractMandateJoin.php +msgid "Required" +msgstr "Erforderlich" + +#: Civi/Sepa/DataProcessor/Join/AbstractMandateJoin.php +msgid "Not required" +msgstr "Nicht erforderlich" + #: api/v3/SepaTransactionGroup.php msgid "SEPA DD Transaction Batch" msgstr "SEPA DD Einzugsgruppe" @@ -1945,10 +2162,8 @@ msgid "Error" msgstr "Fehler" #: sepa.php -#, fuzzy -#| msgid "Recurring SEPA Mandates" msgid "Record SEPA Mandate" -msgstr "Wiederkehrende SEPA-Mandate" +msgstr "SEPA-Mandat erfassen" #: sepa.php msgid "CiviSEPA Settings" @@ -1962,30 +2177,58 @@ msgstr "CiviSEPA" msgid "Create SEPA mandates" msgstr "Lege SEPA-Mandate an" +#: sepa.php +msgid "Allows creating SEPA Direct Debit mandates." +msgstr "" + #: sepa.php msgid "View SEPA mandates" msgstr "SEPA-Mandate ansehen" +#: sepa.php +msgid "Allows viewing SEPA Direct Debit mandates" +msgstr "" + #: sepa.php msgid "Edit SEPA mandates" msgstr "SEPA-Mandate bearbeiten" +#: sepa.php +msgid "Allows editing SEPA Direct Debit mandates." +msgstr "" + #: sepa.php msgid "Delete SEPA mandates" msgstr "SEPA-Mandate löschen" +#: sepa.php +msgid "Allows deleting SEPA Direct Debit mandates" +msgstr "" + #: sepa.php msgid "View SEPA transaction groups" msgstr "SEPA-Gruppen ansehen" +#: sepa.php +msgid "Allows viewing groups of SEPA transactions to be sent to the bank." +msgstr "" + #: sepa.php msgid "Batch SEPA transaction groups" msgstr "SEPA-Gruppen erzeugen" +#: sepa.php +msgid "Allows generating groups of SEPA transactions to be sent to the bank." +msgstr "" + #: sepa.php msgid "Delete SEPA transaction groups" msgstr "SEPA-Gruppen löschen" +#: sepa.php +msgid "Allows deleting groups of SEPA transactions to be sent to the bank." +msgstr "" + #: sepa.php msgid "" "This contribution has no mandate and cannot simply be changed to a SEPA " @@ -1998,39 +2241,6 @@ msgstr "" msgid "Most Recent SEPA Mandate" msgstr "Letztes SEPA Mandat" -#: sepa.php -msgid "Signature Date (raw)" -msgstr "Unterschriftsdatum (unformatiert)" - -#: sepa.php -msgid "IBAN (anonymised)" -msgstr "IBAN (anonymisiert)" - -#: sepa.php -msgid "Amount (raw)" -msgstr "Betrag (unformatiert)" - -#: sepa.php -msgid "First Collection Date (raw)" -msgstr "Datum des ersten Einzugs (unformatiert)" - -#: sepa.php templates/CRM/Contribute/Form/ContributionThankYou.sepa.tpl -#: templates/CRM/Event/Form/RegistrationThankYou.sepa.tpl -msgid "First Collection Date" -msgstr "Erster Einzugstermin" - -#: sepa.php -msgid "Interval Multiplier" -msgstr "Intervallänge" - -#: sepa.php -msgid "Interval Unit" -msgstr "Intervalleinheit" - -#: sepa.php templates/CRM/Sepa/Page/CreateMandate.tpl -msgid "Interval" -msgstr "Turnus" - #: templates/CRM/Admin/Form/Setting/SepaSettings.hlp msgid "" "This is the name of the contact to which this creditor is associated to." @@ -2301,12 +2511,12 @@ msgstr "" #: templates/CRM/Admin/Form/Setting/SepaSettings.hlp msgid "" -"If you have the Little BIC Extension installed, the BIC can be derived automatically " -"from the IBAN in most cases." +"If you have the Little BIC Extension installed, the BIC can be derived " +"automatically from the IBAN in most cases." msgstr "" -"Wenn Sie die Little BIC Extension installiert haben, kann die BIC automatisch " +"Wenn Sie die Little BIC Extension installiert haben, kann die BIC automatisch " "anhand der IBAN ermittelt werden." #: templates/CRM/Admin/Form/Setting/SepaSettings.hlp @@ -2460,6 +2670,12 @@ msgid "" "If you leave this empty, no (new) one-off mandates can be created any more." msgstr "" +#: templates/CRM/Admin/Form/Setting/SepaSettings.hlp +msgid "" +"CUC-code (\"Codice Univoco CBI\") of the financial institution of the " +"creditor. It is only required for CBIBdySDDReq SEPA file format." +msgstr "" + #: templates/CRM/Admin/Form/Setting/SepaSettings.tpl msgid "Creditors" msgstr "Einzugsberechtigte" @@ -2561,10 +2777,6 @@ msgstr "Keine vorläufigen XML-Dateien" msgid "Support Large Groups" msgstr "Unterstützung für große SEPA-Gruppen" -#: templates/CRM/Contact/Page/View/Summary.sepa.tpl -msgid "SEPA Mandate" -msgstr "SEPA-Mandat" - #: templates/CRM/Contact/Page/View/Summary.sepa.tpl #: templates/CRM/Sepa/Page/DashBoard.tpl templates/CRM/Sepa/Page/ListGroup.tpl msgid "Contributions" @@ -2584,10 +2796,6 @@ msgstr "Ich möchte diesen Betrag %1 zahlen." msgid "This payment will be debited from the following account:" msgstr "Die Zahlung wird von dem folgenden Bankkonto abgebucht:" -#: templates/CRM/Contribute/Form/ContributionConfirm.sepa.tpl -msgid "Account Holder" -msgstr "Kontoinhaber" - #: templates/CRM/Contribute/Form/ContributionConfirm.sepa.tpl msgid "Bank Name" msgstr "Bankname" @@ -2646,6 +2854,45 @@ msgstr "Frühester Einzugstermin" msgid "You're replacing mandate %1" msgstr "Sie ersetzen Mandat %1" +#: templates/CRM/Sepa/Form/DataProcessor/Join/MandateContributionJoin.tpl +msgid "" +"Select the ID of the contribution. This could be either on the contribution " +"source with the field id. Or the any other data source which holds a " +"contribution ID field." +msgstr "" + +#: templates/CRM/Sepa/Form/DataProcessor/Join/MandateContributionJoin.tpl +#: templates/CRM/Sepa/Form/DataProcessor/Join/MandateContributionRecurJoin.tpl +msgid "Required join" +msgstr "erforderlicher Join" + +#: templates/CRM/Sepa/Form/DataProcessor/Join/MandateContributionJoin.tpl +msgid "" +"Required means that both Sepa Mandate Entity ID field and the Contribution " +"ID need to be set. " +msgstr "" + +#: templates/CRM/Sepa/Form/DataProcessor/Join/MandateContributionJoin.tpl +msgid "Join on Contribution ID field" +msgstr "Join auf ID-Feld der Zuwendung" + +#: templates/CRM/Sepa/Form/DataProcessor/Join/MandateContributionRecurJoin.tpl +msgid "" +"Select the ID of the contribution recur. This could be either on the " +"contribution recur source with the field id. Or the contribution source and " +"field contribution_recur_id." +msgstr "" + +#: templates/CRM/Sepa/Form/DataProcessor/Join/MandateContributionRecurJoin.tpl +msgid "" +"Required means that both Sepa Mandate Entity ID field and the Recurring " +"Contribution ID need to be set. " +msgstr "" + +#: templates/CRM/Sepa/Form/DataProcessor/Join/MandateContributionRecurJoin.tpl +msgid "Join on Contribution Recur ID field" +msgstr "Join auf ID-Feld der wiederkehrenden Zuwendung" + #: templates/CRM/Sepa/Form/RetryCollection.hlp msgid "" "Allows you to add a customised transaction message, i.e. the message the " @@ -2659,12 +2906,12 @@ msgid "" msgstr "" #: templates/CRM/Sepa/Form/RetryCollection.tpl -#, fuzzy -#| msgid "Select the transaction groups you want to re-collect." msgid "" "Select the transaction groups of which you want to re-collect failed " "transactions" -msgstr "Wählen Sie die SEPA-Gruppen aus, die Sie erneut einziehen möchten." +msgstr "" +"Wählen Sie die SEPA-Gruppen aus, für die Sie fehlgeschlagene Transaktionen " +"erneut einziehen möchten." #: templates/CRM/Sepa/Form/RetryCollection.tpl msgid "Add some filters" @@ -2842,12 +3089,6 @@ msgstr "Jetzt noch nicht" msgid "It's very important to select one of the options below." msgstr "Bitte unbedingt eine der Optionen unten auswählen." -#: templates/CRM/Sepa/Page/CreateMandate.tpl -#: templates/CRM/Sepa/Page/EditMandate.tpl -#: templates/CRM/Sepa/Page/ListGroup.tpl -msgid "Contact" -msgstr "Kontakt" - #: templates/CRM/Sepa/Page/CreateMandate.tpl msgid "Replacing Mandate" msgstr "Mandat ersetzen" @@ -2929,10 +3170,8 @@ msgid "Total" msgstr "Gesamt" #: templates/CRM/Sepa/Page/DashBoard.tpl -#, fuzzy -#| msgid "Transaction Message" msgid "Custom Transaction Message:" -msgstr "Verwendungszweck" +msgstr "Benutzerdefinierte Transaktionsnachricht:" #: templates/CRM/Sepa/Page/DashBoard.tpl msgid "Note:" @@ -3109,6 +3348,10 @@ msgstr "alle %1 löschen" msgid "delete pending %1" msgstr "ausstehende %1 löschen" +#: templates/CRM/Sepa/Page/DeleteGroup.tpl +msgid "recommended" +msgstr "" + #: templates/CRM/Sepa/Page/DeleteGroup.tpl msgid "" "You should consider deleting the open contributions. If the recurring " @@ -3310,10 +3553,6 @@ msgstr "SEPA Gruppe '%1' wurde erfolgreich gelöscht." msgid "Back" msgstr "Zurück" -#: templates/CRM/Sepa/Page/DeleteGroup.tpl -msgid "No group_id given!" -msgstr "Keine Gruppen-ID (group_id) angegeben." - #: templates/CRM/Sepa/Page/DeleteGroup.tpl msgid "Transaction group [%1] couldn't be loaded." msgstr "SEPA Gruppe [%1] konnte nicht geladen werden." @@ -3338,6 +3577,12 @@ msgid "" msgstr "" "Alle Nachrichtenvorlagen die mit 'SEPA' beginnen, werden hier angezeigt." +#: templates/CRM/Sepa/Page/EditMandate.hlp +msgid "" +"When you change the cycle day be aware that you might end up collecting " +"twice in the same month, or miss a collection altogether." +msgstr "" + #: templates/CRM/Sepa/Page/EditMandate.tpl msgid "SEPA Recurring Mandate" msgstr "SEPA Dauereinzugs-Mandat" @@ -3434,6 +3679,18 @@ msgstr "Mandat ändern zum:" msgid "Replace for the following reason:" msgstr "Grund der Änderung:" +#: templates/CRM/Sepa/Page/EditMandate.tpl +msgid "Change Cycle Day" +msgstr "Einzugstag ändern" + +#: templates/CRM/Sepa/Page/EditMandate.tpl +msgid "New cycle day:" +msgstr "Neuer Einzugstag:" + +#: templates/CRM/Sepa/Page/EditMandate.tpl +msgid "Cyle Day" +msgstr "Einzugstag" + #: templates/CRM/Sepa/Page/EditMandate.tpl msgid "Adjust Amount" msgstr "Betrag anpassen" @@ -3542,6 +3799,12 @@ msgstr "Mandat bearbeiten" msgid "This contact has no recorded recurring mandates." msgstr "Dieser Kontakt hat keine Mandate mit wiederkehrenden Zahlungen." +#: templates/CRM/Sepa/Page/MandateTab.tpl +msgid "" +"Note that only mandates associated with contributions of authorized " +"financial types are being displayed." +msgstr "" + #: templates/CRM/Sepa/Page/MandateTab.tpl msgid "One-Off SEPA Mandates" msgstr "Einmalige SEPA-Latschriften" @@ -3584,6 +3847,17 @@ msgstr "Fertig" msgid "Notes" msgstr "Notizen" +#: tests/phpunit/CRM/Sepa/BugReproductionTest.php +#: tests/phpunit/CRM/Sepa/MandateTest.php +msgid "OOFF Mandate status after closing is incorrect." +msgstr "" + +#: tests/phpunit/CRM/Sepa/BugReproductionTest.php +msgid "" +"OOFF contribution status after closing is incorrect, probably related to " +"SEPA-629" +msgstr "" + #: tests/phpunit/CRM/Sepa/HookTest.php msgid "The create_mandate hook has not been called." msgstr "" @@ -3690,10 +3964,6 @@ msgstr "" msgid "RCUR transaction group status after batching is incorrect." msgstr "" -#: tests/phpunit/CRM/Sepa/MandateTest.php -msgid "OOFF Mandate status after closing is incorrect." -msgstr "" - #: tests/phpunit/CRM/Sepa/MandateTest.php msgid "OOFF contribution status after closing is incorrect." msgstr "" @@ -3718,16 +3988,16 @@ msgstr "" msgid "Mandate status of new mandate is incorrect." msgstr "" -#: tests/phpunit/CRM/Sepa/ReferenceGenerationTest.php -msgid "The OOFF mandate reference is invalid." +#: tests/phpunit/CRM/Sepa/MandateTest.php +msgid "Mandate account holder after creation is incorrect." msgstr "" #: tests/phpunit/CRM/Sepa/ReferenceGenerationTest.php -msgid "The RCUR mandate reference is invalid." +msgid "The OOFF mandate reference is invalid." msgstr "" #: tests/phpunit/CRM/Sepa/ReferenceGenerationTest.php -msgid "There should be a clashing reference" +msgid "The RCUR mandate reference is invalid." msgstr "" #: tests/phpunit/CRM/Sepa/SettingsTest.php @@ -3739,9 +4009,7 @@ msgid "set/getSetting doesn't work" msgstr "" #: tests/phpunit/CRM/Sepa/TestBase.php -msgid "" -"The contribution for the mandate is null. That should not be possible at " -"this point." +msgid "This mandate has no contribution, even though there should be one." msgstr "" #: tests/phpunit/CRM/Sepa/VerifyBicTest.php @@ -3784,6 +4052,9 @@ msgstr "" msgid "Blocklistet IBAN should fail but did not!" msgstr "" +#~ msgid "Couldn't read transaction groups. Error was: '%s'" +#~ msgstr "Die Gruppen konnten nicht geladen werden. Fehler ist: '%s'" + #~ msgid "Does this creditor use BICs?" #~ msgstr "Verwendet dieser Kreditor BICs?" @@ -3867,9 +4138,6 @@ msgstr "" #~ "Beendigungsgrund für Mandat [%s] konnte nicht gesetzt werden. Fehler ist: " #~ "'%s'" -#~ msgid "Couldn't create note for contribution #%s" -#~ msgstr "Konnte die Zuwendungsnotiz für [%s] nicht erstellen" - #~ msgid "edit mandate" #~ msgstr "Mandat Bearbeiten" diff --git a/l10n/org.project60.sepa.pot b/l10n/org.project60.sepa.pot index a375ecbe..67776aa6 100644 --- a/l10n/org.project60.sepa.pot +++ b/l10n/org.project60.sepa.pot @@ -782,6 +782,10 @@ msgstr "" msgid "End Date" msgstr "" +#: CRM/Sepa/Form/CreateMandate.php +msgid "The mandate to clone/replace from does not exist or you do not have permission for it." +msgstr "" + #: CRM/Sepa/Form/CreateMandate.php msgid "You can only replace RCUR mandates" msgstr "" @@ -1415,7 +1419,7 @@ msgid "CiviSEPA Dashboard" msgstr "" #: CRM/Sepa/Page/DashBoard.php -msgid "Couldn't read transaction groups. Error was: '%s'" +msgid "Couldn't read transaction groups. Error was: %1" msgstr "" #: CRM/Sepa/Page/DashBoard.php @@ -3050,6 +3054,10 @@ msgstr "" msgid "This contact has no recorded recurring mandates." msgstr "" +#: templates/CRM/Sepa/Page/MandateTab.tpl +msgid "Note that only mandates associated with contributions of authorized financial types are being displayed." +msgstr "" + #: templates/CRM/Sepa/Page/MandateTab.tpl msgid "One-Off SEPA Mandates" msgstr "" diff --git a/templates/CRM/Sepa/Page/DashBoard.tpl b/templates/CRM/Sepa/Page/DashBoard.tpl index 9e5d1c42..f74f80dd 100644 --- a/templates/CRM/Sepa/Page/DashBoard.tpl +++ b/templates/CRM/Sepa/Page/DashBoard.tpl @@ -64,6 +64,12 @@
+{if $financialacls} +
+ {ts}Note that only groups with contributions of authorized financial types are being displayed.{/ts} +
+{/if} + From d694df2429414037e81f766d20fce6fefae8bb9f Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Fri, 23 Aug 2024 13:38:28 +0200 Subject: [PATCH 06/18] Dashboard: filter for SEPA transaction groups the user has permission to view all contributions --- .../Action/SepaTransactionGroup/GetAction.php | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Civi/Sepa/Api4/Action/SepaTransactionGroup/GetAction.php b/Civi/Sepa/Api4/Action/SepaTransactionGroup/GetAction.php index 282ce680..76e0b7a2 100644 --- a/Civi/Sepa/Api4/Action/SepaTransactionGroup/GetAction.php +++ b/Civi/Sepa/Api4/Action/SepaTransactionGroup/GetAction.php @@ -21,19 +21,27 @@ use Civi\Api4\Generic\DAOGetAction; use Civi\Api4\Generic\Result; +use Civi\Api4\SepaContributionGroup; class GetAction extends DAOGetAction { public function _run(Result $result) { - // Add unique joins for permission checks of Financial ACLs. if ($this->getCheckPermissions()) { - $contributionAlias = uniqid('contribution_'); + // Count permissioned contributions (in the join) and the total number of contributions in the transaction group, + // and extract those with matching counts, i.e. groups of which the user has permission to view all contributions. + $fullyPermissionedTxgroups = SepaContributionGroup::get() + ->addSelect( + 'txgroup_id', + 'COUNT(contribution.id) AS allowed_contributions', + 'COUNT(*) AS total_contributions' + ) + ->addJoin('Contribution AS contribution', 'LEFT', ['contribution.id', '=', 'contribution_id']) + ->addGroupBy('txgroup_id') + ->addHaving('allowed_contributions', '=', 'total_contributions', TRUE) + ->execute() + ->column('txgroup_id'); $this - ->addJoin( - 'Contribution AS ' . $contributionAlias, - 'INNER', - 'SepaContributionGroup' - ); + ->addWhere('id', 'IN', $fullyPermissionedTxgroups); } return parent::_run($result); } From 4a875cf19254c58b8de5175cbca0996075f18090 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Mon, 26 Aug 2024 13:15:06 +0200 Subject: [PATCH 07/18] Use API4 for retrieving contributions of SEPA transaction groups, filtering for permissioned financial types only --- CRM/Sepa/Page/ListGroup.php | 191 ++++++++++++++++----------------- l10n/de_DE/LC_MESSAGES/sepa.mo | Bin 72598 -> 73080 bytes l10n/de_DE/LC_MESSAGES/sepa.po | 18 +++- l10n/org.project60.sepa.pot | 8 +- 4 files changed, 111 insertions(+), 106 deletions(-) diff --git a/CRM/Sepa/Page/ListGroup.php b/CRM/Sepa/Page/ListGroup.php index 862bd82f..d7a585f3 100644 --- a/CRM/Sepa/Page/ListGroup.php +++ b/CRM/Sepa/Page/ListGroup.php @@ -14,119 +14,110 @@ | written permission from the original author(s). | +--------------------------------------------------------*/ +use CRM_Sepa_ExtensionUtil as E; +use Civi\Api4\SepaTransactionGroup; +use Civi\Api4\Contribution; + /** * back office sepa group content viewer * * @package CiviCRM_SEPA * */ - - -require_once 'CRM/Core/Page.php'; - class CRM_Sepa_Page_ListGroup extends CRM_Core_Page { function run() { - CRM_Utils_System::setTitle(ts('SEPA Group Contributions', array('domain' => 'org.project60.sepa'))); - if (isset($_REQUEST['group_id'])) { - // get some values - $group_id = (int) $_REQUEST['group_id']; - $financial_types = CRM_Contribute_PseudoConstant::financialType(); - - // load the group - $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('id'=>$group_id, 'version'=>3)); - if (isset($txgroup['is_error']) && $txgroup['is_error']) { - CRM_Core_Session::setStatus(sprintf(ts("Cannot read SEPA transaction group [%s]. Error was: '%s'", array('domain' => 'org.project60.sepa')), $group_id, $txgroup['error_message']), ts("Error", array('domain' => 'org.project60.sepa')), "error"); - } - - // get contribution status option group ID - $status_option_group_id = 99999; - $option_group = civicrm_api3('OptionGroup', 'get', array( - 'name' => 'contribution_status', - 'return' => 'id')); - if (!empty($option_group['id'])) { - $status_option_group_id = $option_group['id']; - } - - // load the group's contributions - $sql = " - SELECT - civicrm_sdd_txgroup.reference AS reference, - civicrm_contact.display_name AS contact_display_name, - civicrm_contact.contact_type AS contact_contact_type, - civicrm_contact.id AS contact_id, - civicrm_contribution.id AS contribution_id, - civicrm_contribution.total_amount AS contribution_amount, - civicrm_contribution.currency AS contribution_currency, - civicrm_contribution.financial_type_id AS contribution_financial_type_id, - civicrm_campaign.title AS contribution_campaign, - civicrm_option_value.label AS contribution_status - FROM - civicrm_sdd_txgroup - LEFT JOIN - civicrm_sdd_contribution_txgroup ON civicrm_sdd_txgroup.id = civicrm_sdd_contribution_txgroup.txgroup_id - LEFT JOIN - civicrm_contribution ON civicrm_contribution.id = civicrm_sdd_contribution_txgroup.contribution_id - LEFT JOIN - civicrm_contact ON civicrm_contact.id = civicrm_contribution.contact_id - LEFT JOIN - civicrm_campaign ON civicrm_campaign.id = civicrm_contribution.campaign_id - LEFT JOIN - civicrm_option_value ON civicrm_option_value.value = civicrm_contribution.contribution_status_id AND civicrm_option_value.option_group_id = {$status_option_group_id} - WHERE - civicrm_sdd_txgroup.id = $group_id;"; - - $total_amount = 0.0; - $total_count = 0; - $total_campaigns = array(); - $total_types = array(); - $total_contacts = array(); - $status_stats = array(); - $contact_base_link = CRM_Utils_System::url('civicrm/contact/view', '&reset=1&cid='); - $contribution_base_link = CRM_Utils_System::url('civicrm/contact/view/contribution', '&reset=1&id=_cid_&cid=_id_&action=view'); - $currency = NULL; - - $contributions = array(); - $result = CRM_Core_DAO::executeQuery($sql); - while ($result->fetch()) { - $currency = $result->contribution_currency; - $contributions[$total_count] = array( - 'contact_display_name' => $result->contact_display_name, - 'contact_type' => $result->contact_contact_type, - 'contact_id' => $result->contact_id, - 'contact_link' => $contact_base_link.$result->contact_id, - 'contribution_link' => str_replace('_id_', $result->contact_id, str_replace('_cid_', $result->contribution_id, $contribution_base_link)), - 'contribution_id' => $result->contribution_id, - 'contribution_status' => $result->contribution_status, - 'contribution_amount' => $result->contribution_amount, - 'contribution_amount_str' => CRM_Utils_Money::format($result->contribution_amount, $result->contribution_currency), - 'financial_type' => $financial_types[$result->contribution_financial_type_id], - 'campaign' => $result->contribution_campaign, - ); + CRM_Utils_System::setTitle(E::ts('SEPA Group Contributions')); + try { + $groupId = CRM_Utils_Request::retrieve('group_id', 'Integer', NULL, TRUE); + $txGroup = SepaTransactionGroup::get(TRUE) + ->selectRowCount() + ->addSelect( + 'id', + 'reference', + 'status_id', + 'COUNT(DISTINCT contribution.id) AS total_count', + 'SUM(contribution.total_amount) AS total_amount', + 'COUNT(DISTINCT contribution.campaign_id) AS different_campaigns', + 'COUNT(DISTINCT contribution.financial_type_id) AS different_types', + 'COUNT(DISTINCT contribution.contact_id) AS different_contacts' + ) + ->addJoin('Contribution AS contribution', 'INNER', 'SepaContributionGroup') + ->addWhere('id', '=', $groupId) + ->addGroupBy('id') + ->execute() + ->single(); + } + catch (CRM_Core_Exception $exception) { + CRM_Core_Error::statusBounce( + E::ts( + 'Cannot read SEPA transaction group [%1]. Error was: %2', + [ + 1 => $groupId, + 2 => $exception->getMessage(), + ] + ), + CRM_Utils_System::url('civicrm/sepa') + ); + } - $total_count += 1; - $total_amount += $result->contribution_amount; - $total_types[$result->contribution_financial_type_id] = 1; - $total_contacts[$result->contact_id] = 1; - $total_campaigns[$result->contribution_campaign] = 1; - $reference = $result->reference; - $status_stats[$result->contribution_status] = 1 + CRM_Utils_Array::value($result->contribution_status, $status_stats, 0); - } + $result = Contribution::get() + ->selectRowCount() + ->addSelect( + 'contact_id.display_name', + 'contact_id.contact_type', + 'contact_id.id', + 'id', + 'total_amount', + 'currency', + 'financial_type_id', + 'financial_type_id:label', + 'campaign_id.title', + 'contribution_status_id:label' + ) + ->addJoin('SepaTransactionGroup AS sepa_transaction_group', 'INNER', 'SepaContributionGroup') + ->addWhere('sepa_transaction_group.id', '=', $groupId) + ->execute(); + $statusStats = []; + $contributions = []; + foreach ($result as $contribution) { + $contributions[] = [ + 'contact_display_name' => $contribution['contact_id.display_name'], + 'contact_type' => $contribution['contact_id.contact_type'], + 'contact_id' => $contribution['contact_id.id'], + 'contact_link' => CRM_Utils_System::url( + 'civicrm/contact/view', + '&reset=1&cid=' . $contribution['contact_id.id'] + ), + 'contribution_link' => CRM_Utils_System::url( + 'civicrm/contact/view/contribution', + '&reset=1&id=' . $contribution['id'] . '&cid=' . $contribution['contact_id.id'] . '&action=view' + ), + 'contribution_id' => $contribution['id'], + 'contribution_status' => $contribution['contribution_status_id:label'], + 'contribution_amount' => $contribution['total_amount'], + 'contribution_amount_str' => CRM_Utils_Money::format($contribution['total_amount'], $contribution['currency']), + 'financial_type_id' => $contribution['financial_type_id'], + 'financial_type' => $contribution['financial_type_id:label'], + 'campaign' => $contribution['campaign_id.title'], + ]; + $statusStats[$contribution['contribution_status_id:label']] = + 1 + ($statusStats[$contribution['contribution_status_id:label']] ?? 0); } - $this->assign("txgroup", $txgroup); - $this->assign("reference", $reference); - $this->assign("group_id", $group_id); - $this->assign("total_count", $total_count); - $this->assign("total_amount", $total_amount); - $this->assign("total_amount_str", CRM_Utils_Money::format($total_amount, $currency)); - $this->assign("contributions", $contributions); - $this->assign("different_campaigns", count($total_campaigns)); - $this->assign("different_types", count($total_types)); - $this->assign("different_contacts", count($total_contacts)); - $this->assign("status_stats", $status_stats); + $this->assign('txgroup', $txGroup); + $this->assign('reference', $txGroup['reference']); + $this->assign('group_id', $groupId); + $this->assign('total_count', $txGroup['total_count']); + $this->assign('total_amount', $txGroup['total_amount']); + $this->assign('total_amount_str', CRM_Utils_Money::format($txGroup['total_amount'], $result->first()['currency'])); + $this->assign('contributions', $contributions); + $this->assign('different_campaigns', $txGroup['different_campaigns']); + $this->assign('different_types', $txGroup['different_types']); + $this->assign('different_contacts', $txGroup['different_contacts']); + $this->assign('status_stats', $statusStats); parent::run(); } -} \ No newline at end of file +} diff --git a/l10n/de_DE/LC_MESSAGES/sepa.mo b/l10n/de_DE/LC_MESSAGES/sepa.mo index 215cd11c62a14acc80233c7e07b72787e1cbd5eb..b9645a54e41b3e56d07ff82c101a8e362df20d4a 100644 GIT binary patch delta 14817 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 J(SN?2{D0sB8#n*} diff --git a/l10n/de_DE/LC_MESSAGES/sepa.po b/l10n/de_DE/LC_MESSAGES/sepa.po index ee0675a5..b2989acb 100644 --- a/l10n/de_DE/LC_MESSAGES/sepa.po +++ b/l10n/de_DE/LC_MESSAGES/sepa.po @@ -262,8 +262,7 @@ msgstr "" #: CRM/Sepa/BAO/SEPAMandate.php CRM/Sepa/Form/CreateMandate.php #: CRM/Sepa/Logic/Queue/Update.php CRM/Sepa/Logic/Status.php #: CRM/Sepa/Page/CloseGroup.php CRM/Sepa/Page/CreateMandate.php -#: CRM/Sepa/Page/DashBoard.php CRM/Sepa/Page/EditMandate.php -#: CRM/Sepa/Page/ListGroup.php js/SepaSettings.js +#: CRM/Sepa/Page/DashBoard.php CRM/Sepa/Page/EditMandate.php js/SepaSettings.js msgid "Error" msgstr "Fehler" @@ -1789,8 +1788,9 @@ msgid "SEPA Group Contributions" msgstr "Zuwendungen der SEPA Gruppe" #: CRM/Sepa/Page/ListGroup.php -msgid "Cannot read SEPA transaction group [%s]. Error was: '%s'" -msgstr "Die SEPA Gruppe [%s] konnte nicht geladen werden. Fehler ist: '%s'" +msgid "Cannot read SEPA transaction group [%1]. Error was: %2" +msgstr "" +"Die SEPA-Transaktionsgruppe [%1] konnte nicht geladen werden. Fehler: %2" #: CRM/Sepa/Page/MandateTab.php sepa.php msgid "SEPA Mandates" @@ -3149,6 +3149,14 @@ msgstr "Dauereinzüge Aktualisieren" msgid "retry collection" msgstr "Einzug erneut versuchen" +#: templates/CRM/Sepa/Page/DashBoard.tpl +msgid "" +"Note that only groups with contributions of authorized financial types are " +"being displayed." +msgstr "" +"Beachten Sie, dass nur Gruppen mit Zuwendungen angezeigt werden, für deren " +"Zuwendungsart Sie die Berechtigung haben." + #: templates/CRM/Sepa/Page/DashBoard.tpl msgid "Group Name" msgstr "Gruppenname" @@ -3804,6 +3812,8 @@ msgid "" "Note that only mandates associated with contributions of authorized " "financial types are being displayed." msgstr "" +"Beachten Sie, dass nur Mandate mit zugehörigen Zuwendungen angezeigt werden, " +"für deren Zuwendungsart Sie die Berechtigung haben." #: templates/CRM/Sepa/Page/MandateTab.tpl msgid "One-Off SEPA Mandates" diff --git a/l10n/org.project60.sepa.pot b/l10n/org.project60.sepa.pot index 67776aa6..84a6e4c5 100644 --- a/l10n/org.project60.sepa.pot +++ b/l10n/org.project60.sepa.pot @@ -178,7 +178,7 @@ msgstr "" msgid "Cannot close mandate [%s], batching in progress!" msgstr "" -#: CRM/Sepa/BAO/SEPAMandate.php CRM/Sepa/Form/CreateMandate.php CRM/Sepa/Logic/Queue/Update.php CRM/Sepa/Logic/Status.php CRM/Sepa/Page/CloseGroup.php CRM/Sepa/Page/CreateMandate.php CRM/Sepa/Page/DashBoard.php CRM/Sepa/Page/EditMandate.php CRM/Sepa/Page/ListGroup.php js/SepaSettings.js +#: CRM/Sepa/BAO/SEPAMandate.php CRM/Sepa/Form/CreateMandate.php CRM/Sepa/Logic/Queue/Update.php CRM/Sepa/Logic/Status.php CRM/Sepa/Page/CloseGroup.php CRM/Sepa/Page/CreateMandate.php CRM/Sepa/Page/DashBoard.php CRM/Sepa/Page/EditMandate.php js/SepaSettings.js msgid "Error" msgstr "" @@ -1519,7 +1519,7 @@ msgid "SEPA Group Contributions" msgstr "" #: CRM/Sepa/Page/ListGroup.php -msgid "Cannot read SEPA transaction group [%s]. Error was: '%s'" +msgid "Cannot read SEPA transaction group [%1]. Error was: %2" msgstr "" #: CRM/Sepa/Page/MandateTab.php sepa.php @@ -2550,6 +2550,10 @@ msgstr "" msgid "retry collection" msgstr "" +#: templates/CRM/Sepa/Page/DashBoard.tpl +msgid "Note that only groups with contributions of authorized financial types are being displayed." +msgstr "" + #: templates/CRM/Sepa/Page/DashBoard.tpl msgid "Group Name" msgstr "" From d4d95ed0b5699edb301be4be765c063063192b4d Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Fri, 13 Sep 2024 13:54:56 +0200 Subject: [PATCH 08/18] Add `financial_type_id` field to SepaTransactionGroup entity --- CRM/Sepa/DAO/SEPATransactionGroup.php | 29 ++++++++++++++ CRM/Sepa/Upgrader.php | 22 +++++++++- sql/sepa.sql | 1 + xml/schema/CRM/Sepa/TransactionGroup.xml | 51 ++++++++++++++---------- 4 files changed, 80 insertions(+), 23 deletions(-) diff --git a/CRM/Sepa/DAO/SEPATransactionGroup.php b/CRM/Sepa/DAO/SEPATransactionGroup.php index 54e812b1..ad86ab6e 100644 --- a/CRM/Sepa/DAO/SEPATransactionGroup.php +++ b/CRM/Sepa/DAO/SEPATransactionGroup.php @@ -67,6 +67,16 @@ class CRM_Sepa_DAO_SEPATransactionGroup extends CRM_Core_DAO { */ public $collection_date; + /** + * Financial type of contained contributions if CiviSEPA is generating groups + * matching financial types. + * + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $financial_type_id; + /** * Latest submission date * @@ -233,6 +243,25 @@ public static function &fields() { 'localizable' => 0, 'add' => NULL, ], + 'financial_type_id' => [ + 'name' => 'financial_type_id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('Financial Type ID'), + 'description' => E::ts('Financial type of contained contributions if CiviSEPA is generating groups matching financial types.'), + 'required' => FALSE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_sdd_txgroup.financial_type_id', + 'table_name' => 'civicrm_sdd_txgroup', + 'entity' => 'SEPATransactionGroup', + 'bao' => 'CRM_Sepa_DAO_SEPATransactionGroup', + 'localizable' => 0, + 'add' => NULL, + ], 'latest_submission_date' => [ 'name' => 'latest_submission_date', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, diff --git a/CRM/Sepa/Upgrader.php b/CRM/Sepa/Upgrader.php index bc73b238..179e3fa6 100644 --- a/CRM/Sepa/Upgrader.php +++ b/CRM/Sepa/Upgrader.php @@ -15,7 +15,7 @@ +--------------------------------------------------------*/ use CRM_Sepa_ExtensionUtil as E; - + /** * Collection of upgrade steps. */ @@ -491,4 +491,24 @@ public function upgrade_1804() { $customData->syncOptionGroup(E::path('resources/formats_option_group.json')); return TRUE; } + + public function upgrade_11301() { + $this->ctx->log->info('Adding financial_type_id column to civicrm_sdd_txgroup table.'); + $column = CRM_Core_DAO::singleValueQuery( + <<executeSql( + << +
{ts domain="org.project60.sepa"}Group Name{/ts}
- CRM/Sepa - SEPATransactionGroup - civicrm_sdd_txgroup + CRM/Sepa + SEPATransactionGroup + civicrm_sdd_txgrouptrue - - id - int unsigned - true - ID + + id + int unsigned + true + ID true - - - id - true - + + + id + true + reference @@ -41,6 +41,13 @@ datetime Target collection date + + + financial_type_id + int unsigned + Financial type of contained contributions if CiviSEPA is generating groups matching financial types. + + latest_submission_date datetime @@ -67,10 +74,10 @@ sdd_creditor_id 4.3 - - sdd_creditor_id -
civicrm_sdd_creditor
- id + + sdd_creditor_id + civicrm_sdd_creditor
+ id SET NULL
@@ -83,10 +90,10 @@ file_id sdd_file_id - - sdd_file_id - civicrm_sdd_file
- id + + sdd_file_id + civicrm_sdd_file
+ id SET NULL
From 7407b4258624adbc84d3c3b46f67c0eae010de81 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Fri, 13 Sep 2024 15:08:00 +0200 Subject: [PATCH 09/18] Add a setting for grouping by financial types --- CRM/Admin/Form/Setting/SepaSettings.php | 3 +++ settings/sepa.setting.php | 13 ++++++++++++- templates/CRM/Admin/Form/Setting/SepaSettings.hlp | 7 ++++++- templates/CRM/Admin/Form/Setting/SepaSettings.tpl | 6 ++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CRM/Admin/Form/Setting/SepaSettings.php b/CRM/Admin/Form/Setting/SepaSettings.php index cd77ac56..0933efa9 100644 --- a/CRM/Admin/Form/Setting/SepaSettings.php +++ b/CRM/Admin/Form/Setting/SepaSettings.php @@ -132,6 +132,7 @@ public function buildQuickForm( ) { // look up some values $async_batch = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_async_batching'); + $financial_type_grouping = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_financial_type_grouping'); $skip_closed = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_skip_closed'); $no_draftxml = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_no_draft_xml'); $excld_we = CRM_Sepa_Logic_Settings::getGenericSetting('exclude_weekends'); @@ -157,6 +158,7 @@ public function buildQuickForm( ) { $this->addElement('checkbox', 'is_test_creditor', E::ts("Is a Test Creditor"), "", array('value' =>'0')); $this->addElement('checkbox', 'exclude_weekends', E::ts("Exclude Weekends"), "", ($excld_we?array('checked'=>'checked'):array())); $this->addElement('checkbox', 'sdd_async_batching', E::ts("Large Groups"), "", ($async_batch?array('checked'=>'checked'):array())); + $this->addElement('checkbox', 'sdd_financial_type_grouping', E::ts('Groups by Financial Types'), "", ($financial_type_grouping?array('checked'=>'checked'):array())); $this->addElement('checkbox', 'sdd_skip_closed', E::ts("Only Completed Contributions"), "", ($skip_closed?array('checked'=>'checked'):array())); $this->addElement('checkbox', 'sdd_no_draft_xml', E::ts("No XML drafts"), "", ($no_draftxml?array('checked'=>'checked'):array())); $this->addElement('text', 'pp_buffer_days', E::ts("Buffer Days"), array('size' => 2, 'value' => $bffrdays)); @@ -252,6 +254,7 @@ function postProcess() { CRM_Sepa_Logic_Settings::setSetting((isset($values['exclude_weekends']) ? "1" : "0"), 'exclude_weekends'); CRM_Sepa_Logic_Settings::setSetting((isset($values['sdd_async_batching']) ? "1" : "0"), 'sdd_async_batching'); + CRM_Sepa_Logic_Settings::setSetting((isset($values['sdd_financial_type_grouping']) ? "1" : "0"), 'sdd_financial_type_grouping'); CRM_Sepa_Logic_Settings::setSetting((isset($values['sdd_skip_closed']) ? "1" : "0"), 'sdd_skip_closed'); CRM_Sepa_Logic_Settings::setSetting((isset($values['sdd_no_draft_xml']) ? "1" : "0"), 'sdd_no_draft_xml'); CRM_Sepa_Logic_Settings::setSetting((isset($values['pp_buffer_days']) ? (int) $values['pp_buffer_days'] : "0"), 'pp_buffer_days'); diff --git a/settings/sepa.setting.php b/settings/sepa.setting.php index ee162d11..40e3eef7 100644 --- a/settings/sepa.setting.php +++ b/settings/sepa.setting.php @@ -273,6 +273,17 @@ 'is_contact' => 0, 'description' => 'Enables asychronous batching', ), + 'sdd_financial_type_grouping' => array( + 'group_name' => 'SEPA Direct Debit Preferences', + 'group' => 'org.project60', + 'name' => 'sdd_financial_type_grouping', + 'type' => 'Boolean', + 'html_type' => 'checkbox', + 'default' => 0, + 'is_domain' => 1, + 'is_contact' => 0, + 'description' => 'Groups by Financial Types.', + ), 'sdd_skip_closed' => array( 'group_name' => 'SEPA Direct Debit Preferences', 'group' => 'org.project60', @@ -308,4 +319,4 @@ 'description' => "Contribution page buffer (in days) before debit needs to be collected", 'help_text' => "Contribution page buffer (in days) before debit needs to be collected", ) - ); \ No newline at end of file + ); diff --git a/templates/CRM/Admin/Form/Setting/SepaSettings.hlp b/templates/CRM/Admin/Form/Setting/SepaSettings.hlp index de0e645f..62463bdc 100644 --- a/templates/CRM/Admin/Form/Setting/SepaSettings.hlp +++ b/templates/CRM/Admin/Form/Setting/SepaSettings.hlp @@ -150,6 +150,11 @@

{ts}This option will activate a runner page with a progress bar for these critical processes. There shouldn't be any timeouts, but you have to keep that open until it's done.{/ts}

{/htxt} +{htxt id='id-financial-type-grouping'} +

{ts}Whether to group transactions by financial types additionally.{/ts}

+

{ts}Enable this settings if you use the Financial ACLs extension, as transaction groups with mixed financial type contributions can cause issues with overlapping Financial ACLs' permissions.{/ts}

+{/htxt} + {htxt id='id-creditor-type'}

{ts}Always use SEPA if you want to do SEPA style direct debit.{/ts}

{ts}PSP can help you to use this extension for other direct debit systems or payment service providers (PSP), but all SEPA style validations and tools are then disabled.{/ts}

@@ -178,4 +183,4 @@

{ts}CUC-code ("Codice Univoco CBI") of the financial institution of the creditor. It is only required for CBIBdySDDReq SEPA file format.{/ts}

{ts}Be careful when changing this!{/ts}

{/htxt} -{/crmScope} \ No newline at end of file +{/crmScope} diff --git a/templates/CRM/Admin/Form/Setting/SepaSettings.tpl b/templates/CRM/Admin/Form/Setting/SepaSettings.tpl index 0f355732..437f9c7b 100644 --- a/templates/CRM/Admin/Form/Setting/SepaSettings.tpl +++ b/templates/CRM/Admin/Form/Setting/SepaSettings.tpl @@ -338,6 +338,12 @@ div.sdd-add-creditor { {$form.sdd_async_batching.html} + + {$form.sdd_financial_type_grouping.label}   + + {$form.sdd_financial_type_grouping.html} + +
{include file="CRM/common/formButtons.tpl" location="bottom"}
From dc6a302b0e77a07b2da60f779829531c7b61a1c2 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Fri, 13 Sep 2024 15:12:56 +0200 Subject: [PATCH 10/18] Update OOFF transaction groups with financial type grouping --- CRM/Sepa/Logic/Batching.php | 248 ++++++++++++++++----------- CRM/Utils/SepaCustomisationHooks.php | 6 +- 2 files changed, 151 insertions(+), 103 deletions(-) diff --git a/CRM/Sepa/Logic/Batching.php b/CRM/Sepa/Logic/Batching.php index f2bbf30f..beb84d07 100644 --- a/CRM/Sepa/Logic/Batching.php +++ b/CRM/Sepa/Logic/Batching.php @@ -264,49 +264,38 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim * @param $offset used for segmented updates * @param $limit used for segmented updates */ - static function updateOOFF($creditor_id, $now = 'now', $offset=NULL, $limit=NULL) { + static function updateOOFF($creditor_id, $now = 'now', $offset = NULL, $limit = NULL) { // check lock $lock = CRM_Sepa_Logic_Settings::getLock(); if (empty($lock)) { return "Batching in progress. Please try again later."; } - if ($offset !== NULL && $limit!==NULL) { - $batch_clause = "LIMIT {$limit} OFFSET {$offset}"; - } else { - $batch_clause = ""; - } - $horizon = (int) CRM_Sepa_Logic_Settings::getSetting('batching.OOFF.horizon', $creditor_id); $ooff_notice = (int) CRM_Sepa_Logic_Settings::getSetting('batching.OOFF.notice', $creditor_id); $group_status_id_open = (int) CRM_Core_PseudoConstant::getKey('CRM_Batch_BAO_Batch', 'status_id', 'Open'); $date_limit = date('Y-m-d', strtotime("$now +$horizon days")); - // step 1: find all active/pending OOFF mandates within the horizon that are NOT in a closed batch - $sql_query = " - SELECT - mandate.id AS mandate_id, - mandate.contact_id AS mandate_contact_id, - mandate.entity_id AS mandate_entity_id, - contribution.receive_date AS start_date - FROM civicrm_sdd_mandate AS mandate - INNER JOIN civicrm_contribution AS contribution ON mandate.entity_id = contribution.id AND mandate.entity_table = 'civicrm_contribution' - WHERE contribution.receive_date <= DATE('$date_limit') - AND mandate.type = 'OOFF' - AND mandate.status = 'OOFF' - AND mandate.creditor_id = $creditor_id - {$batch_clause};"; - $results = CRM_Core_DAO::executeQuery($sql_query); - $relevant_mandates = array(); - while ($results->fetch()) { - // TODO: sanity checks? - $relevant_mandates[$results->mandate_id] = array( - 'mandate_id' => $results->mandate_id, - 'mandate_contact_id' => $results->mandate_contact_id, - 'mandate_entity_id' => $results->mandate_entity_id, - 'start_date' => $results->start_date, - ); - } + // step 1: find all active/pending OOFF mandates within the horizon that are NOT in a closed batch and that have a + // corresponding contribution of a financial type the user has access to (implicit condition added by Financial ACLs + // extension if enabled). + $relevant_mandates = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('id', 'contact_id', 'entity_id', 'contribution.receive_date', 'contribution.financial_type_id') + ->addJoin( + 'Contribution AS contribution', + 'INNER', + ['entity_table', '=', "'civicrm_contribution'"], + ['entity_id', '=', 'contribution.id'] + ) + ->addWhere('contribution.receive_date', '<=', $date_limit) + ->addWhere('type', '=', 'OOFF') + ->addWhere('status', '=', 'OOFF') + ->addWhere('creditor_id', '=', $creditor_id) + ->setLimit($limit ?? 0) + ->setOffset($offset ?? 0) + ->execute() + ->indexBy('id') + ->getArrayCopy(); // step 2: group mandates in collection dates $calculated_groups = array(); @@ -314,16 +303,21 @@ static function updateOOFF($creditor_id, $now = 'now', $offset=NULL, $limit=NULL $latest_collection_date = ''; foreach ($relevant_mandates as $mandate_id => $mandate) { + $mandate['mandate_id'] = $mandate['id']; + $mandate['mandate_contact_id'] = $mandate['contact_id']; + $mandate['mandate_entity_id'] = $mandate['entity_id']; + $mandate['start_date'] = $mandate['contribution.receive_date']; + $mandate['financial_type_id'] = $mandate['contribution.financial_type_id']; $collection_date = date('Y-m-d', strtotime($mandate['start_date'])); if ($collection_date <= $earliest_collection_date) { $collection_date = $earliest_collection_date; } - if (!isset($calculated_groups[$collection_date])) { - $calculated_groups[$collection_date] = array(); + if (!isset($calculated_groups[$collection_date][$mandate['financial_type_id']])) { + $calculated_groups[$collection_date][$mandate['financial_type_id']] = []; } - array_push($calculated_groups[$collection_date], $mandate); + array_push($calculated_groups[$collection_date][$mandate['financial_type_id']], $mandate); if ($collection_date > $latest_collection_date) { $latest_collection_date = $collection_date; @@ -331,13 +325,14 @@ static function updateOOFF($creditor_id, $now = 'now', $offset=NULL, $limit=NULL } if (!$latest_collection_date) { // nothing to do... - return array(); + return []; } // step 3: find all existing OPEN groups in the horizon $sql_query = " SELECT txgroup.collection_date AS collection_date, + txgroup.financial_type_id AS financial_type_id, txgroup.id AS txgroup_id FROM civicrm_sdd_txgroup AS txgroup WHERE txgroup.sdd_creditor_id = $creditor_id @@ -347,11 +342,20 @@ static function updateOOFF($creditor_id, $now = 'now', $offset=NULL, $limit=NULL $existing_groups = array(); while ($results->fetch()) { $collection_date = date('Y-m-d', strtotime($results->collection_date)); - $existing_groups[$collection_date] = $results->txgroup_id; + $existing_groups[$collection_date][$results->financial_type_id ?? 0] = $results->txgroup_id; } // step 4: sync calculated group structure with existing (open) groups - self::syncGroups($calculated_groups, $existing_groups, 'OOFF', 'OOFF', $ooff_notice, $creditor_id, $offset!==NULL, $offset===0); + self::syncGroups( + $calculated_groups, + $existing_groups, + 'OOFF', + 'OOFF', + $ooff_notice, + $creditor_id, + $offset !== NULL, + $offset === 0 + ); $lock->release(); } @@ -449,100 +453,114 @@ static function closeEnded() { * @param $partial_groups Is this a partial update? * @param $partial_first Is this the first call in a partial update? */ - protected static function syncGroups($calculated_groups, $existing_groups, $mode, $type, $notice, $creditor_id, $partial_groups=FALSE, $partial_first=FALSE) { + protected static function syncGroups( + $calculated_groups, + $existing_groups, + $mode, + $type, + $notice, + $creditor_id, + $partial_groups=FALSE, + $partial_first=FALSE + ) { $group_status_id_open = (int) CRM_Core_PseudoConstant::getKey('CRM_Batch_BAO_Batch', 'status_id', 'Open'); - foreach ($calculated_groups as $collection_date => $mandates) { + foreach ($calculated_groups as $collection_date => $financial_type_groups) { // check if we need to defer the collection date (e.g. due to bank holidays) self::deferCollectionDate($collection_date, $creditor_id); - if (!isset($existing_groups[$collection_date])) { - // this group does not yet exist -> create + // If not using financial type grouping, flatten to a "0" financial type. + if (!CRM_Sepa_Logic_Settings::getGenericSetting('sdd_financial_type_grouping')) { + $financial_type_groups = [0 => array_merge(...$financial_type_groups)]; + } - // find unused reference - $reference = "TXG-{$creditor_id}-{$mode}-{$collection_date}"; - $counter = 0; - while (self::referenceExists($reference)) { - $counter += 1; - $reference = "TXG-{$creditor_id}-{$mode}-{$collection_date}--".$counter; + foreach ($financial_type_groups as $financial_type_id => $mandates) { + if (0 === $financial_type_id) { + $financial_type_id = NULL; } + if (!isset($existing_groups[$collection_date][$financial_type_id ?? 0])) { + // this group does not yet exist -> create - // call the hook - CRM_Utils_SepaCustomisationHooks::modify_txgroup_reference($reference, $creditor_id, $mode, $collection_date); + // find unused reference + $reference = self::getTransactionGroupReference($creditor_id, $mode, $collection_date, $financial_type_id); - $group = civicrm_api('SepaTransactionGroup', 'create', array( + $group = civicrm_api('SepaTransactionGroup', 'create', array( 'version' => 3, 'reference' => $reference, 'type' => $mode, 'collection_date' => $collection_date, + 'financial_type_id' => $financial_type_id, 'latest_submission_date' => date('Y-m-d', strtotime("-$notice days", strtotime($collection_date))), 'created_date' => date('Y-m-d'), 'status_id' => $group_status_id_open, 'sdd_creditor_id' => $creditor_id, - )); - if (!empty($group['is_error'])) { - // TODO: Error handling - Civi::log()->debug("org.project60.sepa: batching:syncGroups/createGroup ".$group['error_message']); + )); + if (!empty($group['is_error'])) { + // TODO: Error handling + Civi::log()->debug("org.project60.sepa: batching:syncGroups/createGroup ".$group['error_message']); + } } - } else { - $group = civicrm_api('SepaTransactionGroup', 'getsingle', array('version' => 3, 'id' => $existing_groups[$collection_date], 'status_id' => $group_status_id_open)); - if (!empty($group['is_error'])) { - // TODO: Error handling - Civi::log()->debug("org.project60.sepa: batching:syncGroups/getGroup ".$group['error_message']); + else { + $group = civicrm_api('SepaTransactionGroup', 'getsingle', array('version' => 3, 'id' => $existing_groups[$collection_date][$financial_type_id ?? 0], 'status_id' => $group_status_id_open)); + if (!empty($group['is_error'])) { + // TODO: Error handling + Civi::log()->debug("org.project60.sepa: batching:syncGroups/getGroup ".$group['error_message']); + } + unset($existing_groups[$collection_date][$financial_type_id ?? 0]); } - unset($existing_groups[$collection_date]); - } - // now we have the right group. Prepare some parameters... - $group_id = $group['id']; - $entity_ids = array(); - foreach ($mandates as $mandate) { - // remark: "mandate_entity_id" in this case means the contribution ID - if (empty($mandate['mandate_entity_id'])) { - // this shouldn't happen - Civi::log()->debug("org.project60.sepa: batching:syncGroups mandate with bad mandate_entity_id ignored:" . $mandate['mandate_id']); - } else { - array_push($entity_ids, $mandate['mandate_entity_id']); + // now we have the right group. Prepare some parameters... + $group_id = $group['id']; + $entity_ids = []; + foreach ($mandates as $mandate) { + // remark: "mandate_entity_id" in this case means the contribution ID + if (empty($mandate['mandate_entity_id'])) { + // this shouldn't happen + Civi::log()->debug("org.project60.sepa: batching:syncGroups mandate with bad mandate_entity_id ignored:" . $mandate['mandate_id']); + } + else { + array_push($entity_ids, $mandate['mandate_entity_id']); + } } - } - if (count($entity_ids)<=0) continue; + if (count($entity_ids)<=0) continue; - // now, filter out the entity_ids that are are already in a non-open group - // (DO NOT CHANGE CLOSED GROUPS!) - $entity_ids_list = implode(',', $entity_ids); - $already_sent_contributions = CRM_Core_DAO::executeQuery(" + // now, filter out the entity_ids that are are already in a non-open group + // (DO NOT CHANGE CLOSED GROUPS!) + $entity_ids_list = implode(',', $entity_ids); + $already_sent_contributions = CRM_Core_DAO::executeQuery(" SELECT contribution_id FROM civicrm_sdd_contribution_txgroup LEFT JOIN civicrm_sdd_txgroup ON civicrm_sdd_contribution_txgroup.txgroup_id = civicrm_sdd_txgroup.id WHERE contribution_id IN ($entity_ids_list) AND civicrm_sdd_txgroup.status_id <> $group_status_id_open;"); - while ($already_sent_contributions->fetch()) { - $index = array_search($already_sent_contributions->contribution_id, $entity_ids); - if ($index !== false) unset($entity_ids[$index]); - } - if (count($entity_ids)<=0) continue; + while ($already_sent_contributions->fetch()) { + $index = array_search($already_sent_contributions->contribution_id, $entity_ids); + if ($index !== false) unset($entity_ids[$index]); + } + if (count($entity_ids)<=0) continue; - // remove all the unwanted entries from our group - $entity_ids_list = implode(',', $entity_ids); - if (!$partial_groups || $partial_first) { - CRM_Core_DAO::executeQuery("DELETE FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id=$group_id AND contribution_id NOT IN ($entity_ids_list);"); - } + // remove all the unwanted entries from our group + $entity_ids_list = implode(',', $entity_ids); + if (!$partial_groups || $partial_first) { + CRM_Core_DAO::executeQuery("DELETE FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id=$group_id AND contribution_id NOT IN ($entity_ids_list);"); + } - // remove all our entries from other groups, if necessary - CRM_Core_DAO::executeQuery("DELETE FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id!=$group_id AND contribution_id IN ($entity_ids_list);"); + // remove all our entries from other groups, if necessary + CRM_Core_DAO::executeQuery("DELETE FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id!=$group_id AND contribution_id IN ($entity_ids_list);"); - // now check which ones are already in our group... - $existing = CRM_Core_DAO::executeQuery("SELECT * FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id=$group_id AND contribution_id IN ($entity_ids_list);"); - while ($existing->fetch()) { - // remove from entity ids, if in there: - if(($key = array_search($existing->contribution_id, $entity_ids)) !== false) { - unset($entity_ids[$key]); + // now check which ones are already in our group... + $existing = CRM_Core_DAO::executeQuery("SELECT * FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id=$group_id AND contribution_id IN ($entity_ids_list);"); + while ($existing->fetch()) { + // remove from entity ids, if in there: + if(($key = array_search($existing->contribution_id, $entity_ids)) !== false) { + unset($entity_ids[$key]); + } } - } - // the remaining must be added - foreach ($entity_ids as $entity_id) { - CRM_Core_DAO::executeQuery("INSERT INTO civicrm_sdd_contribution_txgroup (txgroup_id, contribution_id) VALUES ($group_id, $entity_id);"); + // the remaining must be added + foreach ($entity_ids as $entity_id) { + CRM_Core_DAO::executeQuery("INSERT INTO civicrm_sdd_contribution_txgroup (txgroup_id, contribution_id) VALUES ($group_id, $entity_id);"); + } } } @@ -562,6 +580,36 @@ public static function referenceExists($reference) { return !(isset($query['is_error']) && $query['is_error']); } + public static function getTransactionGroupReference( + int $creditorId, + string $mode, + string $collectionDate, + ?int $financialTypeId = NULL + ): string { + $defaultReference = "TXG-{$creditorId}-{$mode}-{$collectionDate}"; + if (isset($financialTypeId)) { + $defaultReference .= "-{$financialTypeId}"; + } + + $counter = 0; + $reference = $defaultReference; + while (self::referenceExists($reference)) { + $counter += 1; + $reference = "{$defaultReference}--".$counter; + } + + // Call the hook. + CRM_Utils_SepaCustomisationHooks::modify_txgroup_reference( + $reference, + $creditorId, + $mode, + $collectionDate, + $financialTypeId + ); + + return $reference; + } + /** * Calculate the next execution date for a recurring contribution */ diff --git a/CRM/Utils/SepaCustomisationHooks.php b/CRM/Utils/SepaCustomisationHooks.php index 5b202da6..0c044c19 100644 --- a/CRM/Utils/SepaCustomisationHooks.php +++ b/CRM/Utils/SepaCustomisationHooks.php @@ -60,9 +60,9 @@ static function create_mandate(&$mandate_parameters) { * * @access public */ - static function modify_txgroup_reference(&$reference, $creditor_id, $mode, $collection_date) { - $names = ['reference', 'creditor_id', 'mode', 'collection_date']; - return CRM_Utils_Hook::singleton()->invoke($names, $reference, $creditor_id, $mode, $collection_date, self::$null, self::$null, 'civicrm_modify_txgroup_reference'); + static function modify_txgroup_reference(&$reference, $creditor_id, $mode, $collection_date, $financial_type_id) { + $names = ['reference', 'creditor_id', 'mode', 'collection_date', 'financial_type_id']; + return CRM_Utils_Hook::singleton()->invoke($names, $reference, $creditor_id, $mode, $collection_date, $financial_type_id, self::$null, 'civicrm_modify_txgroup_reference'); } From 4ca9b6549a5b329f8e384da842ccd65c3c7b6084 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Tue, 17 Sep 2024 16:06:37 +0200 Subject: [PATCH 11/18] Update RCUR transaction groups with financial type grouping --- CRM/Sepa/Logic/Batching.php | 261 +++++++++++++++++++++--------------- 1 file changed, 150 insertions(+), 111 deletions(-) diff --git a/CRM/Sepa/Logic/Batching.php b/CRM/Sepa/Logic/Batching.php index beb84d07..fbc2de58 100644 --- a/CRM/Sepa/Logic/Batching.php +++ b/CRM/Sepa/Logic/Batching.php @@ -34,6 +34,7 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim if (empty($lock)) { return "Batching in progress. Please try again later."; } + $horizon = (int) CRM_Sepa_Logic_Settings::getSetting("batching.RCUR.horizon", $creditor_id); $grace_period = (int) CRM_Sepa_Logic_Settings::getSetting("batching.RCUR.grace", $creditor_id); $latest_date = date('Y-m-d', strtotime("$now +$horizon days")); @@ -54,85 +55,104 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim if ($offset !== NULL && $limit!==NULL) { $batch_clause = "LIMIT {$limit} OFFSET {$offset}"; - } else { + } + else { $batch_clause = ""; } // RCUR-STEP 0: check/repair mandates + // TODO: Does this need changes for Financial ACLs? CRM_Sepa_Logic_MandateRepairs::runWithMandateSelector( "mandate.type = 'RCUR' AND mandate.status = '{$mode}' AND mandate.creditor_id = {$creditor_id} {$batch_clause}", true ); - // RCUR-STEP 1: find all active/pending RCUR mandates within the horizon that are NOT in a closed batch - $sql_query = " - SELECT - mandate.id AS mandate_id, - mandate.contact_id AS mandate_contact_id, - mandate.entity_id AS mandate_entity_id, - mandate.source AS mandate_source, - mandate.creditor_id AS mandate_creditor_id, - first_contribution.receive_date AS mandate_first_executed, - rcontribution.cycle_day AS cycle_day, - rcontribution.frequency_interval AS frequency_interval, - rcontribution.frequency_unit AS frequency_unit, - rcontribution.start_date AS start_date, - rcontribution.cancel_date AS cancel_date, - rcontribution.end_date AS end_date, - rcontribution.amount AS rc_amount, - rcontribution.is_test AS rc_is_test, - rcontribution.contact_id AS rc_contact_id, - rcontribution.financial_type_id AS rc_financial_type_id, - rcontribution.contribution_status_id AS rc_contribution_status_id, - rcontribution.currency AS rc_currency, - rcontribution.campaign_id AS rc_campaign_id, - rcontribution.payment_instrument_id AS rc_payment_instrument_id - FROM civicrm_sdd_mandate AS mandate - INNER JOIN civicrm_contribution_recur AS rcontribution ON mandate.entity_id = rcontribution.id AND mandate.entity_table = 'civicrm_contribution_recur' - LEFT JOIN civicrm_contribution AS first_contribution ON mandate.first_contribution_id = first_contribution.id - WHERE mandate.type = 'RCUR' - AND mandate.status = '{$mode}' - AND mandate.creditor_id = {$creditor_id} - {$batch_clause};"; - $results = CRM_Core_DAO::executeQuery($sql_query); - $relevant_mandates = array(); - while ($results->fetch()) { - // TODO: sanity checks? - $relevant_mandates[$results->mandate_id] = array( - 'mandate_id' => $results->mandate_id, - 'mandate_contact_id' => $results->mandate_contact_id, - 'mandate_entity_id' => $results->mandate_entity_id, - 'mandate_first_executed' => $results->mandate_first_executed, - 'mandate_source' => $results->mandate_source, - 'mandate_creditor_id' => $results->mandate_creditor_id, - 'cycle_day' => $results->cycle_day, - 'frequency_interval' => $results->frequency_interval, - 'frequency_unit' => $results->frequency_unit, - 'start_date' => $results->start_date, - 'end_date' => $results->end_date, - 'cancel_date' => $results->cancel_date, - 'rc_contact_id' => $results->rc_contact_id, - 'rc_amount' => $results->rc_amount, - 'rc_currency' => $results->rc_currency, - 'rc_financial_type_id' => $results->rc_financial_type_id, - 'rc_contribution_status_id' => $results->rc_contribution_status_id, - 'rc_campaign_id' => $results->rc_campaign_id, - 'rc_payment_instrument_id' => $results->rc_payment_instrument_id, - 'rc_is_test' => $results->rc_is_test, - ); + // RCUR-STEP 1: find all active/pending RCUR mandates within the horizon that are NOT in a closed batch and that + // have a corresponding contribution of a financial type the user has access to (implicit condition added by + // Financial ACLs extension if enabled) + $relevant_mandates = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect( + 'id', + 'contact_id', + 'entity_id', + 'source', + 'creditor_id', + 'first_contribution.receive_date', + 'contribution_recur.cycle_day', + 'contribution_recur.frequency_interval', + 'contribution_recur.frequency_unit', + 'contribution_recur.start_date', + 'contribution_recur.cancel_date', + 'contribution_recur.end_date', + 'contribution_recur.amount', + 'contribution_recur.is_test', + 'contribution_recur.contact_id', + 'contribution_recur.financial_type_id', + 'contribution_recur.contribution_status_id', + 'contribution_recur.currency', + 'contribution_recur.campaign_id', + 'contribution_recur.payment_instrument_id' + ) + ->addJoin( + 'ContributionRecur AS contribution_recur', + 'INNER', + ['entity_table', '=', '"civicrm_contribution_recur"'], + ['entity_id', '=', 'contribution_recur.id'] + ) + ->addJoin( + 'Contribution AS first_contribution', + 'LEFT', + ['first_contribution_id', '=', 'first_contribution.id'] + ) + ->addWhere('type', '=', 'RCUR') + ->addWhere('status', '=', $mode) + ->addWhere('creditor_id', '=', $creditor_id) + ->setLimit($limit) + ->setOffset($offset) + ->execute() + ->indexBy('id') + ->getArrayCopy(); + + foreach ($relevant_mandates as &$mandate) { + $mandate += [ + 'mandate_id' => $mandate['id'], + 'mandate_contact_id' => $mandate['contact_id'], + 'mandate_entity_id' => $mandate['entity_id'], + 'mandate_first_executed' => $mandate['first_contribution.receive_date'], + 'mandate_source' => $mandate['source'], + 'mandate_creditor_id' => $mandate['creditor_id'], + 'cycle_day' => $mandate['contribution_recur.cycle_day'], + 'frequency_interval' => $mandate['contribution_recur.frequency_interval'], + 'frequency_unit' => $mandate['contribution_recur.frequency_unit'], + 'start_date' => $mandate['contribution_recur.start_date'], + 'end_date' => $mandate['contribution_recur.end_date'], + 'cancel_date' => $mandate['contribution_recur.cancel_date'], + 'rc_contact_id' => $mandate['contribution_recur.contact_id'], + 'rc_amount' => $mandate['contribution_recur.amount'], + 'rc_currency' => $mandate['contribution_recur.currency'], + 'rc_financial_type_id' => $mandate['contribution_recur.financial_type_id'], + 'rc_contribution_status_id' => $mandate['contribution_recur.contribution_status_id'], + 'rc_campaign_id' => $mandate['contribution_recur.campaign_id'], + 'rc_payment_instrument_id' => $mandate['contribution_recur.payment_instrument_id'], + 'rc_is_test' => $mandate['contribution_recur.is_test'], + ]; } // RCUR-STEP 2: calculate next execution date - $mandates_by_nextdate = array(); + $mandates_by_nextdate = []; foreach ($relevant_mandates as $mandate) { $next_date = self::getNextExecutionDate($mandate, $now, ($mode=='FRST')); - if ($next_date==NULL) continue; - if ($next_date > $latest_date) continue; - - if (!isset($mandates_by_nextdate[$next_date])) - $mandates_by_nextdate[$next_date] = array(); - array_push($mandates_by_nextdate[$next_date], $mandate); + if (NULL === $next_date || $next_date > $latest_date) { + continue; + } + if (!isset($mandates_by_nextdate[$next_date])) { + $mandates_by_nextdate[$next_date] = []; + } + if (!isset($mandates_by_nextdate[$next_date][$mandate['rc_financial_type_id']])) { + $mandates_by_nextdate[$next_date][$mandate['rc_financial_type_id']] = []; + } + array_push($mandates_by_nextdate[$next_date][$mandate['rc_financial_type_id']], $mandate); } // apply any deferrals: $collection_dates = array_keys($mandates_by_nextdate); @@ -142,8 +162,12 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim if ($deferred_collection_date != $collection_date) { if (empty($mandates_by_nextdate[$deferred_collection_date])) { $mandates_by_nextdate[$deferred_collection_date] = $mandates_by_nextdate[$collection_date]; - } else { - $mandates_by_nextdate[$deferred_collection_date] = array_merge($mandates_by_nextdate[$collection_date], $mandates_by_nextdate[$deferred_collection_date]); + } + else { + $mandates_by_nextdate[$deferred_collection_date] = array_merge( + $mandates_by_nextdate[$collection_date], + $mandates_by_nextdate[$deferred_collection_date] + ); } unset($mandates_by_nextdate[$collection_date]); } @@ -151,15 +175,16 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim // RCUR-STEP 3: find already created contributions - $existing_contributions_by_recur_id = array(); - foreach ($mandates_by_nextdate as $collection_date => $mandates) { - $rcontrib_ids = array(); - foreach ($mandates as $mandate) { - array_push($rcontrib_ids, $mandate['mandate_entity_id']); - } - $rcontrib_id_strings = implode(',', $rcontrib_ids); + $existing_contributions_by_recur_id = []; + foreach ($mandates_by_nextdate as $collection_date => $financial_type_mandates) { + foreach ($financial_type_mandates as $financial_type => $mandates) { + $rcontrib_ids = []; + foreach ($mandates as $mandate) { + array_push($rcontrib_ids, $mandate['mandate_entity_id']); + } + $rcontrib_id_strings = implode(',', $rcontrib_ids); - $sql_query = " + $sql_query = " SELECT contribution.contribution_recur_id AS contribution_recur_id, contribution.id AS contribution_id @@ -170,26 +195,29 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim AND DATE(contribution.receive_date) = DATE('{$collection_date}') AND (txg.type IS NULL OR txg.type IN ('RCUR', 'FRST')) AND contribution.payment_instrument_id IN ({$payment_instrument_id_list});"; - $results = CRM_Core_DAO::executeQuery($sql_query); - while ($results->fetch()) { - $existing_contributions_by_recur_id[$results->contribution_recur_id] = $results->contribution_id; + $results = CRM_Core_DAO::executeQuery($sql_query); + while ($results->fetch()) { + $existing_contributions_by_recur_id[$results->contribution_recur_id] = $results->contribution_id; + } } } // RCUR-STEP 4: create the missing contributions, store all in $mandate['mandate_entity_id'] - foreach ($mandates_by_nextdate as $collection_date => $mandates) { - foreach ($mandates as $index => $mandate) { - $recur_id = $mandate['mandate_entity_id']; - if (isset($existing_contributions_by_recur_id[$recur_id])) { - // if the contribution already exists, store it - $contribution_id = $existing_contributions_by_recur_id[$recur_id]; - unset($existing_contributions_by_recur_id[$recur_id]); - $mandates_by_nextdate[$collection_date][$index]['mandate_entity_id'] = $contribution_id; - } else { - // else: create it - $installment_pi = CRM_Sepa_Logic_PaymentInstruments::getInstallmentPaymentInstrument( - $creditor_id, $mandate['rc_payment_instrument_id'], ($mode == 'FRST')); - $contribution_data = array( + foreach ($mandates_by_nextdate as $collection_date => $financial_type_mandates) { + foreach ($financial_type_mandates as $financial_type => $mandates) { + foreach ($mandates as $index => $mandate) { + $recur_id = $mandate['mandate_entity_id']; + if (isset($existing_contributions_by_recur_id[$recur_id])) { + // if the contribution already exists, store it + $contribution_id = $existing_contributions_by_recur_id[$recur_id]; + unset($existing_contributions_by_recur_id[$recur_id]); + $mandates_by_nextdate[$collection_date][$financial_type][$index]['mandate_entity_id'] = $contribution_id; + } + else { + // else: create it + $installment_pi = CRM_Sepa_Logic_PaymentInstruments::getInstallmentPaymentInstrument( + $creditor_id, $mandate['rc_payment_instrument_id'], ($mode == 'FRST')); + $contribution_data = array( "version" => 3, "total_amount" => $mandate['rc_amount'], "currency" => $mandate['rc_currency'], @@ -203,25 +231,27 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim "is_test" => $mandate['rc_is_test'], "payment_instrument_id" => $installment_pi ); - $contribution = civicrm_api('Contribution', 'create', $contribution_data); - if (empty($contribution['is_error'])) { - // Success! Call the post_create hook - CRM_Utils_SepaCustomisationHooks::installment_created($mandate['mandate_id'], $recur_id, $contribution['id']); - - // 'mandate_entity_id' will now be overwritten with the contribution instance ID - // to allow compatibility in with OOFF groups in the syncGroups function - $mandates_by_nextdate[$collection_date][$index]['mandate_entity_id'] = $contribution['id']; - } else { - // in case of an error, we will unset 'mandate_entity_id', so it cannot be - // interpreted as the contribution instance ID (see above) - unset($mandates_by_nextdate[$collection_date][$index]['mandate_entity_id']); - - // log the error - Civi::log()->debug("org.project60.sepa: batching:updateRCUR/createContrib ".$contribution['error_message']); - - // TODO: Error handling? + $contribution = civicrm_api('Contribution', 'create', $contribution_data); + if (empty($contribution['is_error'])) { + // Success! Call the post_create hook + CRM_Utils_SepaCustomisationHooks::installment_created($mandate['mandate_id'], $recur_id, $contribution['id']); + + // 'mandate_entity_id' will now be overwritten with the contribution instance ID + // to allow compatibility in with OOFF groups in the syncGroups function + $mandates_by_nextdate[$collection_date][$financial_type][$index]['mandate_entity_id'] = $contribution['id']; + } + else { + // in case of an error, we will unset 'mandate_entity_id', so it cannot be + // interpreted as the contribution instance ID (see above) + unset($mandates_by_nextdate[$collection_date][$financial_type][$index]['mandate_entity_id']); + + // log the error + Civi::log()->debug("org.project60.sepa: batching:updateRCUR/createContrib ".$contribution['error_message']); + + // TODO: Error handling? + } + unset($existing_contributions_by_recur_id[$recur_id]); } - unset($existing_contributions_by_recur_id[$recur_id]); } } } @@ -242,14 +272,23 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim AND txgroup.sdd_creditor_id = $creditor_id AND txgroup.status_id = $group_status_id_open;"; $results = CRM_Core_DAO::executeQuery($sql_query); - $existing_groups = array(); + $existing_groups = []; while ($results->fetch()) { $collection_date = date('Y-m-d', strtotime($results->collection_date)); $existing_groups[$collection_date] = $results->txgroup_id; } // step 6: sync calculated group structure with existing (open) groups - self::syncGroups($mandates_by_nextdate, $existing_groups, $mode, 'RCUR', $rcur_notice, $creditor_id, $offset!==NULL, $offset===0); + self::syncGroups( + $mandates_by_nextdate, + $existing_groups, + $mode, + 'RCUR', + $rcur_notice, + $creditor_id, + NULL !== $offset, + 0 === $offset + ); $lock->release(); } @@ -298,7 +337,7 @@ static function updateOOFF($creditor_id, $now = 'now', $offset = NULL, $limit = ->getArrayCopy(); // step 2: group mandates in collection dates - $calculated_groups = array(); + $calculated_groups = []; $earliest_collection_date = date('Y-m-d', strtotime("$now +$ooff_notice days")); $latest_collection_date = ''; From e80ed0818f144d4de1c25664385a93ad1a6b8fb1 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Wed, 18 Sep 2024 16:35:00 +0200 Subject: [PATCH 12/18] Use API4 SepaMandate.get for edit mandate page --- CRM/Sepa/Page/EditMandate.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/CRM/Sepa/Page/EditMandate.php b/CRM/Sepa/Page/EditMandate.php index bd6547e6..973d7577 100644 --- a/CRM/Sepa/Page/EditMandate.php +++ b/CRM/Sepa/Page/EditMandate.php @@ -24,8 +24,6 @@ * */ -require_once 'CRM/Core/Page.php'; - use CRM_Sepa_ExtensionUtil as E; class CRM_Sepa_Page_EditMandate extends CRM_Core_Page { @@ -65,10 +63,17 @@ function run() { } // first, load the mandate - $mandate = civicrm_api("SepaMandate", "getsingle", array('id'=>$mandate_id, 'version'=>3)); - if (isset($mandate['is_error']) && $mandate['is_error']) { - CRM_Core_Session::setStatus(sprintf(ts("Cannot read mandate [%s]. Error was: '%s'", array('domain' => 'org.project60.sepa')), $mandate_id, $mandate['error_message']), ts('Error', array('domain' => 'org.project60.sepa')), 'error'); - die(sprintf(ts("Cannot find mandate [%s].", array('domain' => 'org.project60.sepa')), $mandate_id)); + try { + // API4 SepaMandate.get checks Financial ACLs for corresponding (recurring) contribution. + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('id', '=', $mandate_id) + ->execute() + ->single(); + } + catch (Exception $exception) { + CRM_Core_Error::statusBounce( + E::ts("Cannot read mandate [%1]. Error was: '%2'", [1 => $mandate_id, 2 => $exception->getMessage()]) + ); } // load the contribution From c5b95c40ddb347b70eb27ecafe1fc935f3fffbbc Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Wed, 18 Sep 2024 16:35:13 +0200 Subject: [PATCH 13/18] Use API4 SepaMandate.get for create mandate form --- CRM/Sepa/Form/CreateMandate.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/CRM/Sepa/Form/CreateMandate.php b/CRM/Sepa/Form/CreateMandate.php index dbe80035..eca16ca9 100644 --- a/CRM/Sepa/Form/CreateMandate.php +++ b/CRM/Sepa/Form/CreateMandate.php @@ -614,14 +614,15 @@ protected function getKnownBankAccounts() { $known_accounts = array('' => E::ts("new account")); // get data from SepaMandates - $mandates = civicrm_api3('SepaMandate', 'get', array( - 'contact_id' => $this->contact_id, - 'status' => array('IN' => array('RCUR', 'COMPLETE', 'SENT')), - 'option.limit' => 0, - 'return' => 'iban,bic,reference', - 'option.sort' => 'id desc' - )); - foreach ($mandates['values'] as $mandate) { + // API4 SepaMandate.get checks Financial ACLs for corresponding (recurring) contribution. + $mandates = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('iban', 'bic', 'reference') + ->addWhere('contact_id', '=', $this->contact_id) + ->addWhere('status', 'IN', ['RCUR', 'COMPLETE', 'SENT']) + ->addOrderBy('id', 'DESC') + ->execute() + ->indexBy('id'); + foreach ($mandates as $mandate) { $key = "{$mandate['iban']}/{$mandate['bic']}"; if (!isset($known_accounts[$key])) { $known_accounts[$key] = "{$mandate['iban']} ({$mandate['reference']})"; From bef328d02775d406e64c4a9aa2703d745ce634e2 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Wed, 18 Sep 2024 16:45:46 +0200 Subject: [PATCH 14/18] Use API4 SepaMandate.get for Action Provider "FindMandate" action --- .../ActionProvider/Action/FindMandate.php | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Civi/Sepa/ActionProvider/Action/FindMandate.php b/Civi/Sepa/ActionProvider/Action/FindMandate.php index 72f7b0e8..aad7ef2a 100644 --- a/Civi/Sepa/ActionProvider/Action/FindMandate.php +++ b/Civi/Sepa/ActionProvider/Action/FindMandate.php @@ -20,6 +20,7 @@ use \Civi\ActionProvider\Parameter\Specification; use \Civi\ActionProvider\Parameter\SpecificationBag; +use Civi\Api4\SepaMandate; use CRM_Sepa_ExtensionUtil as E; class FindMandate extends CreateRecurringMandate { @@ -106,37 +107,37 @@ public function getOutputSpecification() { protected function doAction(ParameterBagInterface $parameters, ParameterBagInterface $output) { // compile search query - $mandate_search = []; + $mandatesQuery = SepaMandate::get(TRUE); if (!empty($this->configuration->getParameter('creditor_id'))) { - $mandate_search['creditor_id'] = ['IN' => $this->configuration->getParameter('creditor_id')]; + $mandatesQuery->addWhere('creditor_id', 'IN', $this->configuration->getParameter('creditor_id')); } if (!empty($this->configuration->getParameter('type'))) { - $mandate_search['type'] = $this->configuration->getParameter('type'); + $mandatesQuery->addWhere('type', '=', $this->configuration->getParameter('type')); } if (!empty($this->configuration->getParameter('active'))) { - $mandate_search['status'] = ['IN' => ['FRST', 'RCUR', 'OOFF', 'INIT']]; + $mandatesQuery->addWhere('status', 'IN', ['FRST', 'RCUR', 'OOFF', 'INIT']); } if (!empty($parameters->getParameter('contact_id'))) { - $mandate_search['contact_id'] = $parameters->getParameter('contact_id'); + $mandatesQuery->addWhere('contact_id', '=', $parameters->getParameter('contact_id')); } if (!empty($parameters->getParameter('account_holder'))) { - $mandate_search['account_holder'] = $parameters->getParameter('account_holder'); + $mandatesQuery->addWhere('account_holder', '=', $parameters->getParameter('account_holder')); } if (!empty($parameters->getParameter('iban'))) { - $mandate_search['iban'] = $parameters->getParameter('iban'); + $mandatesQuery->addWhere('iban', '=', $parameters->getParameter('iban')); } if (!empty($parameters->getParameter('reference'))) { - $mandate_search['reference'] = $parameters->getParameter('reference'); + $mandatesQuery->addWhere('reference', '=', $parameters->getParameter('reference')); } // add order - $mandate_search['option.sort'] = $this->configuration->getParameter('pick'); - $mandate_search['option.limit'] = 1; + $mandatesQuery->addOrderBy($this->configuration->getParameter('pick')); + // TODO: Shouldn't this use single()? + $mandatesQuery->setLimit(1); // search mandate - $result = \civicrm_api3('SepaMandate', 'get', $mandate_search); - if ($result['count']) { - $mandate = reset($result['values']); + $mandate = $mandatesQuery->execute()->first(); + if (isset($mandate)) { $output->setParameter('id', $mandate['id']); $output->setParameter('reference', $mandate['reference']); $output->setParameter('type', $mandate['type']); @@ -191,4 +192,4 @@ protected function doAction(ParameterBagInterface $parameters, ParameterBagInter } } } -} \ No newline at end of file +} From 5bfdfbdf77f515e2e32359c586c89512e91d7fa2 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Wed, 25 Sep 2024 14:56:42 +0200 Subject: [PATCH 15/18] Fix error in updating RCUR transaction groups --- CRM/Sepa/Logic/Batching.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CRM/Sepa/Logic/Batching.php b/CRM/Sepa/Logic/Batching.php index fbc2de58..94b3ed53 100644 --- a/CRM/Sepa/Logic/Batching.php +++ b/CRM/Sepa/Logic/Batching.php @@ -266,6 +266,7 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim $sql_query = " SELECT txgroup.collection_date AS collection_date, + txgroup.financial_type_id AS financial_type_id, txgroup.id AS txgroup_id FROM civicrm_sdd_txgroup AS txgroup WHERE txgroup.type = '$mode' @@ -275,7 +276,7 @@ static function updateRCUR($creditor_id, $mode, $now = 'now', $offset=NULL, $lim $existing_groups = []; while ($results->fetch()) { $collection_date = date('Y-m-d', strtotime($results->collection_date)); - $existing_groups[$collection_date] = $results->txgroup_id; + $existing_groups[$collection_date][$results->financial_type_id ?? 0] = $results->txgroup_id; } // step 6: sync calculated group structure with existing (open) groups From 1ac233f1a390cc540d5007fc30b7c2341c2cf434 Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Tue, 1 Oct 2024 13:33:06 +0200 Subject: [PATCH 16/18] Replace various ocurrences of APIv3 calls to CiviSEPA entities with APIv4 calls for checking Financial ACLs permissions --- CRM/Sepa/BAO/SEPAMandate.php | 37 ++- CRM/Sepa/BAO/SEPATransactionGroup.php | 39 ++- CRM/Sepa/Form/CreateMandate.php | 13 +- CRM/Sepa/Form/RetryCollection.php | 10 +- CRM/Sepa/Logic/Batching.php | 36 ++- CRM/Sepa/Logic/Group.php | 22 +- CRM/Sepa/Logic/PaymentInstruments.php | 6 +- CRM/Sepa/Logic/Queue/Close.php | 15 +- CRM/Sepa/Page/CloseGroup.php | 223 +++++++++++------- CRM/Sepa/Page/CreateMandate.php | 30 ++- CRM/Sepa/Page/DeleteGroup.php | 152 +++++++----- CRM/Sepa/Page/MarkGroupReceived.php | 5 +- .../Action/CreateOneOffMandate.php | 7 +- .../Action/CreateRecurringMandate.php | 7 +- .../Action/TerminateMandate.php | 10 +- api/v3/SepaMandate.php | 24 +- api/v3/SepaTransactionGroup.php | 11 +- org.project60.sepacustom/sepacustom.php | 9 +- sepa.php | 52 ++-- 19 files changed, 463 insertions(+), 245 deletions(-) diff --git a/CRM/Sepa/BAO/SEPAMandate.php b/CRM/Sepa/BAO/SEPAMandate.php index fdbfc575..0a51d776 100644 --- a/CRM/Sepa/BAO/SEPAMandate.php +++ b/CRM/Sepa/BAO/SEPAMandate.php @@ -21,6 +21,8 @@ * */ +use CRM_Sepa_ExtensionUtil as E; + /** * Class contains functions for Sepa mandates @@ -48,9 +50,14 @@ static function add(&$params) { // new mandate, use the default creditor $default_creditor = CRM_Sepa_Logic_Settings::defaultCreditor(); $params['creditor_id'] = $default_creditor->id; - } else { + } + else { // existing mandate, get creditor - $params['creditor_id'] = civicrm_api3('SepaMandate', 'getvalue', array ('id' => $params['id'], 'return' => 'creditor_id')); + $params['creditor_id'] = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('creditor_id') + ->addWhere('id', '=', $params['id']) + ->execute() + ->single()['creditor_id']; } } $creditor = civicrm_api3 ('SepaCreditor', 'getsingle', array ('id' => $params['creditor_id'], 'return' => 'mandate_prefix,creditor_type')); @@ -371,16 +378,22 @@ static function terminateMandate($mandate_id, $new_end_date_str, $cancel_reason= } // first, load the mandate - $mandate = civicrm_api("SepaMandate", "getsingle", array('id'=>$mandate_id, 'version'=>3)); - if (isset($mandate['is_error'])) { + try { + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('id', '=', $mandate_id) + ->execute() + ->single(); + } + catch (Exception $exception) { $lock->release(); - $error_message = sprintf(ts("Cannot read mandate [%s]. Error was: '%s'", array('domain' => 'org.project60.sepa')), $mandate_id, $mandate['error_message']); + $error_message = E::ts("Cannot read mandate [%1]. Error was: '%2'", [1 => $mandate_id, 2 => $exception->getMessage()]); if ($error_to_ui) { CRM_Core_Session::setStatus($error_message, ts('Error'), 'error'); return FALSE; - } else { - throw new Exception($error_message); + } + else { + throw new CRM_Core_Exception($error_message); } } @@ -546,7 +559,10 @@ static function modifyMandate($mandate_id, $changes) { } // load the mandate - $mandate = civicrm_api3('SepaMandate', 'getsingle', array('id' => $mandate_id)); + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('id', '=', $mandate_id) + ->execute() + ->single(); if ($mandate['type'] != 'RCUR') { $lock->release(); throw new Exception(ts("You can only modify RCUR mandates.", array('domain' => 'org.project60.sepa'))); @@ -906,7 +922,10 @@ public static function getLastMandateOfContact($cid) { ]); if ($result->fetch()) { // return the mandate - return civicrm_api3('SepaMandate', 'getsingle', array('id' => $result->mandate_id)); + return \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('id', '=', $result->mandate_id) + ->execute() + ->single(); } else { return FALSE; diff --git a/CRM/Sepa/BAO/SEPATransactionGroup.php b/CRM/Sepa/BAO/SEPATransactionGroup.php index 740c59da..cde44ece 100644 --- a/CRM/Sepa/BAO/SEPATransactionGroup.php +++ b/CRM/Sepa/BAO/SEPATransactionGroup.php @@ -184,8 +184,13 @@ function generateXML ($id = null) { * @return int id of the sepa file entity created, or an error message string */ static function createFile($txgroup_id, $override = false) { - $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('id'=>$txgroup_id, 'version'=>3)); - if (isset($txgroup['is_error']) && $txgroup['is_error']) { + try { + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $txgroup_id) + ->execute() + ->single(); + } + catch (CRM_Core_Exception $exception) { return "Cannot find transaction group ".$txgroup_id; } @@ -252,7 +257,10 @@ static function createFile($txgroup_id, $override = false) { * @return an update array with the txgroup or a string with an error message */ static function adjustCollectionDate($txgroup_id, $latest_submission_date) { - $txgroup = civicrm_api3('SepaTransactionGroup', 'getsingle', array('id' => $txgroup_id)); + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $txgroup_id) + ->execute() + ->single(); if ($txgroup['type'] == 'RTRY') { $txgroup['type'] = 'RCUR'; } @@ -277,12 +285,16 @@ static function adjustCollectionDate($txgroup_id, $latest_submission_date) { } // reload the item - $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('version'=>3, 'id'=>$txgroup_id)); - if (!empty($txgroup['is_error'])) { - return $txgroup['error_message']; - } else { - return $txgroup; + try { + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $txgroup_id) + ->execute() + ->single(); } + catch (Exception $exception) { + return $exception->getMessage(); + } + return $txgroup; } @@ -302,9 +314,14 @@ static function adjustCollectionDate($txgroup_id, $latest_submission_date) { */ static function deleteGroup($txgroup_id, $delete_contributions_mode = 'no') { // load the group - $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('id' => $txgroup_id, 'version' => 3)); - if (!empty($txgroup['is_error'])) { - return "Transaction group [$txgroup_id] could not be loaded. Error was: ".$txgroup['error_message']; + try { + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $txgroup_id) + ->execute() + ->single(); + } + catch (Exception $exception) { + return "Transaction group [$txgroup_id] could not be loaded. Error was: " . $exception->getMessage(); } // first, delete the contents of this group diff --git a/CRM/Sepa/Form/CreateMandate.php b/CRM/Sepa/Form/CreateMandate.php index eca16ca9..208598c3 100644 --- a/CRM/Sepa/Form/CreateMandate.php +++ b/CRM/Sepa/Form/CreateMandate.php @@ -469,9 +469,11 @@ public function postProcess() { try { $mandate = civicrm_api3('SepaMandate', 'createfull', $mandate_data); - $mandate = civicrm_api3('SepaMandate', 'getsingle', array( - 'id' => $mandate['id'], - 'return' => 'reference,id,type')); + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('reference', 'id', 'type') + ->addWhere('id', '=', $mandate['id']) + ->execute() + ->single(); // if we get here, everything went o.k. CRM_Core_Session::setStatus(E::ts("'%3' SEPA Mandate %1 created.", array( @@ -483,7 +485,10 @@ public function postProcess() { // terminate old mandate, of requested if (!empty($values['replace'])) { - $rpl_mandate = civicrm_api3('SepaMandate', 'getsingle', array('id' => $values['replace'])); + $rpl_mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('id', '=', $values['replace']) + ->execute() + ->single(); CRM_Sepa_BAO_SEPAMandate::terminateMandate( $values['replace'], diff --git a/CRM/Sepa/Form/RetryCollection.php b/CRM/Sepa/Form/RetryCollection.php index 6f9798ad..023e8348 100644 --- a/CRM/Sepa/Form/RetryCollection.php +++ b/CRM/Sepa/Form/RetryCollection.php @@ -222,11 +222,11 @@ protected function getCreditorList() { */ protected function getGroupList() { $txgroup_list = array(); - $txgroup_query = civicrm_api3('SepaTransactionGroup', 'get', array( - 'option.limit' => 0, - 'type' => array('IN' => array('RCUR', 'FRST')), - 'return' => 'reference,id')); - foreach ($txgroup_query['values'] as $txgroup) { + $txgroup_query = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addSelect('reference', 'id') + ->addWhere('type', 'IN', ['RCUR', 'FRST']) + ->execute(); + foreach ($txgroup_query as $txgroup) { $txgroup_list[$txgroup['id']] = $txgroup['reference']; } return $txgroup_list; diff --git a/CRM/Sepa/Logic/Batching.php b/CRM/Sepa/Logic/Batching.php index 94b3ed53..90b208ea 100644 --- a/CRM/Sepa/Logic/Batching.php +++ b/CRM/Sepa/Logic/Batching.php @@ -541,10 +541,16 @@ protected static function syncGroups( } } else { - $group = civicrm_api('SepaTransactionGroup', 'getsingle', array('version' => 3, 'id' => $existing_groups[$collection_date][$financial_type_id ?? 0], 'status_id' => $group_status_id_open)); - if (!empty($group['is_error'])) { + try { + $group = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $existing_groups[$collection_date][$financial_type_id ?? 0]) + ->addWhere('status_id', '=', $group_status_id_open) + ->execute() + ->single(); + } + catch (Exception $exception) { // TODO: Error handling - Civi::log()->debug("org.project60.sepa: batching:syncGroups/getGroup ".$group['error_message']); + Civi::log()->debug('org.project60.sepa: batching:syncGroups/getGroup ' . $exception->getMessage()); } unset($existing_groups[$collection_date][$financial_type_id ?? 0]); } @@ -615,9 +621,17 @@ protected static function syncGroups( * Check if a transaction group reference is already in use */ public static function referenceExists($reference) { - $query = civicrm_api('SepaTransactionGroup', 'getsingle', array('reference'=>$reference, 'version'=>3)); - // this should return an error, if the group exists - return !(isset($query['is_error']) && $query['is_error']); + try { + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('reference', '=', $reference) + ->execute() + ->single(); + $exists = TRUE; + } + catch (Exception $exception) { + $exists = FALSE; + } + return $exists; } public static function getTransactionGroupReference( @@ -718,10 +732,12 @@ public static function getCycleDay($rcontribution, $creditor_id) { if (!empty($rcontribution['mandate_first_executed'])) { $date = $rcontribution['mandate_first_executed']; } else { - $mandate = civicrm_api3('SepaMandate', 'getsingle', array( - 'entity_table' => 'civicrm_contribution_recur', - 'entity_id' => $rcontribution['id'], - 'return' => 'status')); + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('status') + ->addWhere('entity_table', '=', 'civicrm_contribution_recur') + ->addWhere('entity_id', '=', $rcontribution['id']) + ->execute() + ->single(); if ($mandate['status'] == 'RCUR') { // support for RCUR without first contribution diff --git a/CRM/Sepa/Logic/Group.php b/CRM/Sepa/Logic/Group.php index 27e3a14e..b7313be0 100644 --- a/CRM/Sepa/Logic/Group.php +++ b/CRM/Sepa/Logic/Group.php @@ -44,10 +44,15 @@ static function close($txgroup_id) { } $status_closed = (int) CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); $group_status_id_open = (int) CRM_Core_PseudoConstant::getKey('CRM_Batch_BAO_Batch', 'status_id', 'Open'); - $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('id'=>$txgroup_id, 'version'=>3)); - if (isset($txgroup['is_error']) && $txgroup['is_error']) { + try { + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $txgroup_id) + ->execute() + ->single(); + } + catch (Exception $exception) { $lock->release(); - return "Cannot find transaction group ".$txgroup_id; + return 'Cannot find transaction group ' . $txgroup_id; } $collection_date = $txgroup['collection_date']; @@ -169,10 +174,15 @@ static function received($txgroup_id) { return civicrm_api3_create_error("Status 'Pending', 'Completed' or 'In Progress' does not exist!"); // step 0: load the group object - $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('id'=>$txgroup_id, 'version'=>3)); - if (!empty($txgroup['is_error'])) { + try { + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $txgroup_id) + ->execute() + ->single(); + } + catch (Exception $exception) { $lock->release(); - return "Cannot find transaction group ".$txgroup_id; + return 'Cannot find transaction group ' . $txgroup_id; } // check status diff --git a/CRM/Sepa/Logic/PaymentInstruments.php b/CRM/Sepa/Logic/PaymentInstruments.php index 290f352f..9e8a1f2c 100644 --- a/CRM/Sepa/Logic/PaymentInstruments.php +++ b/CRM/Sepa/Logic/PaymentInstruments.php @@ -76,7 +76,11 @@ public static function getSDDPaymentInstrumentsForContribution($contribution_id) return null; } else { // get the creditor ID - $mandate = civicrm_api3('SepaMandate', 'getsingle', ['return' => 'creditor_id,type,status', 'id' => $mandate_id]); + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('creditor_id', 'type', 'status') + ->addWhere('id', '=', $mandate_id) + ->execute() + ->single(); return self::getPaymentInstrumentsForCreditor($mandate['creditor_id'], $mandate['type']); } } diff --git a/CRM/Sepa/Logic/Queue/Close.php b/CRM/Sepa/Logic/Queue/Close.php index 1bec8246..2b8fac60 100644 --- a/CRM/Sepa/Logic/Queue/Close.php +++ b/CRM/Sepa/Logic/Queue/Close.php @@ -48,14 +48,17 @@ public static function launchCloseRunner($txgroup_ids, $target_group_status, $ta $is_received_runner = $target_contribution_status == (int) CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); // fetch the groups - $txgroup_query = civicrm_api3('SepaTransactionGroup', 'get', array( - 'id' => ['IN' => $txgroup_ids], - 'option.limit' => 0 - )); + $txgroups = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', 'IN', $txgroup_ids) + ->execute(); - $group_status_id_busy = (int) CRM_Core_PseudoConstant::getKey('CRM_Batch_BAO_Batch', 'status_id', 'Data Entry'); + $group_status_id_busy = (int) CRM_Core_PseudoConstant::getKey( + 'CRM_Batch_BAO_Batch', + 'status_id', + 'Data Entry' + ); - foreach ($txgroup_query['values'] as $txgroup) { + foreach ($txgroups as $txgroup) { // first: set group status to busy $queue->createItem(new CRM_Sepa_Logic_Queue_Close('set_group_status', $txgroup, $group_status_id_busy)); diff --git a/CRM/Sepa/Page/CloseGroup.php b/CRM/Sepa/Page/CloseGroup.php index 9122ec0b..9e9c7dc0 100644 --- a/CRM/Sepa/Page/CloseGroup.php +++ b/CRM/Sepa/Page/CloseGroup.php @@ -21,93 +21,145 @@ * */ -require_once 'CRM/Core/Page.php'; +use CRM_Sepa_ExtensionUtil as E; class CRM_Sepa_Page_CloseGroup extends CRM_Core_Page { function run() { - CRM_Utils_System::setTitle(ts('Close SEPA Group', array('domain' => 'org.project60.sepa'))); - if (isset($_REQUEST['group_id'])) { - if (isset($_REQUEST['status']) && ($_REQUEST['status'] == "missed" || $_REQUEST['status'] == "invalid" || $_REQUEST['status'] == "closed")) { - $this->assign('status', $_REQUEST['status']); - }else{ - $_REQUEST['status'] = ""; + CRM_Utils_System::setTitle(E::ts('Close SEPA Group')); + $group_id = (int) CRM_Utils_Request::retrieve('group_id', 'Integer'); + $status = CRM_Utils_Request::retrieve('status', 'String'); + if (isset($group_id)) { + if (isset($status) && ('missed' === $status || 'invalid' === $status || 'closed' === $status)) { + $this->assign('status', $status); + } + else { + $status = ''; + } + + $group_id = (int) CRM_Utils_Request::retrieve('group_id', 'Integer'); + $this->assign('txgid', $group_id); + + // LOAD/CREATE THE TXFILE + try { + $group = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $group_id) + ->execute() + ->single(); + + $this->assign('txgroup', $group); + + // check whether this is a group created by a test creditor + $creditor = civicrm_api('SepaCreditor', 'getsingle', [ + 'version' => 3, + 'id' => $group['sdd_creditor_id'], + ]); + if (isset($creditor['is_error']) && $creditor['is_error']) { + CRM_Core_Session::setStatus( + E::ts('Cannot load creditor.') . '
' + . E::ts('Error was: %1', [1 => $creditor['error_message']]), + E::ts('Error'), + 'error' + ); } - - $group_id = (int) $_REQUEST['group_id']; - $this->assign('txgid', $group_id); - - // LOAD/CREATE THE TXFILE - $group = civicrm_api('SepaTransactionGroup', 'getsingle', array('version'=>3, 'id'=>$group_id)); - if (isset($group['is_error']) && $group['is_error']) { - CRM_Core_Session::setStatus("Cannot load group #$group_id.
Error was: ".$group['error_message'], ts('Error', array('domain' => 'org.project60.sepa')), 'error'); - } else { - $this->assign('txgroup', $group); - - // check whether this is a group created by a test creditor - $creditor = civicrm_api('SepaCreditor', 'getsingle', array('version'=>3, 'id'=>$group['sdd_creditor_id'])); - if (isset($creditor['is_error']) && $creditor['is_error']) { - CRM_Core_Session::setStatus("Cannot load creditor.
Error was: ".$creditor['error_message'], ts('Error', array('domain' => 'org.project60.sepa')), 'error'); - }else{ - // check for test group - $isTestGroup = isset($creditor['category']) && ($creditor['category'] == "TEST"); - $this->assign('is_test_group', $isTestGroup); - - // check if this is allowed - $no_draftxml = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_no_draft_xml'); - $this->assign('allow_xml', !$no_draftxml); - - if ($_REQUEST['status'] == "") { - // first adjust group's collection date if requested - if (!empty($_REQUEST['adjust'])) { - $result = CRM_Sepa_BAO_SEPATransactionGroup::adjustCollectionDate($group_id, $_REQUEST['adjust']); - if (is_string($result)) { - // that's an error -> stop here! - die($result); - } else { - // that went well, so result should be the update group data - $group = $result; - } - } - - - // delete old txfile - if (!empty($group['sdd_file_id'])) { - $result = civicrm_api('SepaSddFile', 'delete', array('id'=>$group['sdd_file_id'], 'version'=>3)); - if (isset($result['is_error']) && $result['is_error']) { - CRM_Core_Session::setStatus("Cannot delete file #".$group['sdd_file_id'].".
Error was: ".$result['error_message'], ts('Error', array('domain' => 'org.project60.sepa')), 'error'); - } - } - - $this->createDownloadLink($group_id); + else { + // check for test group + $isTestGroup = isset($creditor['category']) && ('TEST' === $creditor['category']); + $this->assign('is_test_group', $isTestGroup); + + // check if this is allowed + $no_draftxml = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_no_draft_xml'); + $this->assign('allow_xml', !$no_draftxml); + + if ('' === $status) { + // first adjust group's collection date if requested + $adjust = CRM_Utils_Request::retrieve('adjust', 'String'); + if (!empty($adjust)) { + $result = CRM_Sepa_BAO_SEPATransactionGroup::adjustCollectionDate($group_id, $adjust); + if (is_string($result)) { + // that's an error -> stop here! + // TODO: Do not use die(). + die($result); } - - if ($_REQUEST['status'] == "closed" && !$isTestGroup) { - // CLOSE THE GROUP: - $async_batch = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_async_batching'); - if ($async_batch) { - // call the closing runner - $skip_closed = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_skip_closed'); - if ($skip_closed) { - $target_contribution_status = (int) CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); - $target_group_status = (int) CRM_Core_PseudoConstant::getKey('CRM_Batch_BAO_Batch', 'status_id', 'Received'); - } else { - $target_contribution_status = CRM_Sepa_Logic_Settings::contributionInProgressStatusId(); - $target_group_status = (int) CRM_Core_PseudoConstant::getKey('CRM_Batch_BAO_Batch', 'status_id', 'Closed'); - } - // this call doesn't return (redirect to runner) - CRM_Sepa_Logic_Queue_Close::launchCloseRunner(array($group_id), $target_group_status, $target_contribution_status); - } - - $result = civicrm_api('SepaAlternativeBatching', 'close', array('version'=>3, 'txgroup_id'=>$group_id)); - if ($result['is_error']) { - CRM_Core_Session::setStatus("Cannot close group #$group_id.
Error was: ".$result['error_message'], ts('Error', array('domain' => 'org.project60.sepa')), 'error'); - } - $this->createDownloadLink($group_id); + else { + // that went well, so result should be the update group data + $group = $result; + } + } + + // delete old txfile + if (!empty($group['sdd_file_id'])) { + $result = civicrm_api('SepaSddFile', 'delete', [ + 'id' => $group['sdd_file_id'], + 'version' => 3, + ]); + if (isset($result['is_error']) && $result['is_error']) { + CRM_Core_Session::setStatus( + E::ts('Cannot delete file #%1', [1 => $group['sdd_file_id']]) . '
' + . E::ts('Error was: %1', [1 => $result['error_message']]), + E::ts('Error'), + 'error' + ); } + } + + $this->createDownloadLink($group_id); } + if ('closed' === $status && !$isTestGroup) { + // CLOSE THE GROUP: + $async_batch = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_async_batching'); + if ($async_batch) { + // call the closing runner + $skip_closed = CRM_Sepa_Logic_Settings::getGenericSetting('sdd_skip_closed'); + if ($skip_closed) { + $target_contribution_status = (int) CRM_Core_PseudoConstant::getKey( + 'CRM_Contribute_BAO_Contribution', + 'contribution_status_id', + 'Completed' + ); + $target_group_status = (int) CRM_Core_PseudoConstant::getKey( + 'CRM_Batch_BAO_Batch', + 'status_id', + 'Received' + ); + } + else { + $target_contribution_status = CRM_Sepa_Logic_Settings::contributionInProgressStatusId(); + $target_group_status = (int) CRM_Core_PseudoConstant::getKey( + 'CRM_Batch_BAO_Batch', + 'status_id', + 'Closed' + ); + } + // this call doesn't return (redirect to runner) + CRM_Sepa_Logic_Queue_Close::launchCloseRunner([$group_id], $target_group_status, $target_contribution_status); + } + + $result = civicrm_api('SepaAlternativeBatching', 'close', [ + 'version' => 3, + 'txgroup_id' => $group_id, + ]); + if ($result['is_error']) { + CRM_Core_Session::setStatus( + E::ts('Cannot close group #%1', [1 => $group_id]) . '
' + . E::ts('Error was: %1', [1 => $result['error_message']]), + E::ts('Error'), + 'error' + ); + } + $this->createDownloadLink($group_id); + } } + } + catch (Exception $exception) { + CRM_Core_Session::setStatus( + E::ts('Cannot load group #%1', [1 => $group_id]) . '
' + . E::ts('Error was: %1', [1 => $group['error_message']]), + E::ts('Error'), + 'error' + ); + } } parent::run(); @@ -117,13 +169,24 @@ function run() { * generate an XML download link and assign to the template */ protected function createDownloadLink($group_id) { - $xmlfile = civicrm_api('SepaAlternativeBatching', 'createxml', array('txgroup_id'=>$group_id, 'override'=>True, 'version'=>3)); + $xmlfile = civicrm_api( + 'SepaAlternativeBatching', + 'createxml', + ['txgroup_id' => $group_id, 'override' => TRUE, 'version' => 3] + ); if (isset($xmlfile['is_error']) && $xmlfile['is_error']) { - CRM_Core_Session::setStatus("Cannot load for group #".$group_id.".
Error was: ".$xmlfile['error_message'], ts('Error', array('domain' => 'org.project60.sepa')), 'error'); - }else{ + CRM_Core_Session::setStatus( + E::ts('Cannot load for group #%1', [1 => $group_id]) . '
' + . E::ts('Error was: %1', [1 => $xmlfile['error_message']]), + E::ts('Error'), + 'error' + ); + } + else { $file_id = $xmlfile['id']; $this->assign('file_link', CRM_Utils_System::url('civicrm/sepa/xml', "id=$file_id")); $this->assign('file_name', $xmlfile['filename']); } } + } diff --git a/CRM/Sepa/Page/CreateMandate.php b/CRM/Sepa/Page/CreateMandate.php index 71ea6ad9..ffb33187 100644 --- a/CRM/Sepa/Page/CreateMandate.php +++ b/CRM/Sepa/Page/CreateMandate.php @@ -28,6 +28,8 @@ require_once 'CRM/Core/Page.php'; require_once 'packages/php-iban-1.4.0/php-iban.php'; +use CRM_Sepa_ExtensionUtil as E; + class CRM_Sepa_Page_CreateMandate extends CRM_Core_Page { function run() { @@ -327,9 +329,18 @@ function prepareCreateForm($contact_id) { * Will prepare the form by cloning the data from the given mandate */ function prepareClonedData($mandate_id) { - $mandate = civicrm_api('SepaMandate', 'getsingle', array('id'=>$mandate_id, 'version'=>3)); - if (isset($mandate['is_error']) && $mandate['is_error']) { - CRM_Core_Session::setStatus(sprintf(ts("Couldn't load mandate #%s", array('domain' => 'org.project60.sepa')), $mandate_id), ts('Error', array('domain' => 'org.project60.sepa')), 'error'); + try { + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('id', '=', $mandate) + ->execute() + ->single(); + } + catch (Exception $exception) { + CRM_Core_Session::setStatus( + E::ts("Couldn't load mandate #%1", [1 => $mandate_id]), + E::ts('Error'), + 'error' + ); return; } @@ -440,15 +451,20 @@ function validateParameters() { } // check reference - if (!empty($_REQUEST['reference'])) { + $reference = CRM_Utils_Request::retrieve('reference', 'String'); + if (!empty($reference)) { // check if it is formally correct - if (!preg_match("/^[A-Z0-9\\-]{4,35}$/", $_REQUEST['reference'])) { + if (!preg_match("/^[A-Z0-9\\-]{4,35}$/", $reference)) { $errors['reference'] = ts("Reference has to be an upper case alphanumeric string between 4 and 35 characters long.", array('domain' => 'org.project60.sepa')); } else { // check if the reference is taken - $count = civicrm_api3('SepaMandate', 'getcount', array("reference" => $_REQUEST['reference'])); + $count = \Civi\Api4\SepaMandate::get(TRUE) + ->selectRowCount() + ->addWhere('reference', '=', $reference) + ->execute() + ->countMatched(); if ($count > 0) { - $errors['reference'] = ts("This reference is already in use.", array('domain' => 'org.project60.sepa')); + $errors['reference'] = E::ts('This reference is already in use.'); } } } diff --git a/CRM/Sepa/Page/DeleteGroup.php b/CRM/Sepa/Page/DeleteGroup.php index ef16f7eb..6d6d8be5 100644 --- a/CRM/Sepa/Page/DeleteGroup.php +++ b/CRM/Sepa/Page/DeleteGroup.php @@ -26,76 +26,98 @@ class CRM_Sepa_Page_DeleteGroup extends CRM_Core_Page { function run() { - CRM_Utils_System::setTitle(ts('Delete SEPA Group', array('domain' => 'org.project60.sepa'))); + CRM_Utils_System::setTitle(ts('Delete SEPA Group', ['domain' => 'org.project60.sepa'])); if (empty($_REQUEST['group_id'])) { - $this->assign('status', 'error'); - - } else { - $group_id = (int) $_REQUEST['group_id']; - $this->assign('txgid', $group_id); - $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('id'=>$group_id, 'version'=>3)); - if (empty($txgroup['is_error'])) { - $txgroup['status_label'] = CRM_Core_PseudoConstant::getLabel('CRM_Batch_BAO_Batch', 'status_id', $txgroup['status_id']); - $txgroup['status_name'] = CRM_Core_PseudoConstant::getName('CRM_Batch_BAO_Batch', 'status_id', $txgroup['status_id']); - $this->assign('txgroup', $txgroup); - } else { - $_REQUEST['confirmed'] = 'error'; // skip the parts below - } - - if (empty($_REQUEST['confirmed'])) { - // gather information to display - $PENDING = (int) CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'); - $INPROGRESS = CRM_Sepa_Logic_Settings::contributionInProgressStatusId(); - - $stats = array('busy' => 0, 'open' => 0, 'other' => 0, 'total' => 0); - $status2contributions = $this->contributionStats($group_id); - foreach ($status2contributions as $contribution_status_id => $contributions) { - foreach ($contributions as $contribution_id) { - $stats['total'] += 1; - if ($contribution_status_id==$PENDING) { - $stats['open'] += 1; - } elseif ($contribution_status_id==$INPROGRESS) { - $stats['busy'] += 1; - } else { - $stats['other'] += 1; - } - } - } - $this->assign('stats', $stats); - $this->assign('status', 'unconfirmed'); - $this->assign('submit_url', CRM_Utils_System::url('civicrm/sepa/deletegroup')); + $this->assign('status', 'error'); + } + else { + $group_id = (int) $_REQUEST['group_id']; + $this->assign('txgid', $group_id); + try { + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $group_id) + ->execute() + ->single(); - } elseif ($_REQUEST['confirmed']=='yes') { - // delete the group - $this->assign('status', 'done'); - $delete_contributions_mode = $_REQUEST['delete_contents']; - $deleted_ok = array(); - $deleted_error = array(); - $result = CRM_Sepa_BAO_SEPATransactionGroup::deleteGroup($group_id, $delete_contributions_mode); - if (is_string($result)) { - // a very basic error happened - $this->assign('error', $result); - } else { - // do some stats on the result - $deleted_total = count($result); - foreach ($result as $contribution_id => $message) { - if ($message=='ok') { - array_push($deleted_ok, $contribution_id); - } else { - array_push($deleted_error, $contribution_id); - } - } - $this->assign('deleted_result', $result); - $this->assign('deleted_ok', $deleted_ok); - $this->assign('deleted_error', $deleted_error); - } + $txgroup['status_label'] = CRM_Core_PseudoConstant::getLabel( + 'CRM_Batch_BAO_Batch', + 'status_id', + $txgroup['status_id'] + ); + $txgroup['status_name'] = CRM_Core_PseudoConstant::getName( + 'CRM_Batch_BAO_Batch', + 'status_id', + $txgroup['status_id'] + ); + $this->assign('txgroup', $txgroup); + } + catch (Exception $exception) { + // skip the parts below + $_REQUEST['confirmed'] = 'error'; + } - } elseif ($_REQUEST['confirmed']=='error') { - $this->assign('status', 'error'); + if (empty($_REQUEST['confirmed'])) { + // gather information to display + $PENDING = (int) CRM_Core_PseudoConstant::getKey( + 'CRM_Contribute_BAO_Contribution', + 'contribution_status_id', + 'Pending' + ); + $INPROGRESS = CRM_Sepa_Logic_Settings::contributionInProgressStatusId(); - } else { - CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/sepa')); + $stats = ['busy' => 0, 'open' => 0, 'other' => 0, 'total' => 0]; + $status2contributions = $this->contributionStats($group_id); + foreach ($status2contributions as $contribution_status_id => $contributions) { + foreach ($contributions as $contribution_id) { + $stats['total'] += 1; + if ($contribution_status_id == $PENDING) { + $stats['open'] += 1; + } + elseif ($contribution_status_id == $INPROGRESS) { + $stats['busy'] += 1; + } + else { + $stats['other'] += 1; + } + } + } + $this->assign('stats', $stats); + $this->assign('status', 'unconfirmed'); + $this->assign('submit_url', CRM_Utils_System::url('civicrm/sepa/deletegroup')); + } + elseif ($_REQUEST['confirmed'] == 'yes') { + // delete the group + $this->assign('status', 'done'); + $delete_contributions_mode = $_REQUEST['delete_contents']; + $deleted_ok = []; + $deleted_error = []; + $result = CRM_Sepa_BAO_SEPATransactionGroup::deleteGroup($group_id, $delete_contributions_mode); + if (is_string($result)) { + // a very basic error happened + $this->assign('error', $result); + } + else { + // do some stats on the result + $deleted_total = count($result); + foreach ($result as $contribution_id => $message) { + if ($message == 'ok') { + array_push($deleted_ok, $contribution_id); + } + else { + array_push($deleted_error, $contribution_id); + } + } + $this->assign('deleted_result', $result); + $this->assign('deleted_ok', $deleted_ok); + $this->assign('deleted_error', $deleted_error); } + } + elseif ($_REQUEST['confirmed'] == 'error') { + $this->assign('status', 'error'); + } + else { + CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/sepa')); + } } parent::run(); diff --git a/CRM/Sepa/Page/MarkGroupReceived.php b/CRM/Sepa/Page/MarkGroupReceived.php index b41e231b..bb6ab36f 100644 --- a/CRM/Sepa/Page/MarkGroupReceived.php +++ b/CRM/Sepa/Page/MarkGroupReceived.php @@ -38,7 +38,10 @@ function run() { } // get the group - $group = civicrm_api3('SepaTransactionGroup', 'getsingle', ['id' => $group_id]); + $group = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $group_id) + ->execute() + ->single(); $this->assign('txgroup', $group); // check whether this is a group created by a test creditor diff --git a/Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php b/Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php index fbd214f4..325a9bd6 100644 --- a/Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php +++ b/Civi/Sepa/ActionProvider/Action/CreateOneOffMandate.php @@ -22,6 +22,7 @@ use \Civi\ActionProvider\Parameter\Specification; use \Civi\ActionProvider\Parameter\SpecificationBag; +use Civi\Api4\SepaMandate; use CRM_Sepa_ExtensionUtil as E; class CreateOneOffMandate extends AbstractAction { @@ -126,7 +127,11 @@ protected function doAction(ParameterBagInterface $parameters, ParameterBagInter // create mandate try { $mandate = \civicrm_api3('SepaMandate', 'createfull', $mandate_data); - $mandate = \civicrm_api3('SepaMandate', 'getsingle', ['id' => $mandate['id'], 'return' => 'id,reference']); + $mandate = SepaMandate::get(TRUE) + ->addSelect('id', 'reference') + ->addWhere('id', '=', $mandate['id']) + ->execute() + ->single(); $output->setParameter('mandate_id', $mandate['id']); $output->setParameter('mandate_reference', $mandate['reference']); $output->setParameter('error', ''); diff --git a/Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php b/Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php index 0b1eedbe..dd8415d2 100644 --- a/Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php +++ b/Civi/Sepa/ActionProvider/Action/CreateRecurringMandate.php @@ -21,6 +21,7 @@ use \Civi\ActionProvider\Parameter\Specification; use \Civi\ActionProvider\Parameter\SpecificationBag; +use Civi\Api4\SepaMandate; use CRM_Sepa_ExtensionUtil as E; class CreateRecurringMandate extends CreateOneOffMandate { @@ -150,7 +151,11 @@ protected function doAction(ParameterBagInterface $parameters, ParameterBagInter // create mandate try { $mandate = \civicrm_api3('SepaMandate', 'createfull', $mandate_data); - $mandate = \civicrm_api3('SepaMandate', 'getsingle', ['id' => $mandate['id'], 'return' => 'id,reference,entity_id']); + $mandate = SepaMandate::get(TRUE) + ->addSelect('id', 'reference', 'entity_id') + ->addWhere('id', '=', $mandate['id']) + ->execute() + ->single(); $output->setParameter('mandate_id', $mandate['id']); $output->setParameter('recurring_contribution_id', $mandate['entity_id']); $output->setParameter('mandate_reference', $mandate['reference']); diff --git a/Civi/Sepa/ActionProvider/Action/TerminateMandate.php b/Civi/Sepa/ActionProvider/Action/TerminateMandate.php index fab45494..547a3e60 100644 --- a/Civi/Sepa/ActionProvider/Action/TerminateMandate.php +++ b/Civi/Sepa/ActionProvider/Action/TerminateMandate.php @@ -22,6 +22,7 @@ use \Civi\ActionProvider\Parameter\SpecificationBag; use Civi\FormProcessor\API\Exception; +use Civi\Sepa\DataProcessor\Source\SepaMandate; use CRM_Sepa_ExtensionUtil as E; class TerminateMandate extends AbstractAction { @@ -79,10 +80,11 @@ protected function doAction(ParameterBagInterface $parameters, ParameterBagInter if ($mandateReference) { // find mandate ID with reference try { - $mandateId = civicrm_api3('SepaMandate', 'getvalue', [ - 'return' => "id", - 'reference' => $mandateReference, - ]); + $mandateId = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('id') + ->addWhere('reference', '=', $mandateReference) + ->execute() + ->single()['id']; if ($mandateId) { $output->setParameter('mandate_id', $mandateId); // terminate mandate with cancel reason from parameter if provided else condiguration cancel reason diff --git a/api/v3/SepaMandate.php b/api/v3/SepaMandate.php index 84f17cb5..77d83720 100644 --- a/api/v3/SepaMandate.php +++ b/api/v3/SepaMandate.php @@ -441,10 +441,14 @@ function civicrm_api3_sepa_mandate_modify($params) { // look up mandate ID if only reference is given if (empty($params['mandate_id']) && !empty($params['reference'])) { - $mandate = civicrm_api3('SepaMandate', 'get', array('reference' => $params['reference'], 'return' => 'id')); - if ($mandate['id']) { - $params['mandate_id'] = $mandate['id']; - } else { + try { + $params['mandate_id'] = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('id') + ->addWhere('reference', '=', $params['reference']) + ->execute() + ->single()['id']; + } + catch (Exception $exception) { return civicrm_api3_create_error("Couldn't identify mandate with reference '{$params['reference']}'."); } } @@ -519,10 +523,14 @@ function _civicrm_api3_sepa_mandate_modify_spec(&$params) { function civicrm_api3_sepa_mandate_terminate($params) { // look up mandate ID if only reference is given if (empty($params['mandate_id']) && !empty($params['reference'])) { - $mandate = civicrm_api3('SepaMandate', 'get', array('reference' => $params['reference'], 'return' => 'id')); - if ($mandate['id']) { - $params['mandate_id'] = $mandate['id']; - } else { + try { + $params['mandate_id'] = \Civi\Api4\SepaMandate::get(TRUE) + ->addSelect('id') + ->addWhere('reference', '=', $params['reference']) + ->execute() + ->single()['id']; + } + catch (Exception $exception) { return civicrm_api3_create_error("Couldn't identify mandate with reference '{$params['reference']}'."); } } diff --git a/api/v3/SepaTransactionGroup.php b/api/v3/SepaTransactionGroup.php index 4914b864..da9ac2e7 100644 --- a/api/v3/SepaTransactionGroup.php +++ b/api/v3/SepaTransactionGroup.php @@ -242,9 +242,14 @@ function civicrm_api3_sepa_transaction_group_createnext ($params) { function civicrm_api3_sepa_transaction_group_toaccgroup($params) { // first, load the txgroup $txgroup_id = $params['txgroup_id']; - $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('id' => $txgroup_id, 'version' => 3)); - if (isset($txgroup['is_error']) && $txgroup['is_error']) { - return civicrm_api3_create_error("Cannot read transaction group ".$txgroup_id); + try { + $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addWhere('id', '=', $txgroup_id) + ->execute() + ->single(); + } + catch (Exception $exception) { + return civicrm_api3_create_error('Cannot read transaction group ' . $txgroup_id); } if (isset($txgroup['sdd_file_id'])) { diff --git a/org.project60.sepacustom/sepacustom.php b/org.project60.sepacustom/sepacustom.php index 3cf31f10..f2d9cd65 100644 --- a/org.project60.sepacustom/sepacustom.php +++ b/org.project60.sepacustom/sepacustom.php @@ -115,8 +115,13 @@ function sepacustom_civicrm_create_mandate(&$mandate_parameters) { for ($n=0; $n < 10; $n++) { $reference_candidate = sprintf($reference, $n); // check if it exists - $mandate = civicrm_api('SepaMandate', 'getsingle', array('version' => 3, 'reference' => $reference_candidate)); - if (isset($mandate['is_error']) && $mandate['is_error']) { + try { + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('reference', '=', $reference_candidate) + ->execute() + ->single(); + } + catch (Exception $exception) { // does not exist! take it! $mandate_parameters['reference'] = $reference_candidate; return; diff --git a/sepa.php b/sepa.php index 643dc7b6..fd5f1fce 100644 --- a/sepa.php +++ b/sepa.php @@ -46,7 +46,8 @@ function sepa_civicrm_pageRun( &$page ) { ]); } - } elseif (get_class($page) == "CRM_Contribute_Page_Tab") { + } + elseif (get_class($page) == "CRM_Contribute_Page_Tab") { // single contribuion view if (CRM_Core_Permission::check('view sepa mandates')) { $contribution_id = $page->getTemplate()->get_template_vars('id'); @@ -59,34 +60,40 @@ function sepa_civicrm_pageRun( &$page ) { $contribution_recur_id = $page->getTemplate()->get_template_vars('contribution_recur_id'); if (empty($contribution_recur_id)) return; - $mandate = civicrm_api3('SepaMandate', 'getsingle', [ - 'entity_table' => 'civicrm_contribution_recur', - 'entity_id' => $contribution_recur_id - ]); + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('entity_table', '=', 'civicrm_contribution_recur') + ->addWhere('entity_id', '=', $contribution_recur_id) + ->execute() + ->single(); } else { // this is a OOFF contribtion - $mandate = civicrm_api3('SepaMandate', 'getsingle', [ - 'entity_table' => 'civicrm_contribution', - 'entity_id' => $contribution_id - ]); + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('entity_table', '=', 'civicrm_contribution') + ->addWhere('entity_id', '=', $contribution_id) + ->execute() + ->single(); } // add txgroup information - $txgroup_search = civicrm_api3('SepaContributionGroup', 'get', [ - 'contribution_id' => $contribution_id - ]); - if (empty($txgroup_search['id'])) { + $txgroup_search = \Civi\Api4\SepaContributionGroup::get(TRUE) + ->addWhere('contribution_id', '=', $contribution_id) + ->execute() + ->indexBy('id'); + if ($txgroup_search->count() === 0) { $mandate['tx_group'] = ts('None', ['domain' => 'org.project60.sepa']); - } else { - $group = reset($txgroup_search['values']); + } + else { + $group = $txgroup_search->first(); if (empty($group['txgroup_id'])) { $mandate['tx_group'] = ts('Error', ['domain' => 'org.project60.sepa']); - } else { - $mandate['tx_group'] = civicrm_api3('SepaTransactionGroup', 'getvalue', [ - 'return' => 'reference', - 'id' => $group['txgroup_id'] - ]); + } + else { + $mandate['tx_group'] = \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->addSelect('reference') + ->addWhere('id', '=', $group['txgroup_id']) + ->execute() + ->single()['reference']; } } @@ -112,7 +119,10 @@ function sepa_civicrm_pageRun( &$page ) { // this is not a SEPA recurring contribution return; } - $mandate = civicrm_api3("SepaMandate","getsingle", ['id' => $mandate_id]); + $mandate = \Civi\Api4\SepaMandate::get(TRUE) + ->addWhere('id', '=', $mandate_id) + ->execute() + ->single(); // load notes $mandate['notes'] = []; From 053073ccb7e6c1d67114ff5d7555cebd8ceabaac Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Wed, 9 Oct 2024 13:49:31 +0200 Subject: [PATCH 17/18] Catch CRM_Core_Exception only when expected to be thrown by API4 single() --- CRM/Sepa/BAO/SEPAMandate.php | 3 +-- CRM/Sepa/BAO/SEPATransactionGroup.php | 4 ++-- CRM/Sepa/Logic/Batching.php | 4 ++-- CRM/Sepa/Logic/Group.php | 4 ++-- CRM/Sepa/Page/CloseGroup.php | 2 +- CRM/Sepa/Page/CreateMandate.php | 2 +- CRM/Sepa/Page/DeleteGroup.php | 2 +- CRM/Sepa/Page/EditMandate.php | 2 +- api/v3/SepaMandate.php | 4 ++-- api/v3/SepaTransactionGroup.php | 2 +- org.project60.sepacustom/sepacustom.php | 2 +- 11 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CRM/Sepa/BAO/SEPAMandate.php b/CRM/Sepa/BAO/SEPAMandate.php index 0a51d776..4e00413d 100644 --- a/CRM/Sepa/BAO/SEPAMandate.php +++ b/CRM/Sepa/BAO/SEPAMandate.php @@ -23,7 +23,6 @@ use CRM_Sepa_ExtensionUtil as E; - /** * Class contains functions for Sepa mandates */ @@ -384,7 +383,7 @@ static function terminateMandate($mandate_id, $new_end_date_str, $cancel_reason= ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { $lock->release(); $error_message = E::ts("Cannot read mandate [%1]. Error was: '%2'", [1 => $mandate_id, 2 => $exception->getMessage()]); diff --git a/CRM/Sepa/BAO/SEPATransactionGroup.php b/CRM/Sepa/BAO/SEPATransactionGroup.php index cde44ece..9f632be5 100644 --- a/CRM/Sepa/BAO/SEPATransactionGroup.php +++ b/CRM/Sepa/BAO/SEPATransactionGroup.php @@ -291,7 +291,7 @@ static function adjustCollectionDate($txgroup_id, $latest_submission_date) { ->execute() ->single(); } - catch (Exception $exception) { + catch (CRM_Core_Exception $exception) { return $exception->getMessage(); } return $txgroup; @@ -320,7 +320,7 @@ static function deleteGroup($txgroup_id, $delete_contributions_mode = 'no') { ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { return "Transaction group [$txgroup_id] could not be loaded. Error was: " . $exception->getMessage(); } diff --git a/CRM/Sepa/Logic/Batching.php b/CRM/Sepa/Logic/Batching.php index 90b208ea..e6a2a19b 100644 --- a/CRM/Sepa/Logic/Batching.php +++ b/CRM/Sepa/Logic/Batching.php @@ -548,7 +548,7 @@ protected static function syncGroups( ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { // TODO: Error handling Civi::log()->debug('org.project60.sepa: batching:syncGroups/getGroup ' . $exception->getMessage()); } @@ -628,7 +628,7 @@ public static function referenceExists($reference) { ->single(); $exists = TRUE; } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { $exists = FALSE; } return $exists; diff --git a/CRM/Sepa/Logic/Group.php b/CRM/Sepa/Logic/Group.php index b7313be0..c1bc59e4 100644 --- a/CRM/Sepa/Logic/Group.php +++ b/CRM/Sepa/Logic/Group.php @@ -50,7 +50,7 @@ static function close($txgroup_id) { ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { $lock->release(); return 'Cannot find transaction group ' . $txgroup_id; } @@ -180,7 +180,7 @@ static function received($txgroup_id) { ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { $lock->release(); return 'Cannot find transaction group ' . $txgroup_id; } diff --git a/CRM/Sepa/Page/CloseGroup.php b/CRM/Sepa/Page/CloseGroup.php index 9e9c7dc0..20609dae 100644 --- a/CRM/Sepa/Page/CloseGroup.php +++ b/CRM/Sepa/Page/CloseGroup.php @@ -152,7 +152,7 @@ function run() { } } } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { CRM_Core_Session::setStatus( E::ts('Cannot load group #%1', [1 => $group_id]) . '
' . E::ts('Error was: %1', [1 => $group['error_message']]), diff --git a/CRM/Sepa/Page/CreateMandate.php b/CRM/Sepa/Page/CreateMandate.php index ffb33187..4194c5dd 100644 --- a/CRM/Sepa/Page/CreateMandate.php +++ b/CRM/Sepa/Page/CreateMandate.php @@ -335,7 +335,7 @@ function prepareClonedData($mandate_id) { ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { CRM_Core_Session::setStatus( E::ts("Couldn't load mandate #%1", [1 => $mandate_id]), E::ts('Error'), diff --git a/CRM/Sepa/Page/DeleteGroup.php b/CRM/Sepa/Page/DeleteGroup.php index 6d6d8be5..58f55250 100644 --- a/CRM/Sepa/Page/DeleteGroup.php +++ b/CRM/Sepa/Page/DeleteGroup.php @@ -51,7 +51,7 @@ function run() { ); $this->assign('txgroup', $txgroup); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { // skip the parts below $_REQUEST['confirmed'] = 'error'; } diff --git a/CRM/Sepa/Page/EditMandate.php b/CRM/Sepa/Page/EditMandate.php index 973d7577..e6df1acf 100644 --- a/CRM/Sepa/Page/EditMandate.php +++ b/CRM/Sepa/Page/EditMandate.php @@ -70,7 +70,7 @@ function run() { ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { CRM_Core_Error::statusBounce( E::ts("Cannot read mandate [%1]. Error was: '%2'", [1 => $mandate_id, 2 => $exception->getMessage()]) ); diff --git a/api/v3/SepaMandate.php b/api/v3/SepaMandate.php index 77d83720..e1028d90 100644 --- a/api/v3/SepaMandate.php +++ b/api/v3/SepaMandate.php @@ -448,7 +448,7 @@ function civicrm_api3_sepa_mandate_modify($params) { ->execute() ->single()['id']; } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { return civicrm_api3_create_error("Couldn't identify mandate with reference '{$params['reference']}'."); } } @@ -530,7 +530,7 @@ function civicrm_api3_sepa_mandate_terminate($params) { ->execute() ->single()['id']; } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { return civicrm_api3_create_error("Couldn't identify mandate with reference '{$params['reference']}'."); } } diff --git a/api/v3/SepaTransactionGroup.php b/api/v3/SepaTransactionGroup.php index da9ac2e7..439d7f45 100644 --- a/api/v3/SepaTransactionGroup.php +++ b/api/v3/SepaTransactionGroup.php @@ -248,7 +248,7 @@ function civicrm_api3_sepa_transaction_group_toaccgroup($params) { ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { return civicrm_api3_create_error('Cannot read transaction group ' . $txgroup_id); } diff --git a/org.project60.sepacustom/sepacustom.php b/org.project60.sepacustom/sepacustom.php index f2d9cd65..789d8e52 100644 --- a/org.project60.sepacustom/sepacustom.php +++ b/org.project60.sepacustom/sepacustom.php @@ -121,7 +121,7 @@ function sepacustom_civicrm_create_mandate(&$mandate_parameters) { ->execute() ->single(); } - catch (Exception $exception) { + catch (\CRM_Core_Exception $exception) { // does not exist! take it! $mandate_parameters['reference'] = $reference_candidate; return; From ac242726e3f31d19e7c1f76031004d3456b43d0b Mon Sep 17 00:00:00 2001 From: Jens Schuppe Date: Thu, 10 Oct 2024 15:36:49 +0200 Subject: [PATCH 18/18] Code style issues --- CRM/Sepa/Form/CreateMandate.php | 5 +- CRM/Sepa/Form/RetryCollection.php | 11 +-- CRM/Sepa/Logic/Batching.php | 145 +++++++++++++++++------------- CRM/Sepa/Page/CloseGroup.php | 8 +- CRM/Sepa/Page/MandateTab.php | 11 +-- 5 files changed, 98 insertions(+), 82 deletions(-) diff --git a/CRM/Sepa/Form/CreateMandate.php b/CRM/Sepa/Form/CreateMandate.php index 208598c3..41194ac8 100644 --- a/CRM/Sepa/Form/CreateMandate.php +++ b/CRM/Sepa/Form/CreateMandate.php @@ -56,7 +56,7 @@ public function buildQuickForm() { // load mandate try { // API4 SepaMandate.get checks Financial ACLs for corresponding (recurring) contribution. - $this->old_mandate = \Civi\Api4\SepaMandate::get() + $this->old_mandate = \Civi\Api4\SepaMandate::get(TRUE) ->addWhere('id', '=', $mandate_id) ->execute() ->single(); @@ -625,8 +625,7 @@ protected function getKnownBankAccounts() { ->addWhere('contact_id', '=', $this->contact_id) ->addWhere('status', 'IN', ['RCUR', 'COMPLETE', 'SENT']) ->addOrderBy('id', 'DESC') - ->execute() - ->indexBy('id'); + ->execute(); foreach ($mandates as $mandate) { $key = "{$mandate['iban']}/{$mandate['bic']}"; if (!isset($known_accounts[$key])) { diff --git a/CRM/Sepa/Form/RetryCollection.php b/CRM/Sepa/Form/RetryCollection.php index 023e8348..08895f55 100644 --- a/CRM/Sepa/Form/RetryCollection.php +++ b/CRM/Sepa/Form/RetryCollection.php @@ -221,15 +221,12 @@ protected function getCreditorList() { * Get the list of creditors */ protected function getGroupList() { - $txgroup_list = array(); - $txgroup_query = \Civi\Api4\SepaTransactionGroup::get(TRUE) + return \Civi\Api4\SepaTransactionGroup::get(TRUE) ->addSelect('reference', 'id') ->addWhere('type', 'IN', ['RCUR', 'FRST']) - ->execute(); - foreach ($txgroup_query as $txgroup) { - $txgroup_list[$txgroup['id']] = $txgroup['reference']; - } - return $txgroup_list; + ->execute() + ->indexBy('id') + ->column('reference'); } /* diff --git a/CRM/Sepa/Logic/Batching.php b/CRM/Sepa/Logic/Batching.php index e6a2a19b..c6df95b5 100644 --- a/CRM/Sepa/Logic/Batching.php +++ b/CRM/Sepa/Logic/Batching.php @@ -547,6 +547,86 @@ protected static function syncGroups( ->addWhere('status_id', '=', $group_status_id_open) ->execute() ->single(); + + // now we have the right group. Prepare some parameters... + $group_id = $group['id']; + $entity_ids = []; + foreach ($mandates as $mandate) { + // remark: "mandate_entity_id" in this case means the contribution ID + if (empty($mandate['mandate_entity_id'])) { + // this shouldn't happen + Civi::log()->debug("org.project60.sepa: batching:syncGroups mandate with bad mandate_entity_id ignored:" . $mandate['mandate_id']); + } + else { + array_push($entity_ids, $mandate['mandate_entity_id']); + } + } + if (count($entity_ids)<=0) continue; + + // now, filter out the entity_ids that are are already in a non-open group + // (DO NOT CHANGE CLOSED GROUPS!) + $entity_ids_list = implode(',', $entity_ids); + $already_sent_contributions = CRM_Core_DAO::executeQuery( + << $group_status_id_open; + SQL + ); + while ($already_sent_contributions->fetch()) { + $index = array_search($already_sent_contributions->contribution_id, $entity_ids); + if ($index !== false) unset($entity_ids[$index]); + } + if (count($entity_ids)<=0) continue; + + // remove all the unwanted entries from our group + $entity_ids_list = implode(',', $entity_ids); + if (!$partial_groups || $partial_first) { + CRM_Core_DAO::executeQuery( + <<fetch()) { + // remove from entity ids, if in there: + if(($key = array_search($existing->contribution_id, $entity_ids)) !== false) { + unset($entity_ids[$key]); + } + } + + // the remaining must be added + foreach ($entity_ids as $entity_id) { + CRM_Core_DAO::executeQuery( + <<debug("org.project60.sepa: batching:syncGroups mandate with bad mandate_entity_id ignored:" . $mandate['mandate_id']); - } - else { - array_push($entity_ids, $mandate['mandate_entity_id']); - } - } - if (count($entity_ids)<=0) continue; - - // now, filter out the entity_ids that are are already in a non-open group - // (DO NOT CHANGE CLOSED GROUPS!) - $entity_ids_list = implode(',', $entity_ids); - $already_sent_contributions = CRM_Core_DAO::executeQuery(" - SELECT contribution_id - FROM civicrm_sdd_contribution_txgroup - LEFT JOIN civicrm_sdd_txgroup ON civicrm_sdd_contribution_txgroup.txgroup_id = civicrm_sdd_txgroup.id - WHERE contribution_id IN ($entity_ids_list) - AND civicrm_sdd_txgroup.status_id <> $group_status_id_open;"); - while ($already_sent_contributions->fetch()) { - $index = array_search($already_sent_contributions->contribution_id, $entity_ids); - if ($index !== false) unset($entity_ids[$index]); - } - if (count($entity_ids)<=0) continue; - - // remove all the unwanted entries from our group - $entity_ids_list = implode(',', $entity_ids); - if (!$partial_groups || $partial_first) { - CRM_Core_DAO::executeQuery("DELETE FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id=$group_id AND contribution_id NOT IN ($entity_ids_list);"); - } - - // remove all our entries from other groups, if necessary - CRM_Core_DAO::executeQuery("DELETE FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id!=$group_id AND contribution_id IN ($entity_ids_list);"); - - // now check which ones are already in our group... - $existing = CRM_Core_DAO::executeQuery("SELECT * FROM civicrm_sdd_contribution_txgroup WHERE txgroup_id=$group_id AND contribution_id IN ($entity_ids_list);"); - while ($existing->fetch()) { - // remove from entity ids, if in there: - if(($key = array_search($existing->contribution_id, $entity_ids)) !== false) { - unset($entity_ids[$key]); - } - } - - // the remaining must be added - foreach ($entity_ids as $entity_id) { - CRM_Core_DAO::executeQuery("INSERT INTO civicrm_sdd_contribution_txgroup (txgroup_id, contribution_id) VALUES ($group_id, $entity_id);"); - } } } @@ -621,17 +648,11 @@ protected static function syncGroups( * Check if a transaction group reference is already in use */ public static function referenceExists($reference) { - try { - $txgroup = \Civi\Api4\SepaTransactionGroup::get(TRUE) + return \Civi\Api4\SepaTransactionGroup::get(TRUE) + ->selectRowCount() ->addWhere('reference', '=', $reference) ->execute() - ->single(); - $exists = TRUE; - } - catch (\CRM_Core_Exception $exception) { - $exists = FALSE; - } - return $exists; + ->count() === 1; } public static function getTransactionGroupReference( diff --git a/CRM/Sepa/Page/CloseGroup.php b/CRM/Sepa/Page/CloseGroup.php index 20609dae..28d3abe7 100644 --- a/CRM/Sepa/Page/CloseGroup.php +++ b/CRM/Sepa/Page/CloseGroup.php @@ -27,17 +27,15 @@ class CRM_Sepa_Page_CloseGroup extends CRM_Core_Page { function run() { CRM_Utils_System::setTitle(E::ts('Close SEPA Group')); - $group_id = (int) CRM_Utils_Request::retrieve('group_id', 'Integer'); + $group_id = CRM_Utils_Request::retrieve('group_id', 'Integer'); $status = CRM_Utils_Request::retrieve('status', 'String'); - if (isset($group_id)) { - if (isset($status) && ('missed' === $status || 'invalid' === $status || 'closed' === $status)) { + if (is_int($group_id)) { + if ('missed' === $status || 'invalid' === $status || 'closed' === $status) { $this->assign('status', $status); } else { $status = ''; } - - $group_id = (int) CRM_Utils_Request::retrieve('group_id', 'Integer'); $this->assign('txgid', $group_id); // LOAD/CREATE THE TXFILE diff --git a/CRM/Sepa/Page/MandateTab.php b/CRM/Sepa/Page/MandateTab.php index 1abc72fb..25d1c6e8 100644 --- a/CRM/Sepa/Page/MandateTab.php +++ b/CRM/Sepa/Page/MandateTab.php @@ -46,7 +46,7 @@ public function run() { // Retrieve OOFF mandates. $ooffList = []; - $ooffMandates = SepaMandate::get() + $ooffMandates = SepaMandate::get(TRUE) ->addSelect( 'id', 'contribution.id', @@ -100,7 +100,7 @@ public function run() { // Retrieve RCUR mandates. $rcurList = []; - $rcurMandates = SepaMandate::get() + $rcurMandates = SepaMandate::get(TRUE) ->addSelect( 'id', 'contribution_recur.id', @@ -152,7 +152,8 @@ public function run() { ->addWhere('contribution_status_id:name', '!=', 'Pending') ->addOrderBy('receive_date', 'DESC') ->setLimit(1) - ->execute(); + ->execute() + ->first(); $rcurRow = [ 'mandate_id' => $rcurMandate['id'], 'start_date' => $rcurMandate['contribution_recur.start_date'], @@ -168,9 +169,9 @@ public function run() { TRUE ), 'next_collection_date' => $rcurMandate['contribution_recur.next_sched_contribution_date'], - 'last_collection_date' => $lastInstallment->first()['receive_date'] ?? NULL, + 'last_collection_date' => $lastInstallment['receive_date'] ?? NULL, 'cancel_reason' => $rcur_mandates['cancel_reason'], - 'last_cancel_reason' => $lastInstallment->first()['cancel_reason'] ?? NULL, + 'last_cancel_reason' => $lastInstallment['cancel_reason'] ?? NULL, 'end_date' => $rcurMandate['contribution_recur.end_date'], 'currency' => $rcurMandate['contribution_recur.currency'], 'amount' => $rcurMandate['contribution_recur.amount'],