Failed Conditions
Push — master ( d2dc84...8e498d )
by Adrien
07:27
created

AccountingReport   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Test Coverage

Coverage 96.41%

Importance

Changes 0
Metric Value
wmc 53
eloc 231
dl 0
loc 406
ccs 215
cts 223
cp 0.9641
rs 6.96
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getProfitOrLoss() 0 8 1
D processAccounts() 0 30 20
A sumBalance() 0 11 2
A getHeaders() 0 15 1
A getTitleForFilename() 0 3 1
A applyExtraFormatting() 0 11 3
A __construct() 0 10 1
A writeTitle() 0 26 1
A writeFooter() 0 57 1
F writeData() 0 161 18
A setDate() 0 3 1
A cellsToSum() 0 11 3

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, self::$centerFormat
79
        );
80 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(35);
81 1
        ++$this->row;
82
83 1
        $this->column = 1;
84 1
        $this->sheet->mergeCellsByColumnAndRow($this->column, $this->row, $this->column + 6, $this->row);
85 1
        $this->write(
86 1
            sprintf('Bilan'),
87 1
            self::$titleFormat, self::$centerFormat
88
        );
89 1
        $this->column = 9;
90 1
        $this->sheet->mergeCellsByColumnAndRow($this->column, $this->row, $this->column + 6, $this->row);
91 1
        $this->write(
92 1
            sprintf('Résultat'),
93 1
            self::$titleFormat, self::$centerFormat
94
        );
95
96 1
        $this->sheet->getRowDimension($this->row)->setRowHeight(35);
97 1
        ++$this->row;
98 1
    }
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
            $data = [
112 1
                'code' => $account->getCode(),
113 1
                'name' => Format::truncate($account->getName(), 55),
114 1
                'depth' => $depth,
115 1
                'balance' => $balance,
116 1
                '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']) {
128 1
                $children = $account->getChildren()->toArray();
129 1
                $this->processAccounts($children, $depth + 1);
130
            }
131
        }
132 1
    }
133
134
    /**
135
     * @param Account[] $accounts
136
     */
137 1
    protected function writeData(array $accounts): void
138
    {
139 1
        $this->processAccounts($accounts, 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
            $data = [
160 1
                'code' => '',
161 1
                'name' => 'Résultat intermédiaire (bénéfice)',
162 1
                'depth' => 1,
163 1
                '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 1
    }
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
        $cellsToSum = $this->cellsToSum($this->assets);
340 1
        $this->write('=SUM(' . implode(',', $cellsToSum) . ')', self::$balanceFormat, self::$totalFormat);
341
342
        // Margin
343 1
        $this->write('');
344
345
        /** Liabilities */
346
        // Account.balance
347 1
        $cellsToSum = $this->cellsToSum($this->liabilities);
348 1
        $this->write('=SUM(' . implode(',', $cellsToSum) . ')', self::$balanceFormat, self::$totalFormat);
349
350
        // Account.code
351 1
        $this->write('');
352
        // Account.name
353 1
        $this->write('');
354
355
        // Margin
356 1
        $this->write('');
357
358
        /** INCOME STATEMENT */
359
360
        /** Expenses */
361
        // Account.code
362 1
        $this->write('');
363
        // Account.name
364 1
        $this->write('');
365
        // Account.balance
366 1
        $cellsToSum = $this->cellsToSum($this->expenses);
367 1
        $this->write('=SUM(' . implode(',', $cellsToSum) . ')', self::$balanceFormat, self::$totalFormat);
368
369
        // Margin
370 1
        $this->write('');
371
372
        /** Revenues ** */
373
        // Account.balance
374 1
        $cellsToSum = $this->cellsToSum($this->revenues);
375 1
        $this->write('=SUM(' . implode(',', $cellsToSum) . ')', self::$balanceFormat, self::$totalFormat);
376
        // Account.code
377 1
        $this->write('');
378
        // Account.name
379 1
        $this->write('');
380
381
        // Apply style
382 1
        $range = Coordinate::stringFromColumnIndex($initialColumn) . $this->row . ':' . Coordinate::stringFromColumnIndex($this->column - 1) . $this->row;
383 1
        $this->sheet->getStyle($range)->applyFromArray(self::$totalFormat);
384 1
    }
385
386 1
    private function applyExtraFormatting(): void
387
    {
388
        // Format balance numbers
389 1
        foreach ([3, 5, 11, 13] as $colIndex) {
390 1
            $range = Coordinate::stringFromColumnIndex($colIndex) . 4 . ':' . Coordinate::stringFromColumnIndex($colIndex) . $this->lastDataRow;
391 1
            $this->sheet->getStyle($range)->applyFromArray(self::$balanceFormat);
392
        }
393
394
        // Increase row height since account names can wrap on multiple lines
395 1
        for ($r = 4; $r <= $this->lastDataRow; ++$r) {
396 1
            $this->sheet->getRowDimension($r)->setRowHeight(30);
397
        }
398 1
    }
399
400 1
    protected function cellsToSum(array $data): array
401
    {
402 1
        $cells = array_reduce($data, function (array $carry, $data) {
403 1
            if ($data['depth'] === 1 && isset($data['cell'])) {
404 1
                $carry[] = $data['cell'];
405
            }
406
407 1
            return $carry;
408 1
        }, []);
409
410 1
        return $cells;
411
    }
412
413 1
    private function sumBalance(array $data): Money
414
    {
415 1
        $sum = array_reduce($data, function (Money $carry, $data) {
416 1
            if ($data['depth'] === 1) {
417 1
                return $carry->add($data['balance']);
418
            }
419
420 1
            return $carry;
421 1
        }, Money::CHF(0));
422
423 1
        return $sum;
424
    }
425
}
426