Passed
Push — master ( 367538...e6ff4f )
by Adrien
06:37
created

AccountingReport   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Test Coverage

Coverage 96.48%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 58
eloc 233
c 2
b 1
f 0
dl 0
loc 412
ccs 219
cts 227
cp 0.9648
rs 4.5599

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getTitleForFilename() 0 3 1
A __construct() 0 10 1
A setDate() 0 3 1
A getProfitOrLoss() 0 8 1
D processAccounts() 0 30 21
A getHeaders() 0 15 1
A writeTitle() 0 29 1
F writeData() 0 161 18
A writeSum() 0 5 2
A sumBalance() 0 11 3
A applyExtraFormatting() 0 11 3
A writeFooter() 0 53 1
A cellsToSum() 0 11 4

How to fix   Complexity   

Complex Class

Complex classes like AccountingReport often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AccountingReport, and based on these observations, apply Extract Interface, too.

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