Failed Conditions
Push — master ( cd6515...587c3f )
by Sylvain
14:03
created

AccountingReport::incomeStatementHeaders()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 44
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
eloc 31
c 0
b 0
f 0
dl 0
loc 44
ccs 0
cts 0
cp 0
rs 8.8017
cc 6
nc 16
nop 1
crap 42
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service\Exporter;
6
7
use Application\DBAL\Types\AccountTypeType;
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 1
    private array $liabilities = [];
49
50 1
    /**
51
     * @var Data[]
52 1
     */
53
    private array $expenses = [];
54 1
55 1
    /**
56 1
     * @var Data[]
57
     */
58
    private array $revenues = [];
59 1
60
    private static array $balanceFormat = [
61 1
        'fill' => [
62
            'fillType' => Fill::FILL_SOLID,
63
            'startColor' => [
64 1
                'argb' => 'FFDDDDDD',
65
            ],
66 1
        ],
67
        'numberFormat' => [
68
            'formatCode' => NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1, // eg. 12'345.67
69 1
        ],
70
    ];
71 1
72 1
    private static array $wrappedFormat = [
73 1
        'alignment' => [
74 1
            'wrapText' => true,
75 1
        ],
76 1
    ];
77 1
78 1
    private static array $indentFormat = [
79 1
        'alignment' => [
80
            'horizontal' => Alignment::HORIZONTAL_LEFT,
81 1
            'indent' => 1,
82 1
        ],
83 1
    ];
84 1
85 1
    private static array $columnWidth = [
86 1
        'accountCode' => 11,
87 1
        'accountName' => 38,
88 1
        'balance' => 12,
89 1
    ];
90 1
91 1
    public function __construct(string $hostname, private readonly array $accountingConfig)
92 1
    {
93 1
        parent::__construct($hostname);
94 1
95
        $this->zebra = false;
96 1
        $this->autoFilter = false;
97 1
        $this->date = ChronosDate::today();
98
99
        $this->sheet->setTitle(_tr('Bilan') . ' & ' . _tr('Résultat'));
100 1
    }
101
102 1
    protected function getTitleForFilename(): string
103 1
    {
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 1
112 1
    public function showBudget(bool $showBudget): void
113 1
    {
114 1
        $this->showBudget = $showBudget;
115 1
    }
116 1
117 1
    protected function writeTitle(): void
118 1
    {
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
        $this->sheet->getRowDimension($this->row)->setRowHeight(35);
127 1
        ++$this->row;
128 1
    }
129 1
130
    private function processAccount(Account $account, int $depth): void
131
    {
132
        $balance = $account->getBalanceAtDate($this->date);
133
        if ($this->accountingConfig['report']['showAccountsWithZeroBalance'] === false && $depth > 1 && $balance->isZero()) {
134
            return;
135
        }
136
137 1
        if ($account->getType() === AccountTypeType::EQUITY) {
138
            // Don't show special accounts since it's an interim statement, their balance will be computed manually
139 1
            return;
140
        }
141 1
142
        $data = [
143 1
            'code' => $account->getCode(),
144
            'name' => Format::truncate($account->getName(), 55),
145 1
            'depth' => $depth,
146
            'balance' => $balance,
147
            'balancePrevious' => $this->showBudget ? $account->getTotalBalanceFormer() : null,
148
            'budgetAllowed' => $this->showBudget ? $account->getBudgetAllowed() : null,
149
            'budgetBalance' => $this->showBudget ? $account->getBudgetBalance() : null,
150
            'account' => $account,
151
        ];
152
153
        $accountClasses = $this->accountingConfig['report']['accountClasses'];
154
        if ($account->getType() === AccountTypeType::ASSET || ($account->getType() === AccountTypeType::GROUP && in_array(mb_substr((string) $account->getCode(), 0, 1), $accountClasses['assets'], true))) {
155
            $this->assets[] = $data;
156
        } elseif ($account->getType() === AccountTypeType::LIABILITY || ($account->getType() === AccountTypeType::GROUP && in_array(mb_substr((string) $account->getCode(), 0, 1), $accountClasses['liabilities'], true))) {
157
            $this->liabilities[] = $data;
158
        } elseif ($account->getType() === AccountTypeType::REVENUE || ($account->getType() === AccountTypeType::GROUP && in_array(mb_substr((string) $account->getCode(), 0, 1), $accountClasses['revenues'], true))) {
159 1
            $this->revenues[] = $data;
160 1
        } elseif ($account->getType() === AccountTypeType::EXPENSE || ($account->getType() === AccountTypeType::GROUP && in_array(mb_substr((string) $account->getCode(), 0, 1), $accountClasses['expenses'], true))) {
161 1
            $this->expenses[] = $data;
162 1
        }
163 1
164 1
        if ($account->getType() === AccountTypeType::GROUP && $depth <= $this->accountingConfig['report']['maxAccountDepth']) {
165 1
            foreach ($account->getChildren() as $child) {
166 1
                $this->processAccount($child, $depth + 1);
167 1
            }
168 1
        }
169
    }
170
171
    /**
172 1
     * @param Account $item
173 1
     */
174 1
    protected function writeItem($item): void
175 1
    {
176 1
        // This is unusual because we don't write anything but only collect data for later
177 1
        $this->processAccount($item, 1);
178 1
    }
179
180 1
    /**
181 1
     * Compute the profit or loss (at current and previous dates) and insert the result into the list of accounts.
182 1
     */
183 1
    private function insertProfitOrLoss(): void
184 1
    {
185 1
        $profitOrLoss = $this->getProfitOrLoss(false);
186 1
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 1
        }
190 1
191 1
        $data = [
192
            'depth' => 1,
193
            'code' => '',
194 1
            'name' => _tr('Résultat intermédiaire (bénéfice / -perte)'),
195 1
            'account' => null,
196
            'balance' => $profitOrLoss, // can be positive of negative
197 1
            'balancePrevious' => null,
198 1
            'format' => $this->color($profitOrLoss),
199 1
        ];
200
201 1
        // A profit is reported as a POSITIVE green number, and a loss is reported a NEGATIVE red number.
202
        // They are identical lines at the end of both LIABILITIES and EXPENSES columns
203
        $this->liabilities[] = $data;
204
        $this->expenses[] = $data;
205 1
    }
206 1
207 1
    /**
208 1
     * @param Data $data
1 ignored issue
show
Bug introduced by
The type Application\Service\Exporter\Data was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
209 1
     */
210 1
    private function maybeBold(array $data, int $untilDepth): array
211
    {
212
        return $data['depth'] <= $untilDepth ? ['font' => ['bold' => true]] : [];
213 1
    }
214 1
215 1
    private function realWrite(): void
216 1
    {
217 1
        $this->insertProfitOrLoss();
218
219 1
        /*
220 1
         * Page 1
221 1
         * BALANCE SHEET (Asset vs Liabilities)
222 1
         */
223 1
224 1
        $this->lastDataColumn = $this->showBudget ? 13 : 7;
225 1
        $this->column = $initialColumn = 1;
226
227 1
        $this->balanceSheetHeaders($initialColumn);
228 1
229 1
        // Assets
230 1
        $this->column = $initialColumn;
231
        $initialRow = $this->row;
232 1
        $this->lastDataRow = $this->row;
233
        $this->balanceSheet($initialColumn, $this->assets);
234
235
        // Liabilities
236 1
        $this->row = $initialRow;
237 1
        $this->column = $initialColumn = $initialColumn + ($this->showBudget ? 7 : 4);
238 1
        $this->balanceSheet($initialColumn, $this->liabilities);
239 1
240 1
        $this->applyExtraFormatting(6);
241 1
        // set printing area for page 1
242
        $this->sheet->getPageSetup()->setPrintAreaByColumnAndRow(1, 1, $this->lastDataColumn, $this->lastDataRow, 0, 'I');
243 1
244 1
        /*
245 1
         * Page 2
246 1
         * INCOME STATEMENT (Profit vs Loss)
247 1
         */
248 1
249 1
        // start at the bottom of the balance sheet
250 1
        $this->row = $this->lastDataRow + 1;
251
        $this->column = $initialColumn = 1;
252 1
        $this->incomeStatementHeaders($initialColumn);
253 1
254 1
        // Expenses
255
        $initialRow = ++$this->row;
256
        $this->column = $initialColumn;
257 1
        $this->incomeStatement($initialColumn, $this->expenses);
258 1
259 1
        // Revenues
260 1
        $this->row = $initialRow;
261 1
        $this->column = $initialColumn = $initialColumn + ($this->showBudget ? 7 : 4);
262
        $this->incomeStatement($initialColumn, $this->revenues);
263 1
264
        $this->row = $this->lastDataRow + 1;
265
        $this->applyExtraFormatting($initialRow);
266
        // set printing area for page 2
267 1
        $this->sheet->getPageSetup()->setPrintAreaByColumnAndRow(1, $initialRow - 3, $this->lastDataColumn, $this->lastDataRow + 1, 1, 'I');
268 1
    }
269 1
270 1
    private function getProfitOrLoss(bool $isPreviousDate): Money
271 1
    {
272 1
        // Sum the profit and loss root accounts
273
        $totalRevenues = $this->sumBalance($this->revenues, $isPreviousDate);
274
275 1
        $totalExpenses = $this->sumBalance($this->expenses, $isPreviousDate);
276 1
277 1
        return $totalRevenues->subtract($totalExpenses);
278 1
    }
279 1
280
    protected function finalize(string $path): void
281 1
    {
282 1
        // Once we collected all data, we can actually write them all
283 1
        $this->realWrite();
284 1
285 1
        // Print on A4 portrait, scale to full page width, variable height (depending on number of accounts)
286 1
        $pageSetup = $this->sheet->getPageSetup();
287 1
        $pageSetup->setOrientation(PageSetup::ORIENTATION_PORTRAIT);
288
        $pageSetup->setPaperSize(PageSetup::PAPERSIZE_A4);
289 1
        $pageSetup->setFitToWidth(1);
290 1
        $pageSetup->setFitToHeight(0);
291 1
        $pageSetup->setHorizontalCentered(true);
292 1
        $margins = $this->sheet->getPageMargins();
293
        $margins->setTop(0.5);
294 1
        $margins->setRight(0.2);
295
        $margins->setLeft(0.2);
296
        $margins->setBottom(0.5);
297 1
298
        parent::finalize($path);
299
    }
300 1
301
    protected function writeFooter(): void
302
    {
303 1
        // EXPENSES
304
        // Account.code
305 1
        $this->write('');
306
        // Account.name
307 1
        $this->write('');
308
        // Account.balance
309
        $cellsToSum = $this->cellsToSum($this->expenses, 1, 'cellBalance');
310 1
        $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
311
        // Budget columns (optional)
312 1
        if ($this->showBudget) {
313
            $cellsToSum = $this->cellsToSum($this->expenses, 1, 'cellBalancePrevious');
314 1
            $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
315 1
            $cellsToSum = $this->cellsToSum($this->expenses, 1, 'cellBudgetAllowed');
316 1
            $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
317
            $cellsToSum = $this->cellsToSum($this->expenses, 1, 'cellBudgetBalance');
318 1
            $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
319
        }
320 1
321 1
        // Margin
322 1
        $this->write('');
323
324 1
        // REVENUES
325
        // Account.code
326
        $this->write('');
327 1
        // Account.name
328
        $this->write('');
329 1
        // Account.balance
330
        $cellsToSum = $this->cellsToSum($this->revenues, 1, 'cellBalance');
331
        $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
332
        // Account previous balance (optional)
333
        if ($this->showBudget) {
334
            $cellsToSum = $this->cellsToSum($this->revenues, 1, 'cellBalancePrevious');
335 1
            $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
336
            $cellsToSum = $this->cellsToSum($this->revenues, 1, 'cellBudgetAllowed');
337 1
            $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
338
            $cellsToSum = $this->cellsToSum($this->revenues, 1, 'cellBudgetBalance');
339 1
            $this->write($cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '', self::$balanceFormat, self::$totalFormat);
340
        }
341
342 1
        // Apply style
343
        $range = Coordinate::stringFromColumnIndex($this->firstDataColumn) . $this->row . ':' . Coordinate::stringFromColumnIndex($this->column - 1) . $this->row;
344
        $this->sheet->getStyle($range)->applyFromArray(self::$totalFormat);
345
    }
346 1
347
    private function applyExtraFormatting(int $startRow): void
348
    {
349 1
        $columnsToFormat = $this->showBudget ? [3, 4, 5, 6, 10, 11, 12, 13] : [3, 7];
350
        foreach ($columnsToFormat as $colIndex) {
351 1
            // Format balance numbers
352
            $range = Coordinate::stringFromColumnIndex($colIndex) . $startRow . ':' . Coordinate::stringFromColumnIndex($colIndex) . $this->lastDataRow;
353
            $this->sheet->getStyle($range)->applyFromArray(self::$balanceFormat);
354 1
        }
355
356
        // Increase row height since account names can wrap on multiple lines
357
        for ($r = $startRow; $r <= $this->lastDataRow; ++$r) {
358
            $this->sheet->getRowDimension($r)->setRowHeight(30);
359
        }
360 1
    }
361
362 1
    private function cellsToSum(array $data, int $depth, string $dataIndex = 'cell'): array
363
    {
364 1
        $equityAccountsClasses = $this->accountingConfig['report']['accountClasses']['equity'];
365
        $cells = array_reduce($data, function (array $carry, $data) use ($equityAccountsClasses, $depth, $dataIndex) {
366
            // We only sum accounts at the given depth, plus equity special accounts
367 1
            if (isset($data[$dataIndex]) && ($data['depth'] === $depth || in_array(mb_substr((string) $data['code'], 0, 1), $equityAccountsClasses, true))) {
368
                $carry[] = $data[$dataIndex];
369
            }
370
371 1
            return $carry;
372
        }, []);
373 1
374
        return $cells;
375 1
    }
376
377
    /**
378 1
     * Sum root or special accounts balance (for the profit and loss calculation)
379 1
     * - Root accounts have depth = 1
380
     * - Special accounts have code 7xxx, 8xxx, 9xxx.
381
     *
382 1
     * @param Data[] $data profits or expenses
383
     */
384
    private function sumBalance(array $data, bool $isPreviousDate): Money
385 1
    {
386 1
        $sum = array_reduce($data, function (Money $carry, $data) use ($isPreviousDate) {
387 1
            if ($data['depth'] === 1 || (int) mb_substr((string) $data['code'], 0, 1) > 6) {
388
                return $carry->add($isPreviousDate ? $data['balancePrevious'] : $data['balance']);
389
            }
390
391 1
            return $carry;
392 1
        }, Money::CHF(0));
393
394
        return $sum;
395
    }
396 1
397
    private function balanceSheetHeaders(int $initialColumn): void
398 1
    {
399 1
        $this->sheet->mergeCells([$this->column, $this->row, $this->column + ($this->showBudget ? 12 : 6), $this->row]);
400 1
        $this->write(
401
            _tr('Bilan'),
402
            self::$titleFormat,
403 1
            self::$centerFormat
404
        );
405 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(40);
406 1
        ++$this->row;
407 1
408
        // Header line 1
409
        $headers = [
410 1
            ['label' => _tr('Actifs'), 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => $this->showBudget ? 6 : 3],
411 1
            ['label' => '', 'width' => 3, 'formats' => []], // gap
412
            ['label' => _tr('Passifs'), 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => $this->showBudget ? 6 : 3],
413 1
        ];
414
        $this->column = $initialColumn;
415
        $this->writeHeaders($headers);
416 1
        ++$this->row;
417
418 1
        // Header line 2: date(s) of balance
419 1
        $headers = [
420 1
            ['label' => '', 'colspan' => 2], // empty margin
421
            ['label' => 'Solde', 'formats' => [self::$headerFormat, self::$centerFormat]],
422
        ];
423 1
        if ($this->showBudget) {
424 1
            $headers[] = ['label' => 'Solde précédent', 'formats' => [self::$headerFormat, self::$centerFormat]];
425
            $headers[] = ['label' => 'Budget prévu', 'formats' => [self::$headerFormat, self::$centerFormat]];
426 1
            $headers[] = ['label' => 'Budget restant', 'formats' => [self::$headerFormat, self::$centerFormat]];
427
        }
428
429
        $headers[] = ['label' => '', 'formats' => []]; // gap
430
431
        $headers[] = ['label' => '', 'colspan' => 2]; // empty margin
432
        $headers[] = ['label' => 'Solde', 'formats' => [self::$headerFormat, self::$centerFormat]];
433
        if ($this->showBudget) {
434
            $headers[] = ['label' => 'Solde précédent', 'formats' => [self::$headerFormat, self::$centerFormat]];
435
            $headers[] = ['label' => 'Budget prévu', 'formats' => [self::$headerFormat, self::$centerFormat]];
436
            $headers[] = ['label' => 'Budget restant', 'formats' => [self::$headerFormat, self::$centerFormat]];
437
        }
438
        $this->column = $initialColumn;
439
        $this->writeHeaders($headers);
440
        $this->sheet->getRowDimension($this->row)->setRowHeight(1.2, 'cm');
441
        ++$this->row;
442
    }
443
444
    /**
445
     * @param Data[] $allData
446
     */
447
    private function balanceSheet(int $initialColumn, array &$allData): void
448
    {
449
        // Coordinates (i.e. E3) of the cells with the totals
450
        $currentTotalCells = '';
451
        $previousTotalCells = '';
452
        $budgetAllowedTotalCells = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $budgetAllowedTotalCells is dead and can be removed.
Loading history...
453
        $budgetBalanceTotalCells = '';
454
        $firstLine = true;
455
456
        foreach ($allData as $index => $data) {
457
            // Column: account code
458
            if ($firstLine) {
459
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountCode']);
460
            }
461
            $maybeBold = $this->maybeBold($data, 2);
1 ignored issue
show
Bug introduced by
$data of type Application\Service\Exporter\Data is incompatible with the type array expected by parameter $data of Application\Service\Expo...tingReport::maybeBold(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

461
            $maybeBold = $this->maybeBold(/** @scrutinizer ignore-type */ $data, 2);
Loading history...
462
            if (!$firstLine && $maybeBold) {
463
                ++$this->row;
464
            }
465
466
            $this->write(
467
                str_repeat('  ', $data['depth'] - 1) . $data['code'],
468
                self::$indentFormat,
469
                $maybeBold
470
            );
471
472
            // Column: account name
473
            if ($firstLine) {
474
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountName']);
475
            }
476
            $this->write($data['name'], self::$wrappedFormat, $maybeBold);
477
478
            // Column: balance at date
479
            if ($firstLine) {
480
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
481
                $currentTotalCells = Coordinate::stringFromColumnIndex($this->column) . $this->row;
482
            }
483
            // Store the coordinate of the cell to later compute totals
484
            $allData[$index]['cell'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
485
            $this->write($data['balance'], self::$balanceFormat, $maybeBold, $data['format'] ?? []);
486
487
            // Budget columns (optional)
488
            if ($this->showBudget) {
489
                $allData[$index]['cellBalancePrevious'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
490
                if ($firstLine) {
491
                    $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
492
                    $previousTotalCells = Coordinate::stringFromColumnIndex($this->column) . $this->row;
493
                }
494
                $this->write($data['balancePrevious'] ?? '', self::$balanceFormat, $maybeBold);
495
496
                $allData[$index]['cellBudgetAllowed'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
497
                if ($firstLine) {
498
                    $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
499
                }
500
                $this->write($data['budgetAllowed'] ?? '', self::$balanceFormat, $maybeBold);
501
502
                $allData[$index]['cellBudgetBalance'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
503
                if ($firstLine) {
504
                    $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
505
                    $budgetBalanceTotalCells = Coordinate::stringFromColumnIndex($this->column) . $this->row;
506
                }
507
                $this->write($data['budgetBalance'] ?? '', self::$balanceFormat, $maybeBold);
508
            }
509
510
            $firstLine = false;
511
            ++$this->row;
512
            $this->column = $initialColumn;
513
514
            $this->lastDataRow = max($this->lastDataRow, $this->row);
515
        }
516
517
        // Replace the total value computed from database by a formula computed from the child accounts cells
518
        // Level 2 (= direct child accounts)
519
        $cellsToSum = $this->cellsToSum($allData, 2, 'cellBalance');
520
        if ($cellsToSum) {
521
            $this->sheet->setCellValue($currentTotalCells, '=SUM(' . implode(',', $cellsToSum) . ')');
522
        }
523
        if ($this->showBudget) {
524
            $cellsToSum = $this->cellsToSum($allData, 2, 'cellBalancePrevious');
525
            if ($cellsToSum) {
526
                $this->sheet->setCellValue($previousTotalCells, '=SUM(' . implode(',', $cellsToSum) . ')');
527
            }
528
            $cellsToSum = $this->cellsToSum($allData, 2, 'cellBudgetBalance');
529
            if ($cellsToSum) {
530
                $this->sheet->setCellValue($budgetBalanceTotalCells, '=SUM(' . implode(',', $cellsToSum) . ')');
531
            }
532
        }
533
    }
534
535
    private function incomeStatementHeaders(int $initialColumn): void
536
    {
537
        $this->sheet->mergeCells([$this->column, $this->row, $this->column + ($this->showBudget ? 12 : 6), $this->row]);
538
        $this->write(
539
            _tr('Compte de résultat'),
540
            self::$titleFormat,
541
            self::$centerFormat
542
        );
543
        $this->sheet->getRowDimension($this->row)->setRowHeight(40);
544
        ++$this->row;
545
546
        // Header line 1
547
        $headers = [
548
            ['label' => _tr('Charges'), 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => $this->showBudget ? 6 : 3],
549
            ['label' => '', 'width' => 3, 'formats' => []], // gap
550
            ['label' => _tr('Profits'), 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => $this->showBudget ? 6 : 3],
551
        ];
552
        $this->column = $initialColumn;
553
        $this->writeHeaders($headers);
554
        ++$this->row;
555
556
        // Header line 2: date(s) of balance
557
        $headers = [
558
            ['label' => '', 'colspan' => 2], // empty margin
559
            ['label' => 'Solde', 'formats' => [self::$headerFormat, self::$centerFormat]],
560
        ];
561
        if ($this->showBudget) {
562
            $headers[] = ['label' => 'Solde précédent', 'formats' => [self::$headerFormat, self::$centerFormat]];
563
            $headers[] = ['label' => 'Budget prévu', 'formats' => [self::$headerFormat, self::$centerFormat]];
564
            $headers[] = ['label' => 'Budget restant', 'formats' => [self::$headerFormat, self::$centerFormat]];
565
        }
566
567
        $headers[] = ['label' => '', 'formats' => []]; // gap
568
569
        $headers[] = ['label' => '', 'colspan' => 2]; // empty margin
570
        $headers[] = ['label' => 'Solde', 'formats' => [self::$headerFormat, self::$centerFormat]];
571
        if ($this->showBudget) {
572
            $headers[] = ['label' => 'Solde précédent', 'formats' => [self::$headerFormat, self::$centerFormat]];
573
            $headers[] = ['label' => 'Budget prévu', 'formats' => [self::$headerFormat, self::$centerFormat]];
574
            $headers[] = ['label' => 'Budget restant', 'formats' => [self::$headerFormat, self::$centerFormat]];
575
        }
576
        $this->column = $initialColumn;
577
        $this->writeHeaders($headers);
578
        $this->sheet->getRowDimension($this->row)->setRowHeight(1.2, 'cm');
579
    }
580
581
    /**
582
     * @param Data[] $allData
583
     */
584
    private function incomeStatement(int $initialColumn, array &$allData): void
585
    {
586
        $firstLine = true;
587
        foreach ($allData as $index => $data) {
588
            // Column: account code
589
            $maybeBold = $this->maybeBold($data, 1);
1 ignored issue
show
Bug introduced by
$data of type Application\Service\Exporter\Data is incompatible with the type array expected by parameter $data of Application\Service\Expo...tingReport::maybeBold(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

589
            $maybeBold = $this->maybeBold(/** @scrutinizer ignore-type */ $data, 1);
Loading history...
590
            if (!$firstLine && $maybeBold) {
591
                ++$this->row;
592
            }
593
594
            $this->write(
595
                str_repeat('  ', $data['depth'] - 1) . $data['code'],
596
                self::$indentFormat,
597
                $maybeBold
598
            );
599
600
            // Column: account name
601
            $this->write($data['name'], self::$wrappedFormat, $maybeBold);
602
603
            // Column: balance at date
604
            // Store the coordinate of the cell to later compute totals
605
            $allData[$index]['cellBalance'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
606
            $this->write($data['balance'], self::$balanceFormat, $maybeBold, $data['format'] ?? []);
607
608
            // Budget columns (optional)
609
            if ($this->showBudget) {
610
                $allData[$index]['cellBalancePrevious'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
611
                $this->write($data['balancePrevious'] ?? '', self::$balanceFormat, $maybeBold);
612
613
                $allData[$index]['cellBudgetAllowed'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
614
                $this->write($data['budgetAllowed'] ?? '', self::$balanceFormat, $maybeBold);
615
616
                $allData[$index]['cellBudgetBalance'] = $this->sheet->getCell([$this->column, $this->row])->getCoordinate();
617
                $this->write($data['budgetBalance'] ?? '', self::$balanceFormat, $maybeBold);
618
            }
619
620
            ++$this->row;
621
            $this->column = $initialColumn;
622
623
            $this->lastDataRow = max($this->lastDataRow, $this->row);
624
            $firstLine = false;
625
        }
626
    }
627
628
    private function color(Money $profitOrLoss): array
629
    {
630
        return [
631
            'font' => [
632
                'color' => [
633
                    'argb' => $profitOrLoss->isPositive() ? Color::COLOR_DARKGREEN : Color::COLOR_RED,
634
                ],
635
            ],
636
        ];
637
    }
638
}
639