Failed Conditions
Push — master ( 2be5da...8667b1 )
by Adrien
04:39
created

AccountingReport::writeItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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