Skip to content

Commit 05d9420

Browse files
committed
FOSFASPRT-78: Add native overpayment support
Add ability to allocate overpayments from contributions to credit notes. Features: - New CreditNote.allocateOverpayment API action - Company setting for overpayment financial type - Admin setting to enable/disable overpayments feature - "Allocate overpayment" link on overpaid contributions - Creates credit note with overpayment amount as line item - Records negative payment on contribution to balance it Database changes: - Added overpayment_financial_type_id column to Company table - New upgrade_1006.sql migration
1 parent e8acb3d commit 05d9420

File tree

20 files changed

+1070
-4
lines changed

20 files changed

+1070
-4
lines changed

CRM/Financeextras/DAO/Company.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ class CRM_Financeextras_DAO_Company extends CRM_Core_DAO {
102102
*/
103103
public $receivable_payment_method;
104104

105+
/**
106+
* Financial type to use for overpayment credit notes
107+
*
108+
* @var int|string|null
109+
* (SQL type: int unsigned)
110+
* Note that values will be retrieved from the database as a string.
111+
*/
112+
public $overpayment_financial_type_id;
113+
105114
/**
106115
* Class constructor.
107116
*/
@@ -132,6 +141,7 @@ public static function getReferenceColumns() {
132141
Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'contact_id', 'civicrm_contact', 'id');
133142
Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'invoice_template_id', 'civicrm_msg_template', 'id');
134143
Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'creditnote_template_id', 'civicrm_msg_template', 'id');
144+
Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'overpayment_financial_type_id', 'civicrm_financial_type', 'id');
135145
CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']);
136146
}
137147
return Civi::$statics[__CLASS__]['links'];
@@ -293,6 +303,27 @@ public static function &fields() {
293303
],
294304
'add' => NULL,
295305
],
306+
'overpayment_financial_type_id' => [
307+
'name' => 'overpayment_financial_type_id',
308+
'type' => CRM_Utils_Type::T_INT,
309+
'title' => E::ts('Financial Type for Overpayments'),
310+
'description' => E::ts('Financial type to use for overpayment credit notes'),
311+
'where' => 'financeextras_company.overpayment_financial_type_id',
312+
'table_name' => 'financeextras_company',
313+
'entity' => 'Company',
314+
'bao' => 'CRM_Financeextras_DAO_Company',
315+
'localizable' => 0,
316+
'FKClassName' => 'CRM_Financial_DAO_FinancialType',
317+
'html' => [
318+
'label' => E::ts("Financial Type for Overpayments"),
319+
],
320+
'pseudoconstant' => [
321+
'table' => 'civicrm_financial_type',
322+
'keyColumn' => 'id',
323+
'labelColumn' => 'name',
324+
],
325+
'add' => NULL,
326+
],
296327
];
297328
CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
298329
}

CRM/Financeextras/Form/Company/Add.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ public function buildQuickForm() {
8282
TRUE
8383
);
8484

85+
$this->addEntityRef(
86+
'overpayment_financial_type_id',
87+
ts('Financial Type for Overpayments'),
88+
[
89+
'entity' => 'FinancialType',
90+
'api' => ['params' => ['is_active' => 1]],
91+
'select' => ['minimumInputLength' => 0],
92+
'placeholder' => ts('Select Financial Type'),
93+
'multiple' => FALSE,
94+
],
95+
FALSE
96+
);
97+
8598
$this->addButtons([
8699
[
87100
'type' => 'submit',
@@ -111,6 +124,7 @@ public function setDefaultValues() {
111124
$values['creditnote_prefix'] = $company->creditnote_prefix;
112125
$values['next_creditnote_number'] = $company->next_creditnote_number;
113126
$values['receivable_payment_method'] = $company->receivable_payment_method;
127+
$values['overpayment_financial_type_id'] = $company->overpayment_financial_type_id;
114128

115129
return $values;
116130
}
@@ -159,6 +173,7 @@ public function postProcess() {
159173
$params['creditnote_prefix'] = $submittedValues['creditnote_prefix'];
160174
$params['next_creditnote_number'] = $submittedValues['next_creditnote_number'];
161175
$params['receivable_payment_method'] = $submittedValues['receivable_payment_method'];
176+
$params['overpayment_financial_type_id'] = !empty($submittedValues['overpayment_financial_type_id']) ? $submittedValues['overpayment_financial_type_id'] : NULL;
162177

163178
CRM_Financeextras_BAO_Company::create($params);
164179

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
use Civi\Api4\CreditNote;
4+
use Civi\Api4\Contribution;
5+
use Civi\Financeextras\Utils\OverpaymentUtils;
6+
use CRM_Financeextras_ExtensionUtil as E;
7+
8+
/**
9+
* Form to allocate overpayment to a new credit note.
10+
*/
11+
class CRM_Financeextras_Form_Contribute_OverpaymentAllocate extends CRM_Core_Form {
12+
13+
/**
14+
* Contribution ID.
15+
*
16+
* @var int
17+
*/
18+
protected $contributionId;
19+
/**
20+
* Contribution data.
21+
*
22+
* @var array
23+
*/
24+
protected $contribution;
25+
/**
26+
* Overpayment amount.
27+
*
28+
* @var float
29+
*/
30+
protected $overpaymentAmount;
31+
32+
/**
33+
* {@inheritDoc}
34+
*/
35+
public function preProcess() {
36+
$this->contributionId = CRM_Utils_Request::retrieve('contribution_id', 'Positive', $this, TRUE);
37+
38+
// Validate eligibility.
39+
if (!OverpaymentUtils::isEligibleForOverpaymentAllocation($this->contributionId)) {
40+
throw new CRM_Core_Exception(E::ts('This contribution is not eligible for overpayment allocation.'));
41+
}
42+
43+
$this->contribution = Contribution::get(FALSE)
44+
->addWhere('id', '=', $this->contributionId)
45+
->addSelect('*', 'contact_id.display_name')
46+
->execute()
47+
->first();
48+
49+
$this->overpaymentAmount = OverpaymentUtils::getOverpaymentAmount($this->contributionId);
50+
51+
CRM_Utils_System::setTitle(E::ts('Allocate overpayment to credit note'));
52+
}
53+
54+
/**
55+
* {@inheritDoc}
56+
*/
57+
public function buildQuickForm() {
58+
$currencySymbol = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_Currency', $this->contribution['currency'], 'symbol', 'name');
59+
$formattedAmount = CRM_Utils_Money::format($this->overpaymentAmount, $this->contribution['currency']);
60+
61+
$this->assign('message', E::ts('Allocate an overpaid amount to a new credit note. The credit can then be applied to future invoices. This cannot be undone.'));
62+
$this->assign('overpaymentAmount', $formattedAmount);
63+
$this->assign('contactName', $this->contribution['contact_id.display_name']);
64+
65+
$this->addButtons([
66+
[
67+
'type' => 'submit',
68+
'name' => E::ts('Save'),
69+
'isDefault' => TRUE,
70+
],
71+
[
72+
'type' => 'cancel',
73+
'name' => E::ts('Cancel'),
74+
],
75+
]);
76+
77+
parent::buildQuickForm();
78+
}
79+
80+
/**
81+
* {@inheritDoc}
82+
*/
83+
public function postProcess() {
84+
try {
85+
$result = CreditNote::allocateOverpayment(FALSE)
86+
->setContributionId($this->contributionId)
87+
->execute()
88+
->first();
89+
90+
CRM_Core_Session::setStatus(
91+
E::ts('Overpayment allocated to credit note "%1" successfully.', [1 => $result['cn_number']]),
92+
E::ts('Success'),
93+
'success'
94+
);
95+
}
96+
catch (\Throwable $th) {
97+
CRM_Core_Session::setStatus(
98+
E::ts('Error allocating overpayment: %1', [1 => $th->getMessage()]),
99+
E::ts('Error'),
100+
'error'
101+
);
102+
}
103+
}
104+
105+
}

CRM/Financeextras/Page/Company.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ public function run() {
1111
public function browse() {
1212
$optionGroupId = civicrm_api3('OptionGroup', 'Getvalue', ['name' => 'payment_instrument', 'return' => 'id']);
1313

14-
$getQuery = 'SELECT fc.*, cc.display_name as company_name, mt.msg_title as invoice_template_name, mt2.msg_title as creditnote_template_name, ov.label AS receivable_payment_method FROM financeextras_company fc
14+
$getQuery = 'SELECT fc.*, cc.display_name as company_name, mt.msg_title as invoice_template_name, mt2.msg_title as creditnote_template_name, ov.label AS receivable_payment_method, ft.name AS overpayment_financial_type_name FROM financeextras_company fc
1515
LEFT JOIN civicrm_contact cc on cc.id = fc.contact_id
1616
LEFT JOIN civicrm_msg_template mt ON mt.id = fc.invoice_template_id
1717
LEFT JOIN civicrm_msg_template mt2 ON mt2.id = fc.creditnote_template_id
18-
LEFT JOIN civicrm_option_value ov ON ov.value = fc.receivable_payment_method AND ov.option_group_id = ' . $optionGroupId . ' ORDER BY fc.id DESC
18+
LEFT JOIN civicrm_option_value ov ON ov.value = fc.receivable_payment_method AND ov.option_group_id = ' . $optionGroupId . '
19+
LEFT JOIN civicrm_financial_type ft ON ft.id = fc.overpayment_financial_type_id
20+
ORDER BY fc.id DESC
1921
';
2022
$company = CRM_Core_DAO::executeQuery($getQuery);
2123
$rows = [];

CRM/Financeextras/Upgrader.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,17 @@ public function upgrade_1005(): bool {
233233
}
234234
}
235235

236+
/**
237+
* Add overpayment_financial_type_id to Company table.
238+
*/
239+
public function upgrade_1006(): bool {
240+
$this->ctx->log->info('Applying update 1006 - Adding overpayment_financial_type_id to Company');
241+
242+
if (!\CRM_Core_BAO_SchemaHandler::checkIfFieldExists('financeextras_company', 'overpayment_financial_type_id', FALSE)) {
243+
$this->executeSqlFile('sql/upgrade_1006.sql');
244+
}
245+
246+
return TRUE;
247+
}
248+
236249
}

0 commit comments

Comments
 (0)