Passed
Push — master ( f8361b...5ed10a )
by Adrien
07:04
created

AccountingReport::processAccounts()   D

Complexity

Conditions 21
Paths 13

Size

Total Lines 30
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 21.0475

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 30
ccs 20
cts 21
cp 0.9524
rs 4.1666
c 0
b 0
f 0
cc 21
nc 13
nop 2
crap 21.0475

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Date;
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
18
class AccountingReport extends AbstractExcel
19
{
20
    private Date $date;
21
22
    private array $assets = [];
23
24
    private array $liabilities = [];
25
26
    private array $expenses = [];
27
28
    private array $revenues = [];
29
30
    private static array $balanceFormat = [
31
        'fill' => [
32
            'fillType' => Fill::FILL_SOLID,
33
            'startColor' => [
34
                'argb' => 'FFDDDDDD',
35
            ],
36
        ],
37
        'numberFormat' => [
38
            'formatCode' => NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1, // eg. 12'345.67
39
        ],
40
    ];
41
42
    private static array $columnWidth = [
43
        'accountCode' => 11,
44
        'accountName' => 35,
45
        'balance' => 12,
46
    ];
47
48 1
    public function __construct(string $hostname, private readonly array $accountingConfig)
49
    {
50 1
        parent::__construct($hostname);
51
52 1
        $this->date = Date::today();
53
54 1
        $this->sheet->setTitle('Bilan + PP');
55 1
        $this->zebra = false;
56 1
        $this->autoFilter = false;
57
    }
58
59 1
    protected function getTitleForFilename(): string
60
    {
61 1
        return sprintf('compta_rapport_%s', $this->date->format('Y-m-d'));
62
    }
63
64 1
    public function setDate(Date $date): void
65
    {
66 1
        $this->date = $date;
67
    }
68
69 1
    protected function writeTitle(): void
70
    {
71 1
        $this->column = 1;
72 1
        $this->sheet->mergeCellsByColumnAndRow($this->column, $this->row, $this->column + 14, $this->row);
73 1
        $this->write(
74 1
            sprintf($this->hostname . ': rapport comptable au %s', $this->date->format('d.m.Y')),
75 1
            self::$titleFormat,
76 1
            self::$centerFormat
77
        );
78 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(35);
79 1
        ++$this->row;
80
81 1
        $this->column = 1;
82 1
        $this->sheet->mergeCellsByColumnAndRow($this->column, $this->row, $this->column + 6, $this->row);
83 1
        $this->write(
84
            'Bilan',
85 1
            self::$titleFormat,
86 1
            self::$centerFormat
87
        );
88 1
        $this->column = 9;
89 1
        $this->sheet->mergeCellsByColumnAndRow($this->column, $this->row, $this->column + 6, $this->row);
90 1
        $this->write(
91
            'Résultat',
92 1
            self::$titleFormat,
93 1
            self::$centerFormat
94
        );
95
96 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(35);
97 1
        ++$this->row;
98
    }
99
100 1
    private function processAccounts(array $accounts, int $depth): void
101
    {
102 1
        foreach ($accounts as $account) {
103 1
            $balance = $account->getBalanceAtDate($this->date);
104 1
            if ($this->accountingConfig['report']['showAccountsWithZeroBalance'] === false && $depth > 1 && $balance->isZero()) {
105
                continue;
106
            }
107 1
            if ($account->getType() === AccountTypeType::EQUITY) {
108
                // Don't show special accounts since it's an interim statement, their balance will be computed manually
109 1
                continue;
110
            }
111 1
            $data = [
112 1
                'code' => $account->getCode(),
113 1
                'name' => Format::truncate($account->getName(), 55),
114
                'depth' => $depth,
115
                'balance' => $balance,
116
                'account' => $account,
117
            ];
118 1
            if ($account->getType() === AccountTypeType::ASSET || ($account->getType() === AccountTypeType::GROUP && mb_substr((string) $account->getCode(), 0, 1) === '1')) {
119 1
                $this->assets[] = $data;
120 1
            } elseif ($account->getType() === AccountTypeType::LIABILITY || ($account->getType() === AccountTypeType::GROUP && mb_substr((string) $account->getCode(), 0, 1) === '2')) {
121 1
                $this->liabilities[] = $data;
122 1
            } elseif ($account->getType() === AccountTypeType::REVENUE || ($account->getType() === AccountTypeType::GROUP && mb_substr((string) $account->getCode(), 0, 1) === '3')) {
123 1
                $this->revenues[] = $data;
124 1
            } elseif ($account->getType() === AccountTypeType::EXPENSE || ($account->getType() === AccountTypeType::GROUP && in_array(mb_substr((string) $account->getCode(), 0, 1), ['4', '5', '6'], true))) {
125 1
                $this->expenses[] = $data;
126
            }
127 1
            if ($account->getType() === AccountTypeType::GROUP && $depth <= $this->accountingConfig['report']['maxAccountDepth'] && $account->getCode() !== $this->accountingConfig['customerDepositsAccountCode']) {
128 1
                $children = $account->getChildren()->toArray();
129 1
                $this->processAccounts($children, $depth + 1);
130
            }
131
        }
132
    }
133
134
    /**
135
     * @param Account[] $items
136
     */
137 1
    protected function writeData(array $items): void
138
    {
139 1
        $this->processAccounts($items, 1);
140
141 1
        $this->sheet->setShowGridlines(false);
142
143 1
        $profitOrLoss = $this->getProfitOrLoss();
144
145 1
        if ($profitOrLoss->isNegative()) {
146
            // A loss is written at the end of the Revenues and Assets columns
147
            $data = [
148
                'code' => '',
149
                'name' => 'Résultat intermédiaire (perte)',
150
                'depth' => 1,
151
                'balance' => $profitOrLoss->absolute(),
152
                'account' => null,
153
                'format' => ['font' => ['color' => ['argb' => Color::COLOR_RED]]],
154
            ];
155
            $this->revenues[] = $data;
156
            $this->assets[] = $data;
157
        } else {
158
            // A profit is written at the end of the Expenses and Liabilities columns
159 1
            $data = [
160
                'code' => '',
161
                'name' => 'Résultat intermédiaire (bénéfice)',
162
                'depth' => 1,
163
                'balance' => $profitOrLoss,
164
                'account' => null,
165 1
                'format' => ['font' => ['color' => ['argb' => Color::COLOR_DARKGREEN]]],
166
            ];
167 1
            $this->expenses[] = $data;
168 1
            $this->liabilities[] = $data;
169
        }
170
171
        // Assets
172 1
        $this->column = $initialColumn = 1;
173 1
        $initialRow = $this->row;
174 1
        $firstLine = true;
175 1
        $this->lastDataRow = $this->row;
176 1
        foreach ($this->assets as $index => $data) {
177 1
            if ($firstLine) {
178 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountCode']);
179
            }
180 1
            $format = ['font' => ['bold' => $data['depth'] <= 2]];
181 1
            $this->write(
182 1
                str_repeat('  ', $data['depth'] - 1) . $data['code'],
183 1
                ['alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'indent' => 1]],
184
                $format
185
            );
186 1
            if ($firstLine) {
187 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountName']);
188
            }
189 1
            $this->write($data['name'], ['alignment' => ['wrapText' => true]], $format, $data['format'] ?? []);
190 1
            if ($firstLine) {
191 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
192
            }
193
            // Store the coordinate of the cell to later compute totals
194 1
            $this->assets[$index]['cell'] = $this->sheet->getCellByColumnAndRow($this->column, $this->row)->getCoordinate();
195 1
            $this->write($data['balance'], self::$balanceFormat);
196
197 1
            $firstLine = false;
198 1
            ++$this->row;
199 1
            $this->column = $initialColumn;
200
201 1
            $this->lastDataRow = max($this->lastDataRow, $this->row);
202
        }
203
204
        // Liabilities
205 1
        $this->row = $initialRow;
206 1
        $this->column = $initialColumn = $initialColumn + 4;
207 1
        $firstLine = true;
208 1
        foreach ($this->liabilities as $index => $data) {
209 1
            if ($firstLine) {
210 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
211
            }
212
            // Store the coordinate of the cell to later compute totals
213 1
            $this->liabilities[$index]['cell'] = $this->sheet->getCellByColumnAndRow($this->column, $this->row)->getCoordinate();
214 1
            $this->write($data['balance'], self::$balanceFormat);
215 1
            $format = ['font' => ['bold' => $data['depth'] <= 2]];
216 1
            if ($firstLine) {
217 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountCode']);
218
            }
219 1
            $this->write(
220 1
                str_repeat('  ', $data['depth'] - 1) . $data['code'],
221 1
                ['alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'indent' => 1]],
222
                $format
223
            );
224 1
            if ($firstLine) {
225 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountName']);
226
            }
227 1
            $this->write($data['name'], ['alignment' => ['wrapText' => true]], $format, $data['format'] ?? []);
228 1
            $firstLine = false;
229 1
            ++$this->row;
230 1
            $this->column = $initialColumn;
231
232 1
            $this->lastDataRow = max($this->lastDataRow, $this->row);
233
        }
234
235
        // Expenses
236 1
        $this->row = $initialRow;
237 1
        $this->column = $initialColumn = $initialColumn + 4;
238 1
        $firstLine = true;
239 1
        foreach ($this->expenses as $index => $data) {
240 1
            if ($firstLine) {
241 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountCode']);
242
            }
243 1
            $format = ['font' => ['bold' => $data['depth'] === 1]];
244 1
            $this->write(
245 1
                str_repeat('  ', $data['depth'] - 1) . $data['code'],
246 1
                ['alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'indent' => 1]],
247
                $format
248
            );
249 1
            if ($firstLine) {
250 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountName']);
251
            }
252 1
            $this->write($data['name'], ['alignment' => ['wrapText' => true]], $format, $data['format'] ?? []);
253 1
            if ($firstLine) {
254 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
255
            }
256
            // Store the coordinate of the cell to later compute totals
257 1
            $this->expenses[$index]['cell'] = $this->sheet->getCellByColumnAndRow($this->column, $this->row)->getCoordinate();
258 1
            $this->write($data['balance'], self::$balanceFormat);
259 1
            $firstLine = false;
260 1
            ++$this->row;
261 1
            $this->column = $initialColumn;
262
263 1
            $this->lastDataRow = max($this->lastDataRow, $this->row);
264
        }
265
266
        // Revenues
267 1
        $this->row = $initialRow;
268 1
        $this->column = $initialColumn = $initialColumn + 4;
269 1
        $firstLine = true;
270 1
        foreach ($this->revenues as $index => $data) {
271 1
            if ($firstLine) {
272 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['balance']);
273
            }
274
            // Store the coordinate of the cell to later compute totals
275 1
            $this->revenues[$index]['cell'] = $this->sheet->getCellByColumnAndRow($this->column, $this->row)->getCoordinate();
276 1
            $this->write($data['balance'], self::$balanceFormat);
277 1
            $format = ['font' => ['bold' => $data['depth'] === 1]];
278 1
            if ($firstLine) {
279 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountCode']);
280
            }
281 1
            $this->write(
282 1
                str_repeat('  ', $data['depth'] - 1) . $data['code'],
283 1
                ['alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'indent' => 1]],
284
                $format
285
            );
286 1
            if ($firstLine) {
287 1
                $this->sheet->getColumnDimensionByColumn($this->column)->setWidth(self::$columnWidth['accountName']);
288
            }
289 1
            $this->write($data['name'], ['alignment' => ['wrapText' => true]], $format, $data['format'] ?? []);
290 1
            $firstLine = false;
291 1
            ++$this->row;
292 1
            $this->column = $initialColumn;
293
294 1
            $this->lastDataRow = max($this->lastDataRow, $this->row);
295
        }
296
297 1
        $this->applyExtraFormatting();
298
    }
299
300 1
    private function getProfitOrLoss(): Money
301
    {
302
        // Sum the profit and loss root accounts
303 1
        $totalRevenues = $this->sumBalance($this->revenues);
304
305 1
        $totalExpenses = $this->sumBalance($this->expenses);
306
307 1
        return $totalRevenues->subtract($totalExpenses);
308
    }
309
310 1
    protected function getHeaders(): array
311
    {
312 1
        $headers = [];
313
314 1
        $headers[] = ['label' => 'Actifs', 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => 3];
315 1
        $headers[] = ['label' => '', 'width' => 3, 'formats' => []]; // margin
316 1
        $headers[] = ['label' => 'Passifs', 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => 3];
317
318 1
        $headers[] = ['label' => '', 'width' => 5, 'formats' => []]; // margin
319
320 1
        $headers[] = ['label' => 'Charges', 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => 3];
321 1
        $headers[] = ['label' => '', 'width' => 3, 'formats' => []]; // margin
322 1
        $headers[] = ['label' => 'Profits', 'formats' => [self::$headerFormat, self::$centerFormat], 'colspan' => 3];
323
324 1
        return $headers;
325
    }
326
327 1
    protected function writeFooter(): void
328
    {
329 1
        $initialColumn = $this->column;
330
331
        // BALANCE SHEET
332
333
        // Assets
334
        // Account.code
335 1
        $this->write('');
336
        // Account.name
337 1
        $this->write('');
338
        // Account.balance
339 1
        $this->writeSum($this->assets);
340
341
        // Margin
342 1
        $this->write('');
343
344
        // Liabilities
345
        // Account.balance
346 1
        $this->writeSum($this->liabilities);
347
348
        // Account.code
349 1
        $this->write('');
350
        // Account.name
351 1
        $this->write('');
352
353
        // Margin
354 1
        $this->write('');
355
356
        // INCOME STATEMENT
357
358
        // Expenses
359
        // Account.code
360 1
        $this->write('');
361
        // Account.name
362 1
        $this->write('');
363
        // Account.balance
364 1
        $this->writeSum($this->expenses);
365
366
        // Margin
367 1
        $this->write('');
368
369
        // Revenues
370
        // Account.balance
371 1
        $this->writeSum($this->revenues);
372
        // Account.code
373 1
        $this->write('');
374
        // Account.name
375 1
        $this->write('');
376
377
        // Apply style
378 1
        $range = Coordinate::stringFromColumnIndex($initialColumn) . $this->row . ':' . Coordinate::stringFromColumnIndex($this->column - 1) . $this->row;
379 1
        $this->sheet->getStyle($range)->applyFromArray(self::$totalFormat);
380
    }
381
382 1
    private function applyExtraFormatting(): void
383
    {
384
        // Format balance numbers
385 1
        foreach ([3, 5, 11, 13] as $colIndex) {
386 1
            $range = Coordinate::stringFromColumnIndex($colIndex) . 4 . ':' . Coordinate::stringFromColumnIndex($colIndex) . $this->lastDataRow;
387 1
            $this->sheet->getStyle($range)->applyFromArray(self::$balanceFormat);
388
        }
389
390
        // Increase row height since account names can wrap on multiple lines
391 1
        for ($r = 4; $r <= $this->lastDataRow; ++$r) {
392 1
            $this->sheet->getRowDimension($r)->setRowHeight(30);
393
        }
394
    }
395
396 1
    private function writeSum(array $data): void
397
    {
398 1
        $cellsToSum = $this->cellsToSum($data);
399 1
        $sum = $cellsToSum ? '=SUM(' . implode(',', $cellsToSum) . ')' : '';
400 1
        $this->write($sum, self::$balanceFormat, self::$totalFormat);
401
    }
402
403 1
    private function cellsToSum(array $data): array
404
    {
405 1
        $cells = array_reduce($data, function (array $carry, $data) {
406 1
            if (isset($data['cell']) && ($data['depth'] === 1 || (int) mb_substr((string) $data['code'], 0, 1) > 6)) {
407 1
                $carry[] = $data['cell'];
408
            }
409
410 1
            return $carry;
411 1
        }, []);
412
413 1
        return $cells;
414
    }
415
416 1
    private function sumBalance(array $data): Money
417
    {
418 1
        $sum = array_reduce($data, function (Money $carry, $data) {
419 1
            if ($data['depth'] === 1 || (int) mb_substr((string) $data['code'], 0, 1) > 6) {
420 1
                return $carry->add($data['balance']);
421
            }
422
423 1
            return $carry;
424 1
        }, Money::CHF(0));
425
426 1
        return $sum;
427
    }
428
}
429