AccountingReport::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service\Exporter;
6
7
use Application\Enum\AccountType;
8
use Application\Model\Account;
9
use Cake\Chronos\ChronosDate;
10
use Ecodev\Felix\Api\Exception;
11
use Ecodev\Felix\Format;
12
use Money\Money;
13
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
14
use PhpOffice\PhpSpreadsheet\Style\Alignment;
15
use PhpOffice\PhpSpreadsheet\Style\Color;
16
use PhpOffice\PhpSpreadsheet\Style\Fill;
17
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
18
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
19
20
/**
21
 * @extends AbstractExcel<Account>
22
 *
23
 * @phpstan-type Data array{
24
 *     account: ?Account,
25
 *     code: int | '',
26
 *     name: string,
27
 *     depth: int,
28
 *     balance: Money,
29
 *     balancePrevious: ?Money,
30
 *     budgetAllowed: ?Money,
31
 *     budgetBalance: ?Money,
32
 *     format?: array,
33
 *     formatPrevious?: array,
34
 * }
35
 */
36
class AccountingReport extends AbstractExcel
37
{
38
    private ChronosDate $date;
39
40
    private bool $showBudget = false;
41
42
    private ?ChronosDate $datePrevious = null;
43
44
    /**
45
     * @var Data[]
46
     */
47
    private array $assets = [];
48
49
    /**
50
     * @var Data[]
51
     */
52
    private array $liabilities = [];
53
54
    /**
55
     * @var Data[]
56
     */
57
    private array $expenses = [];
58
59
    /**
60
     * @var Data[]
61
     */
62
    private array $revenues = [];
63
64
    private static array $balanceFormat = [
65
        'fill' => [
66
            'fillType' => Fill::FILL_SOLID,
67
            'startColor' => [
68
                'argb' => 'FFDDDDDD',
69
            ],
70
        ],
71
        'numberFormat' => [
72
            'formatCode' => NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1, // eg. 12'345.67
73
        ],
74
    ];
75
76
    protected static array $ctrlFormat = [
77
        'font' => [
78
            'color' => [
79
                'bold' => false,
80
                'argb' => 'FF606060',
81
            ],
82
        ],
83
        'alignment' => ['wrapText' => true],
84
        'fill' => [
85
            'fillType' => Fill::FILL_SOLID,
86
            'startColor' => [
87
                'argb' => 'FFDDDDDD',
88
            ],
89
        ],
90
    ];
91
92
    private static array $wrappedFormat = [
93
        'alignment' => [
94
            'wrapText' => true,
95
        ],
96
    ];
97
98
    private static array $indentFormat = [
99
        'alignment' => [
100
            'horizontal' => Alignment::HORIZONTAL_LEFT,
101
            'indent' => 1,
102
        ],
103
    ];
104
105
    private static array $columnWidth = [
106
        'accountCode' => 14,
107
        'accountName' => 38,
108
        'balance' => 12,
109
    ];
110
111 1
    public function __construct(string $hostname, private readonly array $accountingConfig)
112
    {
113 1
        parent::__construct($hostname);
114
115 1
        $this->zebra = false;
116 1
        $this->autoFilter = false;
117 1
        $this->date = ChronosDate::today();
118
119 1
        $this->sheet->setTitle(_tr('Bilan') . ' & ' . _tr('Résultat'));
120
    }
121
122 1
    protected function getTitleForFilename(): string
123
    {
124 1
        return _tr('compta_rapport_%date%', ['date' => $this->date->format('Y-m-d')]);
125
    }
126
127 1
    public function setDate(ChronosDate $date): void
128
    {
129 1
        $this->date = $date;
130
    }
131
132 1
    public function setDatePrevious(?ChronosDate $datePrevious): void
133
    {
134 1
        $this->datePrevious = $datePrevious;
135
    }
136
137 1
    public function showBudget(bool $showBudget): void
138
    {
139 1
        $this->showBudget = $showBudget;
140
    }
141
142 1
    protected function writeTitle(): void
143
    {
144 1
        $this->column = 1;
145 1
        $this->sheet->mergeCells([$this->column, $this->row, $this->column + $this->getColspan(), $this->row]);
146 1
        $this->write(
147 1
            sprintf($this->hostname . ': rapport comptable au %s', $this->date->format('d.m.Y')),
148 1
            self::$titleFormat,
149 1
            self::$centerFormat
150 1
        );
151 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(35);
152 1
        ++$this->row;
153
    }
154
155 1
    private function processAccount(Account $account, int $depth): void
156
    {
157 1
        $balance = $account->getBalanceAtDate($this->date);
158 1
        $balancePrevious = $this->datePrevious ? $account->getBalanceAtDate($this->datePrevious) : null;
159
        // Skip the account if:
160
        // - accounting is configured to hide accounts with zero balance
161
        // - AND account at report date has zero balance
162
        // - AND account at previous date has zero balance (but only if "show previous year" mode is enabled)
163 1
        if ($this->accountingConfig['report']['showAccountsWithZeroBalance'] === false && $depth > 1 && $balance->isZero() && (!$this->datePrevious || ($balancePrevious && $balancePrevious->isZero()))) {
164
            return;
165
        }
166
167 1
        if ($account->getType() === AccountType::Equity) {
168
            // Don't show special accounts since it's an interim statement, their balance will be computed manually
169 1
            return;
170
        }
171
172 1
        $data = [
173 1
            'code' => $account->getCode(),
174 1
            'name' => Format::truncate($account->getName(), 55),
175 1
            'depth' => $depth,
176 1
            'balance' => $balance,
177 1
            'balancePrevious' => $balancePrevious,
178 1
            'budgetAllowed' => $this->showBudget ? $account->getBudgetAllowed() : null,
179 1
            'budgetBalance' => $this->showBudget ? $account->getBudgetBalance() : null,
180 1
            'account' => $account,
181 1
        ];
182
183 1
        if ($this->isAccountType($account, AccountType::Asset)) {
184 1
            $this->assets[] = $data;
185 1
        } elseif ($this->isAccountType($account, AccountType::Liability)) {
186 1
            $this->liabilities[] = $data;
187 1
        } elseif ($this->isAccountType($account, AccountType::Revenue)) {
188 1
            $this->revenues[] = $data;
189 1
        } elseif ($this->isAccountType($account, AccountType::Expense)) {
190 1
            $this->expenses[] = $data;
191
        }
192
193 1
        if ($account->getType() === AccountType::Group && $depth <= $this->accountingConfig['report']['maxAccountDepth']) {
194 1
            foreach ($account->getChildren() as $child) {
195 1
                $this->processAccount($child, $depth + 1);
196
            }
197
        }
198
    }
199
200
    /**
201
     * @param Account $item
202
     */
203 1
    protected function writeItem($item): void
204
    {
205
        // This is unusual because we don't write anything but only collect data for later
206 1
        $this->processAccount($item, 1);
207
    }
208
209
    /**
210
     * Compute the profit or loss (at current and previous dates) and insert the result into the list of accounts.
211
     */
212 1
    private function insertProfitOrLoss(): void
213
    {
214 1
        $profitOrLoss = $this->getProfitOrLoss(false);
215
216 1
        if ($profitOrLoss->isZero()) {
217
            return; // If financial result is balanced, it likely a final accounting report so we don't show the intermediate result
218
        }
219
220 1
        $data = [
221 1
            'depth' => 1,
222 1
            'code' => '',
223 1
            'name' => _tr('Résultat intermédiaire (bénéfice / -perte)'),
224 1
            'account' => null,
225 1
            'balance' => $profitOrLoss, // can be positive of negative
226 1
            'balancePrevious' => null,
227 1
            'budgetAllowed' => null,
228 1
            'budgetBalance' => null,
229 1
            'format' => $this->color($profitOrLoss),
230 1
        ];
231
232 1
        if ($this->datePrevious) {
233 1
            $profitOrLossPrevious = $this->getProfitOrLoss(true);
234 1
            $data['balancePrevious'] = $profitOrLossPrevious;
235 1
            $data['formatPrevious'] = $this->color($profitOrLossPrevious);
236
        }
237
238
        // A profit is reported as a POSITIVE green number, and a loss is reported a NEGATIVE red number.
239
        // They are identical lines at the end of both LIABILITIES and EXPENSES columns
240 1
        $this->liabilities[] = $data;
241 1
        $this->expenses[] = $data;
242
    }
243
244
    /**
245
     * @param Data $data
246
     */
247 1
    private function maybeBold(array $data, int $untilDepth): array
248
    {
249 1
        return $data['depth'] <= $untilDepth ? ['font' => ['bold' => true]] : [];
250
    }
251
252 1
    private function realWrite(): void
253
    {
254 1
        $this->insertProfitOrLoss();
255
256
        /*
257
         * Page 1
258
         * BALANCE SHEET (Asset vs Liabilities)
259
         */
260
261 1
        $this->lastDataColumn = 1 + $this->getColspan();
262 1
        $this->column = $initialColumn = 1;
263
264 1
        $this->balanceSheetHeaders($initialColumn);
265
266
        // Assets
267 1
        $this->column = $initialColumn;
268 1
        $initialRow = $this->row;
269 1
        $this->lastDataRow = $this->row;
270 1
        $this->balanceSheet($initialColumn, $this->assets);
271
272
        // Liabilities
273 1
        $this->row = $initialRow;
274 1
        $this->column = $initialColumn = $initialColumn + $this->getColspan() / 2 + 1;
275 1
        $this->balanceSheet($initialColumn, $this->liabilities);
276
277 1
        $this->row = $this->lastDataRow + 1;
278 1
        $this->writeTotals($this->assets, $this->liabilities);
279
280 1
        $this->applyExtraFormatting(5);
281
        // set printing area for page 1
282 1
        $this->sheet->getPageSetup()->setPrintAreaByColumnAndRow(1, 1, $this->lastDataColumn, $this->lastDataRow, 0, 'I');
283
284
        /*
285
         * Page 2
286
         * INCOME STATEMENT (Profit vs Loss)
287
         */
288
289
        // start at the bottom of the balance sheet
290 1
        $this->row = $this->lastDataRow + 1;
291 1
        $this->column = $initialColumn = 1;
292 1
        $this->incomeStatementHeaders($initialColumn);
293
294
        // Expenses
295 1
        $initialRow = ++$this->row;
296 1
        $this->column = $initialColumn;
297 1
        $this->incomeStatement($initialColumn, $this->expenses);
298
299
        // Revenues
300 1
        $this->row = $initialRow;
301 1
        $this->column = $initialColumn = $initialColumn + $this->getColspan() / 2 + 1;
302 1
        $this->incomeStatement($initialColumn, $this->revenues);
303
304 1
        $this->row = $this->lastDataRow + 1;
305 1
        $this->writeTotals($this->expenses, $this->revenues);
306
307 1
        $this->applyExtraFormatting($initialRow);
308
        // set printing area for page 2
309 1
        $this->sheet->getPageSetup()->setPrintAreaByColumnAndRow(1, $initialRow - 3, $this->lastDataColumn, $this->lastDataRow + 1, 1, 'I');
310
    }
311
312 1
    private function getProfitOrLoss(bool $isPreviousDate): Money
313
    {
314
        // Sum the profit and loss root accounts
315 1
        $totalRevenues = $this->sumBalance($this->revenues, $isPreviousDate);
316
317 1
        $totalExpenses = $this->sumBalance($this->expenses, $isPreviousDate);
318
319 1
        return $totalRevenues->subtract($totalExpenses);
320
    }
321
322 1
    protected function finalize(string $path): void
323
    {
324
        // Once we collected all data, we can actually write them all
325 1
        $this->realWrite();
326
327
        // Print on A4 portrait, scale to full page width, variable height (depending on number of accounts)
328 1
        $pageSetup = $this->sheet->getPageSetup();
329 1
        $pageSetup->setOrientation(PageSetup::ORIENTATION_PORTRAIT);
330 1
        $pageSetup->setPaperSize(PageSetup::PAPERSIZE_A4);
331 1
        $pageSetup->setFitToWidth(1);
332 1
        $pageSetup->setFitToHeight(0);
333 1
        $pageSetup->setHorizontalCentered(true);
334 1
        $margins = $this->sheet->getPageMargins();
335 1
        $margins->setTop(0.5);
336 1
        $margins->setRight(0.2);
337 1
        $margins->setLeft(0.2);
338 1
        $margins->setBottom(0.5);
339
340 1
        parent::finalize($path);
341
    }
342
343
    // Insert a row with the control totals
344 1
    protected function writeTotals(array $accountsColumn1, array $accountsColumn2): void
345
    {
346 1
        $this->column = $this->firstDataColumn;
347 1
        foreach ([$accountsColumn1, $accountsColumn2] as $col => $accounts) {
348
            // Account.code
349 1
            $this->write(mb_strtoupper(_tr('Contrôle')));
350
            // Account.name
351 1
            $this->write('');
352
            // Account.balance
353 1
            $cellsToSum = $this->cellsToSum($accounts, 1);
354 1
            $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
355
            // Account previous balance (optional)
356 1
            if ($this->datePrevious) {
357 1
                $cellsToSum = $this->cellsToSum($accounts, 1, 'cellPrevious');
358 1
                $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
359
            }
360
            // Budget columns (optional)
361 1
            if ($this->showBudget) {
362 1
                $cellsToSum = $this->cellsToSum($accounts, 1, 'cellBudgetAllowed');
363 1
                $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
364 1
                $cellsToSum = $this->cellsToSum($accounts, 1, 'cellBudgetBalance');
365 1
                $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
366
            }
367 1
            if ($col === 0) {
368
                // Inner columns gap
369 1
                $this->write('');
370
            }
371
        }
372 1
        $this->lastDataRow = $this->row;
373
        // Apply style
374 1
        $range = Coordinate::stringFromColumnIndex($this->firstDataColumn) . $this->row . ':' . Coordinate::stringFromColumnIndex($this->column - 1) . $this->row;
375 1
        $this->sheet->getStyle($range)->applyFromArray(self::$ctrlFormat);
376
    }
377
378 1
    private function applyExtraFormatting(int $startRow): void
379
    {
380 1
        if ($this->datePrevious && !$this->showBudget) {
381
            $columnsToFormat = [3, 4, 8, 9];
382 1
        } elseif (!$this->datePrevious && $this->showBudget) {
383
            $columnsToFormat = [3, 4, 5, 9, 10, 11];
384 1
        } elseif ($this->datePrevious && $this->showBudget) {
385 1
            $columnsToFormat = [3, 4, 5, 6, 10, 11, 12, 13];
386
        } else {
387
            // Only current balance cols
388 1
            $columnsToFormat = [3, 7];
389
        }
390 1
        foreach ($columnsToFormat as $colIndex) {
391
            // Format balance numbers
392 1
            $range = Coordinate::stringFromColumnIndex($colIndex) . $startRow . ':' . Coordinate::stringFromColumnIndex($colIndex) . $this->lastDataRow;
393 1
            $this->sheet->getStyle($range)->applyFromArray(self::$balanceFormat);
394
        }
395
396
        // Increase row height since account names can wrap on multiple lines
397 1
        for ($r = $startRow; $r <= $this->lastDataRow; ++$r) {
398 1
            $this->sheet->getRowDimension($r)->setRowHeight(30);
399
        }
400
    }
401
402 1
    private function cellsToSum(array $data, int $depth, string $dataIndex = 'cell'): array
403
    {
404 1
        $equityAccountsClasses = $this->accountingConfig['report']['accountClasses']['equity'];
405 1
        $cells = array_reduce($data, function (array $carry, $data) use ($equityAccountsClasses, $depth, $dataIndex) {
406
            // We only sum accounts at the given depth, plus equity special accounts
407 1
            if (isset($data[$dataIndex]) && ($data['depth'] === $depth || in_array(mb_substr((string) $data['code'], 0, 1), $equityAccountsClasses, true))) {
408 1
                $carry[] = $data[$dataIndex];
409
            }
410
411 1
            return $carry;
412 1
        }, []);
413
414 1
        return $cells;
415
    }
416
417
    /**
418
     * Sum root or special accounts balance (for the profit and loss calculation)
419
     * - Root accounts have depth = 1
420
     * - Special accounts have code 7xxx, 8xxx, 9xxx.
421
     *
422
     * @param Data[] $data profits or expenses
423
     */
424 1
    private function sumBalance(array $data, bool $isPreviousDate): Money
425
    {
426 1
        $sum = array_reduce($data, function (Money $carry, $data) use ($isPreviousDate) {
427 1
            if ($data['depth'] === 1 || (int) mb_substr((string) $data['code'], 0, 1) > 6) {
428 1
                return $carry->add($isPreviousDate ? $data['balancePrevious'] : $data['balance']);
429
            }
430
431 1
            return $carry;
432 1
        }, Money::CHF(0));
433
434 1
        return $sum;
435
    }
436
437 1
    private function balanceSheetHeaders(int $initialColumn): void
438
    {
439 1
        $this->sheet->mergeCells([$this->column, $this->row, $this->column + $this->getColspan(), $this->row]);
440 1
        $this->write(
441 1
            _tr('Bilan'),
442 1
            self::$titleFormat,
443 1
            self::$centerFormat
444 1
        );
445 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(40);
446 1
        ++$this->row;
447
448
        // Header line 1
449 1
        $headers = [
450 1
            ['label' => _tr('Actifs'), 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => $this->getColspan() / 2],
451 1
            ['label' => '', 'width' => 3, 'formats' => []], // gap
452 1
            ['label' => _tr('Passifs'), 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => $this->getColspan() / 2],
453 1
        ];
454 1
        $this->column = $initialColumn;
455 1
        $this->writeHeaders($headers);
456 1
        ++$this->row;
457
458
        // Header line 2: date(s) of balance
459 1
        $headers = [
460 1
            ['label' => '', 'colspan' => 2], // empty margin
461 1
            ['label' => $this->date->format('d.m.Y'), 'formats' => [self::$headerFormat, self::$centerFormat]], // current date
462 1
        ];
463 1
        if ($this->datePrevious) {
464 1
            $headers[] = ['label' => $this->datePrevious->format('d.m.Y'), 'formats' => [self::$headerFormat, self::$centerFormat]]; // previous date
465
        }
466 1
        if ($this->showBudget) {
467 1
            $headers[] = ['label' => _tr('Budget prévu'), 'formats' => [self::$headerFormat, self::$centerFormat]];
468 1
            $headers[] = ['label' => _tr('Budget restant'), 'formats' => [self::$headerFormat, self::$centerFormat]];
469
        }
470
471 1
        $headers[] = ['label' => '', 'formats' => []]; // gap
472 1
        $headers[] = ['label' => '', 'colspan' => 2]; // empty margin
473 1
        $headers[] = ['label' => $this->date->format('d.m.Y'), 'formats' => [self::$headerFormat, self::$centerFormat]]; // current date
474 1
        if ($this->datePrevious) {
475 1
            $headers[] = ['label' => $this->datePrevious->format('d.m.Y'), 'formats' => [self::$headerFormat, self::$centerFormat]]; // previous date
476
        }
477 1
        if ($this->showBudget) {
478 1
            $headers[] = ['label' => _tr('Budget prévu'), 'formats' => [self::$headerFormat, self::$centerFormat]];
479 1
            $headers[] = ['label' => _tr('Budget restant'), 'formats' => [self::$headerFormat, self::$centerFormat]];
480
        }
481
482 1
        $this->column = $initialColumn;
483 1
        $this->writeHeaders($headers);
484 1
        ++$this->row;
485
    }
486
487
    /**
488
     * @param Data[] $allData
489
     */
490 1
    private function balanceSheet(int $initialColumn, array &$allData): void
491
    {
492
        // Store coordinates (ie. E3) of the 2nd level account budget cells to later use in formula
493 1
        $budgetAllowedTotalCells = '';
494 1
        $budgetBalanceTotalCells = '';
495
496 1
        $firstLine = true;
497
498 1
        foreach ($allData as $index => $data) {
499
            // Column: account code
500 1
            if ($firstLine) {
501 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountCode']);
502
            }
503 1
            $maybeBold = $this->maybeBold($data, 2);
504 1
            if (!$firstLine && $maybeBold) {
505 1
                ++$this->row;
506
            }
507
508 1
            $this->write(
509 1
                str_repeat('  ', $data['depth'] - 1) . $data['code'],
510 1
                self::$indentFormat,
511 1
                $maybeBold
512 1
            );
513
514
            // Column: account name
515 1
            if ($firstLine) {
516 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountName']);
517
            }
518 1
            $this->write($data['name'], self::$wrappedFormat, $maybeBold);
519
520
            // Column: balance at date
521 1
            if ($firstLine) {
522 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
523
            }
524
            // Store the coordinate of the cell to later compute totals
525 1
            $allData[$index]['cell'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
526 1
            $this->write($data['balance'], self::$balanceFormat, $maybeBold, $data['format'] ?? []);
527
528
            // Column: balance at previous date (optional)
529 1
            if ($this->datePrevious) {
530 1
                $allData[$index]['cellPrevious'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
531 1
                if ($firstLine) {
532 1
                    $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
533
                }
534 1
                $this->write($data['balancePrevious'], $maybeBold, $data['formatPrevious'] ?? []);
535
            }
536
537
            // Budget columns (optional)
538 1
            if ($this->showBudget) {
539 1
                $allData[$index]['cellBudgetAllowed'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
540 1
                if ($firstLine) {
541 1
                    $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
542 1
                    $budgetAllowedTotalCells = Coordinate::stringFromColumnIndex($this->column) . $this->row;
543
                }
544 1
                $this->write($data['budgetAllowed'] ?? '', self::$balanceFormat, $maybeBold);
545
546 1
                $allData[$index]['cellBudgetBalance'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
547 1
                if ($firstLine) {
548 1
                    $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
549 1
                    $budgetBalanceTotalCells = Coordinate::stringFromColumnIndex($this->column) . $this->row;
550
                }
551 1
                $this->write($data['budgetBalance'] ?? '', self::$balanceFormat, $maybeBold);
552
            }
553
554 1
            $firstLine = false;
555 1
            ++$this->row;
556 1
            $this->column = $initialColumn;
557
558 1
            $this->lastDataRow = max($this->lastDataRow, $this->row);
559
        }
560
561
        // Compute the sum of budgets columns using an Excel formula
562
        // We don't do it for account balance, since the total of group accounts is computed in DB
563
        // Level 2 (= direct child accounts)
564 1
        if ($this->showBudget) {
565 1
            $cellsToSum = $this->cellsToSum($allData, 2, 'cellBudgetAllowed');
566 1
            if ($cellsToSum) {
567 1
                $this->sheet->setCellValue($budgetAllowedTotalCells, '=SUM(' . implode(',', $cellsToSum) . ')');
568
            }
569 1
            $cellsToSum = $this->cellsToSum($allData, 2, 'cellBudgetBalance');
570 1
            if ($cellsToSum) {
571 1
                $this->sheet->setCellValue($budgetBalanceTotalCells, '=SUM(' . implode(',', $cellsToSum) . ')');
572
            }
573
        }
574
    }
575
576 1
    private function getColspan(): int
577
    {
578 1
        $colspan = 6;
579 1
        if ($this->datePrevious) {
580 1
            $colspan += 2;
581
        }
582 1
        if ($this->showBudget) {
583 1
            $colspan += 4;
584
        }
585
586 1
        return $colspan;
587
    }
588
589 1
    private function incomeStatementHeaders(int $initialColumn): void
590
    {
591 1
        $this->sheet->mergeCells([$this->column, $this->row, $this->column + $this->getColspan(), $this->row]);
592 1
        $this->write(
593 1
            _tr('Compte de résultat'),
594 1
            self::$titleFormat,
595 1
            self::$centerFormat
596 1
        );
597 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(40);
598 1
        ++$this->row;
599
600
        // Header line 1
601 1
        $headers = [
602 1
            ['label' => _tr('Charges'), 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => $this->getColspan() / 2],
603 1
            ['label' => '', 'width' => 3, 'formats' => []], // gap
604 1
            ['label' => _tr('Profits'), 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => $this->getColspan() / 2],
605 1
        ];
606 1
        $this->column = $initialColumn;
607 1
        $this->writeHeaders($headers);
608 1
        ++$this->row;
609
610
        // Header line 2: date(s) of balance
611 1
        $headers = [
612 1
            ['label' => '', 'colspan' => 2], // empty margin
613 1
            ['label' => $this->date->format('d.m.Y'), 'formats' => [self::$headerFormat, self::$centerFormat]], // current date
614 1
        ];
615 1
        if ($this->datePrevious) {
616 1
            $headers[] = ['label' => $this->datePrevious->format('d.m.Y'), 'formats' => [self::$headerFormat, self::$centerFormat]]; // previous date
617
        }
618 1
        if ($this->showBudget) {
619 1
            $headers[] = ['label' => _tr('Budget prévu'), 'formats' => [self::$headerFormat, self::$centerFormat]];
620 1
            $headers[] = ['label' => _tr('Budget restant'), 'formats' => [self::$headerFormat, self::$centerFormat]];
621
        }
622
623 1
        $headers[] = ['label' => '', 'formats' => []]; // gap
624 1
        $headers[] = ['label' => '', 'colspan' => 2]; // empty margin
625 1
        $headers[] = ['label' => $this->date->format('d.m.Y'), 'formats' => [self::$headerFormat, self::$centerFormat]]; // current date
626 1
        if ($this->datePrevious) {
627 1
            $headers[] = ['label' => $this->datePrevious->format('d.m.Y'), 'formats' => [self::$headerFormat, self::$centerFormat]]; // previous date
628
        }
629 1
        if ($this->showBudget) {
630 1
            $headers[] = ['label' => _tr('Budget prévu'), 'formats' => [self::$headerFormat, self::$centerFormat]];
631 1
            $headers[] = ['label' => _tr('Budget restant'), 'formats' => [self::$headerFormat, self::$centerFormat]];
632
        }
633
634 1
        $this->column = $initialColumn;
635 1
        $this->writeHeaders($headers);
636 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(1.2, 'cm');
637
    }
638
639
    /**
640
     * @param Data[] $allData
641
     */
642 1
    private function incomeStatement(int $initialColumn, array &$allData): void
643
    {
644 1
        $firstLine = true;
645 1
        foreach ($allData as $index => $data) {
646
            // Column: account code
647 1
            $maybeBold = $this->maybeBold($data, 1);
648 1
            if (!$firstLine && $maybeBold) {
649 1
                ++$this->row;
650
            }
651
652 1
            $this->write(
653 1
                str_repeat('  ', $data['depth'] - 1) . $data['code'],
654 1
                self::$indentFormat,
655 1
                $maybeBold
656 1
            );
657
658
            // Column: account name
659 1
            $this->write($data['name'], self::$wrappedFormat, $maybeBold);
660
661
            // Column: balance at date
662
            // Store the coordinate of the cell to later compute totals
663 1
            $allData[$index]['cell'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
664 1
            $this->write($data['balance'], self::$balanceFormat, $maybeBold, $data['format'] ?? []);
665
666
            // Column: balance at previous date (optional)
667 1
            if ($this->datePrevious) {
668 1
                $allData[$index]['cellPrevious'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
669 1
                $this->write($data['balancePrevious'], $maybeBold, $data['formatPrevious'] ?? []);
670
            }
671
672
            // Budget columns (optional)
673 1
            if ($this->showBudget) {
674 1
                $allData[$index]['cellBudgetAllowed'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
675 1
                $this->write($data['budgetAllowed'] ?? '', self::$balanceFormat, $maybeBold);
676
677 1
                $allData[$index]['cellBudgetBalance'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
678 1
                $this->write($data['budgetBalance'] ?? '', self::$balanceFormat, $maybeBold);
679
            }
680
681 1
            ++$this->row;
682 1
            $this->column = $initialColumn;
683
684 1
            $this->lastDataRow = max($this->lastDataRow, $this->row);
685 1
            $firstLine = false;
686
        }
687
    }
688
689 1
    private function color(Money $profitOrLoss): array
690
    {
691 1
        return [
692 1
            'font' => [
693 1
                'color' => [
694 1
                    'argb' => $profitOrLoss->isPositive() ? Color::COLOR_DARKGREEN : Color::COLOR_RED,
695 1
                ],
696 1
            ],
697 1
        ];
698
    }
699
700 1
    private function isAccountType(Account $account, AccountType $accountType): bool
701
    {
702 1
        return $account->getType() === $accountType
703 1
            || (
704 1
                $account->getType() === AccountType::Group
705 1
                && $this->isCodeAccountType($account->getCode(), $accountType)
706 1
            );
707
    }
708
709 1
    private function isCodeAccountType(string|int $code, AccountType $accountType): bool
710
    {
711 1
        $accountClasses = $this->accountingConfig['report']['accountClasses'];
712 1
        $class = match ($accountType) {
713 1
            AccountType::Asset => $accountClasses['assets'],
714 1
            AccountType::Liability => $accountClasses['liabilities'],
715 1
            AccountType::Revenue => $accountClasses['revenues'],
716 1
            AccountType::Expense => $accountClasses['expenses'],
717 1
            default => throw new Exception('Non supported related account type: ' . $accountType->value),
718 1
        };
719
720 1
        return in_array(mb_substr((string) $code, 0, 1), $class, true);
721
    }
722
}
723