Passed
Push — master ( bdc919...c8a2a2 )
by Sylvain
35:03
created

AccountingReport::setDatePrevious()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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