Formatting
[memberdb.git] / include / fees.php
1 <?php
2
3
4 /* HELPER functions FOR FEES {{{ */
5
6 // build an empty structure for holding monthly information
7 function _fees_build_month_array($start_date, $end_date, $preset = array())
8 {
9     $start_info = getdate($start_date);
10     $end_info = getdate($end_date);
11
12     if ($start_info['year'] > $end_info['year']) {
13         return $preset;
14     }
15     if ($start_info['year'] == $end_info['year'] && $start_info['mon'] > $end_info['mon']) {
16         return $preset;
17     }
18
19     $ret = $preset;
20     for ($year = $start_info['year']; $year <= $end_info['year']; $year++) {
21         if (!isset($ret[$year])) {
22             $ret[$year] = array();
23         }
24         for (
25             $month = (($year == $start_info['year']) ? $start_info['mon'] : 1);
26             $month <= (($year == $end_info['year']) ? $end_info['mon'] : 12);
27             $month++
28         ) {
29             if (isset($ret[$year][$month])) {
30                 continue;
31             }
32             $ret[$year][$month] = array(
33                 'is_member'        => null,
34                 'member_type'      => null,
35                 'fee'              => null,
36                 'payment_interval' => null
37             );
38         }
39     }
40     return $ret;
41 }
42
43 function _fees_apply_event_information(&$info, $events)
44 {
45     if (!empty($events)) {
46
47         foreach ($events as $event) {
48
49             $timestamp = db_date2unixtime($event['event_date']);
50             if ($timestamp < FOUNDING_DATE) {
51                 $timestamp = FOUNDING_DATE;
52             }
53             $date_info = getdate($timestamp);
54
55             if (!isset($info[$date_info['year']])) {
56                 continue;
57             }
58             if (!isset($info[$date_info['year']][$date_info['mon']])) {
59                 continue;
60             }
61
62             if ($event['fee'] !== null) {
63                 $info[$date_info['year']][$date_info['mon']]['fee'] = $event['fee'];
64             }
65             if ($event['member_type'] !== null) {
66                 $info[$date_info['year']][$date_info['mon']]['member_type'] = $event['member_type'];
67             }
68             if ($event['event_type'] !== 'changed') {
69                 $info[$date_info['year']][$date_info['mon']]['is_member'] = ($event['event_type'] == 'joined' ? 1 : 0);
70             }
71             if ($event['payment_interval'] !== null) {
72                 $info[$date_info['year']][$date_info['mon']]['payment_interval'] = $event['payment_interval'];
73             }
74         }
75     }
76
77     $fee = 0;
78     $member_type = null;
79     $is_member = false;
80     $payment_interval = null;
81
82     foreach (array_keys($info) as $year) {
83         foreach (array_keys($info[$year]) as $month) {
84             if (!isset($info[$year][$month]['fee'])) {
85                 $info[$year][$month]['fee'] = $fee;
86             } else {
87                 $fee = $info[$year][$month]['fee'];
88             }
89
90             if (!isset($info[$year][$month]['member_type'])) {
91                 $info[$year][$month]['member_type'] = $member_type;
92             } else {
93                 $member_type = $info[$year][$month]['member_type'];
94             }
95
96             if (!isset($info[$year][$month]['is_member'])) {
97                 $info[$year][$month]['is_member'] = $is_member;
98             } else {
99                 $is_member = $info[$year][$month]['is_member'];
100             }
101
102             if (!isset($info[$year][$month]['payment_interval'])) {
103                 $info[$year][$month]['payment_interval'] = $payment_interval;
104             } else {
105                 $payment_interval = $info[$year][$month]['payment_interval'];
106             }
107         }
108     }
109     return;
110 }
111
112 function fees_get_list_for_member($member_id, $end_date)
113 {
114
115     static $cache = array();
116
117     $end_date = mktime(0, 0, 0, date('m', $end_date) + 1, 0, date('Y', $end_date)); // last day of given month
118
119     if (isset($cache[$member_id][$end_date])) {
120         return $cache[$member_id][$end_date];
121     }
122     if (isset($cache[$member_id])) {
123         foreach (array_reverse(array_keys($cache[$member_id])) as $cache_date) {
124             if ($cache_date <= $end_date) {
125                 $ret = _fees_build_month_array($cache_date /* XXX einen Monat später wäre an dieser Stelle richtiger*/,
126                     $end_date, $cache[$member_id][$cache_date]);
127                 _fees_apply_event_information($ret, db_get_events_for_member($member_id, $cache_date, $end_date));
128                 $cache[$member_id][$end_date] = $ret;
129                 return $ret;
130             }
131         }
132     }
133
134     $ret = _fees_build_month_array(FOUNDING_DATE, $end_date);
135     if (empty($ret)) {
136         return;
137     }
138
139     _fees_apply_event_information($ret, db_get_events_for_member($member_id));
140
141     $cache[$member_id][$end_date] = $ret;
142     return $ret;
143 }
144
145 function fees_sum_for_member($member_id, $end_date)
146 {
147     $membership_info = fees_get_list_for_member($member_id, $end_date);
148
149     $total = '0';
150
151     foreach ($membership_info as $year => $months) {
152         foreach ($months as $month => $info) {
153             if ($info['is_member']) {
154                 $total = bcadd($total, $info['fee']);
155             }
156         }
157     }
158     return $total;
159 }
160
161 function fees_for_member_at_date($member_id, $end_date)
162 {
163     $membership_info = fees_get_list_for_member($member_id, $end_date);
164
165     $this_year = array_pop($membership_info);
166     $this_month = array_pop($this_year);
167     if ($this_month['is_member']) {
168         return $this_month['fee'];
169     }
170     return null;
171 }
172
173 function fees_info_for_member($member_id, $end_date)
174 {
175     $membership_info = fees_get_list_for_member($member_id, $end_date);
176
177     $this_year = array_pop($membership_info);
178     return array_pop($this_year);
179 }
180
181 function fees_sum_by_month($end_date)
182 {
183     $members = db_get_members();
184     $fees = array();
185     if (empty($members)) {
186         return array();
187     }
188     foreach ($members as $member) {
189         $membership_info = fees_get_list_for_member($member['id'], $end_date);
190         foreach ($membership_info as $year => $months) {
191             foreach ($months as $month => $info) {
192                 if (!isset($fees[$year][$month])) {
193                     $fees[$year][$month] = '0';
194                 }
195                 if ($info['is_member']) {
196                     $fees[$year][$month] = bcadd($fees[$year][$month], $info['fee']);
197                 }
198             }
199         }
200     }
201     return $fees;
202 }
203
204 function fees_get_list_for_month($year, $month)
205 {
206     $members = db_get_members();
207     $fees = array();
208     foreach ($members as $member) {
209         $membership_info = fees_get_list_for_member($member['id'], mktime(0, 0, 0, $month, 1, $year));
210
211         if (empty($membership_info)) {
212             continue;
213         }
214         $member['fee'] = $membership_info[$year][$month]['fee'];
215         $member['is_member'] = $membership_info[$year][$month]['is_member'];
216         $fees[] = $member;
217     }
218     return $fees;
219 }
220
221 function fee_next_directdebit_for_member($member_id, $max_date = null)
222 {
223
224     $member = db_get_member_with_id($member_id);
225     if (!$member['directdebit']) {
226         return null;
227     }
228
229     $sum_old_fees = fees_sum_for_member($member_id, DIRECTDEBIT_DATE - 86400);
230     $sum_new_paid = finance_get_paid_fees_for_member($member_id);
231     $year = date('Y', DIRECTDEBIT_DATE);
232     $month = date('n', DIRECTDEBIT_DATE);
233     $day = 1;
234
235
236     while (true) {
237         $start_date = mktime(0, 0, 0, $month, $day, $year);
238         if (isset($max_date) && $start_date > $max_date) {
239             return null;
240         }
241
242         // check if fee is zero at the moment and skip to next event
243         // quit searching if theres no event in future
244         $current_fee = fees_for_member_at_date($member_id, $start_date);
245         if (empty($current_fee)) {
246             $events = db_get_events_for_member($member_id, $start_date + 86400);
247             if (empty($events)) {
248                 return null;
249             }
250             $start_date = db_date2unixtime($events[0]['event_date']);
251             $day = date('j', $start_date);
252             $month = date('n', $start_date);
253             $year = date('Y', $start_date);
254             continue;
255         }
256
257         $sum_fees = fees_sum_for_member($member_id, $start_date);
258         $sum_new_fees = bcsub($sum_fees, $sum_old_fees);
259         if (bccomp($sum_new_fees, $sum_new_paid) == 1) {
260             $info = fees_get_list_for_member($member_id, $start_date);
261             $months = 1;
262             $ret = array(
263                 'date'  => $start_date,
264                 'value' => bcsub($sum_new_fees, $sum_new_paid),
265                 'info'  => '',
266             );
267             switch ($info[$year][$month]['payment_interval']) {
268                 case 'monthly'   :
269                     $months = 1;
270                     break;
271                 case 'quarterly' :
272                     $months = 3;
273                     break;
274                 case 'halfyearly':
275                     $months = 6;
276                     break;
277                 case 'yearly'    :
278                     $months = 12;
279                     break;
280             }
281             if ($months == 1) {
282                 $ret['info'] = dtaus_string(sprintf('CCCFFM %d, %s', $member['number'], format_month($start_date)));
283                 return $ret;
284             }
285             $end_date = mktime(0, 0, 0, $month + $months - 1, 1, $year);
286             $sum_fee_end = fees_sum_for_member($member_id, $end_date);
287             $ret['value'] = bcadd($ret['value'], bcsub($sum_fee_end, $sum_fees));
288             $ret['info'] = dtaus_string(sprintf('CCCFFM %d, %s-%s', $member['number'], format_month($start_date),
289                 format_month($end_date)));
290             return $ret;
291         }
292         $day = 1;
293         $month++;
294         if ($month == 13) {
295             $month = 1;
296             $year++;
297         }
298     }
299
300
301 }
302
303 /* }}} */
304
305
306 function action_fees()
307 {/*{{{*/
308
309     if (isset($_REQUEST['member_id'])) {
310         render_fees_for_member($_REQUEST['member_id']);
311         return;
312     }
313     if (isset($_REQUEST['year']) && isset($_REQUEST['month'])) {
314         render_accrued_fees_for_month($_REQUEST['year'], $_REQUEST['month']);
315         return;
316     }
317
318     render_fees_by_member();
319     render_accrued_fees_by_month();
320     render_next_direct_debit();
321     render_future_fees();
322
323 }/*}}}*/
324
325 function render_fees_by_member()
326 {/*{{{*/
327     $members = db_get_members();
328     ?>
329     <h2>Mitgliedsbeitr&auml;ge nach Mitglied</h2>
330     <table>
331         <tr>
332             <th>Mitgliedsnummer</th>
333             <th>Nickname</th>
334             <th style="text-align: right;">Angefallene Beitr&auml;ge</th>
335             <th style="text-align: right;">Aktueller Beitrag</th>
336             <th style="text-align: right;">Offener Beitrag</th>
337         </tr>
338         <?php if (empty($members)) {
339             $members = array();
340         } ?>
341         <?php foreach ($members as $member) : ?>
342             <?php
343             $current_fee = fees_for_member_at_date($member['id'], time());
344
345             $sum_fees = fees_sum_for_member($member['id'], time());
346             $sum_old_fees = fees_sum_for_member($member['id'], DIRECTDEBIT_DATE - 86400);
347             $sum_old_paid = finance_get_paid_fees_for_member($member['id'], true);
348             $sum_new_paid = finance_get_paid_fees_for_member($member['id']);
349             $sum_new_fees = bcsub($sum_fees, $sum_old_fees);
350             $open_fees = bcadd(bcsub($sum_old_fees, $sum_old_paid), max(bcsub($sum_new_fees, $sum_new_paid), 0));
351             ?>
352             <tr>
353                 <td><a href="<?= html_escape(link_to('fees',
354                         array('member_id' => $member['id']))) ?>"><?= html_escape($member['number']) ?></a></td>
355                 <td><?= html_escape($member['nickname']) ?></td>
356                 <td style="text-align: right;"><?= format_money($sum_fees) ?></td>
357                 <td style="text-align: right;"><?= isset($current_fee) ? format_money($current_fee) : '-' ?></td>
358                 <td style="text-align: right;"><?= $open_fees > 0 ? format_money($open_fees) : '-' ?></td>
359             </tr>
360         <?php endforeach ?>
361     </table>
362     <?php
363 }/*}}}*/
364
365 function render_future_fees()
366 {/*{{{*/
367     $total_paid = finance_get_total_paid_fees();
368     $this_year = date('Y');
369     $this_month = date('m');
370     $fees = fees_sum_by_month(mktime(0, 0, 0, date('m') + 6, date('d'), date('Y') + 1));
371     $total = 0;
372     foreach ($fees as $year => $months) {
373         foreach ($months as $month => $fee) {
374             $total = bcadd($total, $fee);
375             $fees[$year][$month] = array('total' => $total, 'fee' => $fee);
376         }
377     }
378     $fees = array_reverse($fees, true);
379     ?>
380     <h2>Beitragsprognose nach Monat</h2>
381     <table>
382         <tr>
383             <th>Monat</th>
384             <th style="text-align: right;">Mitgliedsbeitr&auml;ge</th>
385             <th style="text-align: right;">kummuliert</th>
386             <th style="text-align: right;">eingenommen</th>
387             <th style="text-align: right;"><strong>offen</strong></th>
388         </tr>
389         <?php foreach ($fees as $year => $months) : ?>
390             <?php $months = array_reverse($months, true); ?>
391             <?php foreach ($months as $month => $data) : ?>
392                 <tr<?php if ($year == $this_year && $month == $this_month) : ?> class="current"<?php endif ?>>
393                     <td><a href="<?= html_escape(link_to('fees',
394                             array('year' => $year, 'month' => $month))) ?>"><?= html_escape(format_month($year,
395                                 $month)) ?></a></td>
396                     <td style="text-align: right;"><?= html_escape(format_money($data['fee'])) ?></td>
397                     <td style="text-align: right;"><?= html_escape(format_money($data['total'])) ?></td>
398                     <td style="text-align: right;">
399                         <?php if ($year == $this_year && $month == $this_month) : ?>
400                             <?= html_escape(format_money($total_paid)) ?>
401                         <?php endif ?>
402                     </td>
403                     <td style="text-align: right;">
404                         <?php if ($year > $this_year || ($year >= $this_year && $month >= $this_month)) : ?>
405                             <?= html_escape(format_money(bcsub($data['total'], $total_paid))) ?>
406                         <?php endif ?>
407                     </td>
408                 </tr>
409             <?php endforeach ?>
410         <?php endforeach ?>
411     </table>
412     <?php
413 }/*}}}*/
414
415 function render_accrued_fees_by_month()
416 {/*{{{*/
417     $fees = fees_sum_by_month(time());
418     $fees = array_reverse($fees, true);
419     ?>
420     <h2>Angefallene Mitgliedsbeitr&auml;ge nach Monat</h2>
421     <table>
422         <tr>
423             <th>Monat</th>
424             <th style="text-align: right;">Mitgliedsbeitrag</th>
425         </tr>
426         <?php foreach ($fees as $year => $months) : ?>
427             <?php $months = array_reverse($months, true); ?>
428             <?php foreach ($months as $month => $fee) : ?>
429                 <tr>
430                     <td><a href="<?= html_escape(link_to('fees',
431                             array('year' => $year, 'month' => $month))) ?>"><?= html_escape(format_month($year,
432                                 $month)) ?></a></td>
433                     <td style="text-align: right;"><?= html_escape(format_money($fee)) ?></td>
434                 </tr>
435             <?php endforeach ?>
436         <?php endforeach ?>
437     </table>
438     <?php
439 }/*}}}*/
440
441 function render_accrued_fees_for_month($year, $month)
442 {/*{{{*/
443     $fees = fees_get_list_for_month($year, $month);
444     ?>
445     <h2>Angefallene Mitgliedsbeitr&auml;ge f&uuml;r <?= format_month($year, $month) ?></h2>
446     <table>
447         <tr>
448             <th>Mitgliedsnummer</th>
449             <th>Nickname</th>
450             <th style="text-align: right;">Mitgliedsbeitrag</th>
451         </tr>
452         <?php foreach ($fees as $info) : ?>
453             <tr>
454                 <td><a href="<?= html_escape(link_to('fees',
455                         array('member_id' => $info['id']))) ?>"><?= html_escape($info['number']) ?></a></td>
456                 <td><?= html_escape($info['nickname']) ?></td>
457                 <td style="text-align: right;"><?= html_escape($info['is_member'] ? format_money($info['fee']) : '-') ?></td>
458             </tr>
459         <?php endforeach ?>
460     </table>
461     <p><a href="<?= html_escape(link_to('fees')) ?>">Alle angefallenen Mitgliedsbeitr&auml;ge</a></p>
462     <?php
463 }/*}}}*/
464
465 function render_next_direct_debit()
466 {/*{{{*/
467     $members = db_get_members();
468     ?>
469     <h2>Nächste Abbuchungen nach Mitglied</h2>
470     <table>
471         <tr>
472             <th>Mitgliedsnummer</th>
473             <th>Nickname</th>
474             <th style="text-align: right;">Verwendungszweck</th>
475             <th style="text-align: right;">Betrag</th>
476         </tr>
477         <?php if (empty($members)) {
478             $members = array();
479         } ?>
480         <?php foreach ($members as $member) : ?>
481             <?php $next_debit = fee_next_directdebit_for_member($member['id']); ?>
482             <tr>
483                 <td><a href="<?= html_escape(link_to('fees',
484                         array('member_id' => $member['id']))) ?>"><?= html_escape($member['number']) ?></a></td>
485                 <td><?= html_escape($member['nickname']) ?></td>
486                 <?php if (empty($next_debit)) : ?>
487                     <td>-</td>
488                     <td style="text-align: right;">-</td>
489                 <?php else : ?>
490                     <td><?= html_escape($next_debit['info']) ?></td>
491                     <td style="text-align: right;"><?= format_money($next_debit['value']) ?></td>
492                 <?php endif ?>
493             </tr>
494         <?php endforeach ?>
495     </table>
496     <?php
497 }
498
499 function render_fees_for_member($member_id)
500 {/*{{{*/
501     global $MEMBER_TYPES, $EARNING_TYPES, $EXPENSE_TYPES;
502
503     $member = db_get_member_with_id($member_id);
504     if (!isset($member)) {
505         redirect(link_to('fees'));
506     }
507
508     $membership_info = fees_get_list_for_member($member_id, time());
509     $membership_info = array_reverse($membership_info, true);
510
511     $paid_fees = finance_list_paid_fees_for_member($member_id, time(), true);
512
513     $sum_new_paid = finance_get_paid_fees_for_member($member_id);
514     $sum_old_paid = finance_get_paid_fees_for_member($member_id, true);
515     $sum_old_fees = fees_sum_for_member($member_id, DIRECTDEBIT_DATE - 86400);
516     $sum_fees = fees_sum_for_member($member_id, time());
517     $sum_new_fees = bcsub($sum_fees, $sum_old_fees);
518
519     $state = '';
520     $new_open = 0;
521     $old_open = 0;
522     if (bccomp($sum_new_fees, $sum_new_paid) == 1) {
523         $new_open = 1;
524     }
525     if (bccomp($sum_old_fees, $sum_old_paid) == 1) {
526         $old_open = 1;
527     }
528
529     if ($new_open && $old_open) {
530         $state = sprintf('Es sind noch %1$s Mitgliedsbeitrag offen, davon %2$s für die Zeit vor dem %3$s und %4$s für danach.',
531             format_money(bcadd(bcsub($sum_old_fees, $sum_old_paid), bcsub($sum_new_fees, $sum_new_paid))),
532             format_money(bcsub($sum_old_fees, $sum_old_paid)),
533             format_date(DIRECTDEBIT_DATE),
534             format_money(bcsub($sum_new_fees, $sum_new_paid))
535         );
536     } elseif ($new_open) {
537         $state = sprintf('Es sind noch %1$s Mitgliedsbeitrag offen.',
538             format_money(bcsub($sum_new_fees, $sum_new_paid)));
539     } elseif ($old_open) {
540         $state = sprintf('Für die Zeit vor dem %1$s sind noch %2$s Mitgliedsbeitrag offen.',
541             format_date(DIRECTDEBIT_DATE), format_money(bcsub($sum_old_fees, $sum_old_paid)));
542     }
543
544     $next_debit = fee_next_directdebit_for_member($member_id);
545
546     ?>
547     <h2>Mitgliedsbeitr&auml;ge
548         von <?= html_escape(!empty($member['nickname']) ? $member['nickname'] : sprintf('Mitglied Nr. %d',
549             $member['number'])) ?></h2>
550     <h3>Mitgliedsdetails</h3>
551     <table>
552         <tr>
553             <th>Mitgliedsnummer</th>
554             <th>Nickname</th>
555             <th>Status</th>
556         </tr>
557         <tr>
558             <td><a href="<?= html_escape(link_to('view_member',
559                     array('id' => $member['id']))) ?>"><?= html_escape($member['number']) ?></a></td>
560             <td><?= html_escape($member['nickname']) ?></strong></p></td>
561             <td>
562                 <?php if (empty($state)) : ?>
563                     Kein Beitragsrückstand
564                 <?php else : ?>
565                     <?= wordwrap(html_escape($state), 70, '<br/>') ?>
566                 <?php endif ?>
567             </td>
568         </tr>
569     </table>
570     <div style="float: left">
571         <h3>Angefallene Mitgliedsbeitr&auml;ge</h3>
572         <table>
573             <tr>
574                 <th>Monat</th>
575                 <th>Mitgliedsart</th>
576                 <th style="text-align: right;">Mitgliedsbeitrag</th>
577             </tr>
578             <?php foreach ($membership_info as $year => $months) : ?>
579                 <?php $months = array_reverse($months, true); ?>
580                 <?php foreach ($months as $month => $info) : ?>
581                     <tr>
582                         <td><?= html_escape(format_month($year, $month)) ?></td>
583                         <td><?= html_escape($info['is_member'] ? $MEMBER_TYPES[$info['member_type']] : 'Kein Mitglied') ?></td>
584                         <td style="text-align: right;"><?= html_escape($info['is_member'] ? format_money($info['fee']) : '-') ?></td>
585                     </tr>
586                 <?php endforeach ?>
587             <?php endforeach ?>
588         </table>
589         <p><a href="<?= html_escape(link_to('fees')) ?>">Alle angefallenen Mitgliedsbeitr&auml;ge</a></p>
590     </div>
591     <div style="float: left; margin-left: 1em;">
592         <h3>Nächste Abbuchung</h3>
593         <table>
594             <tr>
595                 <th>Verwendungszweck</th>
596                 <th style="text-align: right;">Betrag</th>
597             </tr>
598             <?php if (empty($next_debit)) : ?>
599                 <td>-</td>
600                 <td style="text-align: right;">-</td>
601             <?php else : ?>
602                 <td><?= html_escape($next_debit['info']) ?></td>
603                 <td style="text-align: right;"><?= format_money($next_debit['value']) ?></td>
604             <?php endif ?>
605         </table>
606         <h3>Bezahlte Mitgliedsbeitr&auml;ge</h3>
607         <table>
608             <tr>
609                 <th>Monat</th>
610                 <th style="text-align: right;">Typ</th>
611                 <th style="text-align: right;">Betrag</th>
612             </tr>
613             <?php foreach ($paid_fees as $payment) : ?>
614                 <tr>
615                     <td><?= html_escape(format_date(db_date2unixtime($payment['date']))) ?></td>
616                     <td><?= ($payment['value'] < 0) ? $EXPENSE_TYPES[$payment['type']] : $EARNING_TYPES[$payment['type']] ?></td>
617                     <td style="text-align: right;"><?= format_money($payment['value']) ?></td>
618                 </tr>
619             <?php endforeach ?>
620         </table>
621     </div>
622     <br style="clear: left;"/>
623     <?php
624 }/*}}}*/