Completed
Push — master ( b43734...838a39 )
by Carlos
06:00 queued 18s
created

GridView::initLineData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 5
rs 10
cc 3
nc 3
nop 1
1
<?php
2
/**
3
 * This file is part of FacturaScripts
4
 * Copyright (C) 2017-2019 Carlos Garcia Gomez <[email protected]>
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18
 */
19
namespace FacturaScripts\Core\Lib\ExtendedController;
20
21
use Exception;
22
use FacturaScripts\Core\Base\DataBase;
23
use FacturaScripts\Core\Base\DataBase\DataBaseWhere;
24
use FacturaScripts\Core\Model\Base\ModelClass;
25
use FacturaScripts\Dinamic\Lib\AssetManager;
26
use FacturaScripts\Dinamic\Lib\ExportManager;
27
use FacturaScripts\Dinamic\Lib\Widget\ColumnItem;
28
use FacturaScripts\Dinamic\Lib\Widget\WidgetAutocomplete;
29
use FacturaScripts\Dinamic\Lib\Widget\WidgetSelect;
30
31
/**
32
 * Description of GridView
33
 *
34
 * @author Artex Trading sa     <[email protected]>
35
 * @author Carlos García Gómez  <[email protected]>
36
 */
37
class GridView extends EditView
38
{
39
40
    const GRIDVIEW_TEMPLATE = 'Master/GridView.html.twig';
41
42
    /**
43
     *
44
     * @var ModelClass
45
     */
46
    public $detailModel;
47
48
    /**
49
     * Detail view
50
     *
51
     * @var BaseView
52
     */
53
    public $detailView;
54
55
    /**
56
     * Template for edit master data
57
     *
58
     * @var string
59
     */
60
    public $editTemplate = self::EDIT_TEMPLATE;
61
62
    /**
63
     * Grid data configuration and data
64
     *
65
     * @var array
66
     */
67
    private $gridData;
68
69
    /**
70
     * GridView constructor and initialization.
71
     * Master/Detail params:
72
     *   ['name' = 'viewName', 'model' => 'modelName']
73
     *
74
     * @param array   $master
75
     * @param array   $detail
76
     * @param string  $title
77
     * @param string  $icon
78
     */
79
    public function __construct($master, $detail, $title, $icon)
80
    {
81
        parent::__construct($master['name'], $title, $master['model'], $icon);
82
83
        // Create detail view
84
        $this->detailView = new EditView($detail['name'], $title, $detail['model'], $icon);
85
        $this->detailModel = $this->detailView->model;
86
87
        // custom template
88
        $this->template = self::GRIDVIEW_TEMPLATE;
89
    }
90
91
    /**
92
     *
93
     * @param ExportManager $exportManager
94
     *
95
     * @return bool
96
     */
97
    public function export(&$exportManager): bool
98
    {
99
        if (!parent::export($exportManager)) {
100
            return false;
101
        }
102
103
        $headers = $this->gridData['headers'];
104
        $formattedRows = [];
105
        foreach ($this->gridData['rows'] as $row) {
106
            $formattedRow = [];
107
            foreach ($this->gridData['columns'] as $column) {
108
                $formattedRow[] = isset($row[$column['data']]) ? $row[$column['data']] : '';
109
            }
110
            $formattedRows[] = array_combine($headers, $formattedRow);
111
        }
112
        return $exportManager->addTablePage($headers, $formattedRows);
113
    }
114
115
    /**
116
     * Returns detail column configuration
117
     *
118
     * @param string $key
119
     *
120
     * @return ColumnItem[]
121
     */
122
    public function getDetailColumns($key = '')
123
    {
124
        if (!array_key_exists($key, $this->detailView->columns)) {
125
            if ($key == 'master') {
126
                return [];
127
            }
128
            $key = array_keys($this->detailView->columns)[0];
129
        }
130
131
        return $this->detailView->columns[$key]->columns;
132
    }
133
134
    /**
135
     * Returns JSON into string with Grid view data
136
     *
137
     * @return string
138
     */
139
    public function getGridData(): string
140
    {
141
        return json_encode($this->gridData);
142
    }
143
144
    /**
145
     * Load the data in the model property, according to the code specified.
146
     *
147
     * @param string          $code
148
     * @param DataBaseWhere[] $where
149
     * @param array           $order
150
     * @param int             $offset
151
     * @param int             $limit
152
     */
153
    public function loadData($code = '', $where = [], $order = [], $offset = 0, $limit = \FS_ITEM_LIMIT)
154
    {
155
        parent::loadData($code, $where, $order, $offset, $limit);
156
157
        if ($this->count == 0) {
158
            $this->template = self::EDIT_TEMPLATE;
159
            return;
160
        }
161
162
        if ($this->newCode !== null) {
163
            $code = $this->newCode;
164
        }
165
166
        $where[] = new DataBaseWhere($this->model->primaryColumn(), $code);
167
        $order[$this->detailModel->primaryColumn()] = 'ASC';
168
        $this->loadGridData($where, $order);
169
    }
170
171
    /**
172
     * Load detail data and set grid configuration
173
     *
174
     * @param DataBaseWhere[] $where
175
     * @param array           $order
176
     */
177
    public function loadGridData($where = [], $order = [])
178
    {
179
        // load columns configuration
180
        $this->gridData = $this->getGridColumns();
181
182
        // load detail model data
183
        $this->gridData['rows'] = [];
184
        $this->detailView->count = $this->detailView->model->count($where);
185
        if ($this->detailView->count == 0) {
186
            return;
187
        }
188
189
        foreach ($this->detailModel->all($where, $order, 0, 0) as $line) {
190
            /// do not change to (array) $line
191
            $row = [];
192
            foreach (array_keys($line->getModelFields()) as $field) {
193
                $row[$field] = $line->{$field};
194
            }
195
196
            $this->gridData['rows'][] = $row;
197
        }
198
    }
199
200
    /**
201
     *
202
     * @param array $lines
203
     * @return array
204
     */
205
    public function processFormLines(&$lines): array
206
    {
207
        $result = [];
208
        foreach ($lines as $data) {
209
            if (!is_array($data)) {
210
                $result[] = [];
211
                continue;
212
            }
213
214
            if (!isset($data[$this->detailModel->primaryColumn()])) {
215
                $this->initLineData($data);
216
            }
217
            $result[] = $data;
218
        }
219
220
        return $result;
221
    }
222
223
    /**
224
     *
225
     * @param array $data
226
     *
227
     * @return array
228
     */
229
    public function saveData($data): array
230
    {
231
        $result = [
232
            'error' => false,
233
            'message' => '',
234
            'url' => ''
235
        ];
236
237
        try {
238
            // load master document data and test it's ok
239
            if (!$this->loadDocumentDataFromArray('code', $data['document'])) {
240
                throw new Exception($this->toolBox()->i18n()->trans('parent-document-test-error'));
241
            }
242
243
            // load detail document data (old)
244
            $documentFieldKey = $this->model->primaryColumn();
245
            $documentFieldValue = $this->model->primaryColumnValue();
246
            $linesOld = $this->detailModel->all([new DataBaseWhere($documentFieldKey, $documentFieldValue)]);
247
248
            // start transaction
249
            $dataBase = new DataBase();
250
            $dataBase->beginTransaction();
251
252
            // delete old lines not used
253
            if (!$this->deleteLinesOld($linesOld, $data['lines'])) {
254
                throw new Exception($this->toolBox()->i18n()->trans('error-deleting-lines'));
255
            }
256
257
            // Proccess detail document data (new)
258
            $this->model->initTotals(); // Master Model must implement GridModelInterface
0 ignored issues
show
Bug introduced by
The method initTotals() does not exist on FacturaScripts\Core\Model\Base\ModelClass. It seems like you code against a sub-type of said class. However, the method does not exist in FacturaScripts\Core\Model\Base\Contact or FacturaScripts\Core\Model\Base\Address or FacturaScripts\Dinamic\Model\Base\ModelClass or FacturaScripts\Core\Model\Base\ModelOnChangeClass or FacturaScripts\Core\Model\Base\BankAccount or FacturaScripts\Core\Model\Base\Payment or FacturaScripts\Dinamic\Model\Base\Contact or FacturaScripts\Core\Model\Base\ComercialContact or FacturaScripts\Dinamic\Model\Base\ComercialContact or FacturaScripts\Dinamic\Model\Base\Address or FacturaScripts\Core\Model\Base\Receipt or FacturaScripts\Core\Mode...se\BusinessDocumentLine or FacturaScripts\Dinamic\M...Base\ModelOnChangeClass or FacturaScripts\Core\Model\Base\BusinessDocument or FacturaScripts\Dinamic\Model\Base\Receipt or FacturaScripts\Dinamic\M...se\BusinessDocumentLine or FacturaScripts\Core\Model\Base\SalesDocumentLine or FacturaScripts\Dinamic\M...\Base\SalesDocumentLine or FacturaScripts\Dinamic\Model\Base\BusinessDocument or FacturaScripts\Core\Model\Base\TransformerDocument or FacturaScripts\Core\Model\Base\PurchaseDocument or FacturaScripts\Dinamic\M...ase\TransformerDocument or FacturaScripts\Core\Model\Base\SalesDocument or FacturaScripts\Dinamic\Model\Base\PurchaseDocument or FacturaScripts\Dinamic\Model\Base\SalesDocument or FacturaScripts\Dinamic\Model\Base\BankAccount or FacturaScripts\Dinamic\Model\Base\Payment. Are you sure you never get one of those? ( Ignorable by Annotation )

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

258
            $this->model->/** @scrutinizer ignore-call */ 
259
                          initTotals(); // Master Model must implement GridModelInterface
Loading history...
259
            foreach ($data['lines'] as $newLine) {
260
                if (!$this->saveLines($documentFieldKey, $documentFieldValue, $newLine)) {
261
                    throw new Exception($this->toolBox()->i18n()->trans('error-saving-lines'));
262
                }
263
                $this->model->accumulateAmounts($newLine); // Master Model must implement GridModelInterface
0 ignored issues
show
Bug introduced by
The method accumulateAmounts() does not exist on FacturaScripts\Core\Model\Base\ModelClass. It seems like you code against a sub-type of said class. However, the method does not exist in FacturaScripts\Core\Model\Base\Contact or FacturaScripts\Core\Model\Base\Address or FacturaScripts\Dinamic\Model\Base\ModelClass or FacturaScripts\Core\Model\Base\ModelOnChangeClass or FacturaScripts\Core\Model\Base\BankAccount or FacturaScripts\Core\Model\Base\Payment or FacturaScripts\Dinamic\Model\Base\Contact or FacturaScripts\Core\Model\Base\ComercialContact or FacturaScripts\Dinamic\Model\Base\ComercialContact or FacturaScripts\Dinamic\Model\Base\Address or FacturaScripts\Core\Model\Base\Receipt or FacturaScripts\Core\Mode...se\BusinessDocumentLine or FacturaScripts\Dinamic\M...Base\ModelOnChangeClass or FacturaScripts\Core\Model\Base\BusinessDocument or FacturaScripts\Dinamic\Model\Base\Receipt or FacturaScripts\Dinamic\M...se\BusinessDocumentLine or FacturaScripts\Core\Model\Base\SalesDocumentLine or FacturaScripts\Dinamic\M...\Base\SalesDocumentLine or FacturaScripts\Dinamic\Model\Base\BusinessDocument or FacturaScripts\Core\Model\Base\TransformerDocument or FacturaScripts\Core\Model\Base\PurchaseDocument or FacturaScripts\Dinamic\M...ase\TransformerDocument or FacturaScripts\Core\Model\Base\SalesDocument or FacturaScripts\Dinamic\Model\Base\PurchaseDocument or FacturaScripts\Dinamic\Model\Base\SalesDocument or FacturaScripts\Dinamic\Model\Base\BankAccount or FacturaScripts\Dinamic\Model\Base\Payment. Are you sure you never get one of those? ( Ignorable by Annotation )

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

263
                $this->model->/** @scrutinizer ignore-call */ 
264
                              accumulateAmounts($newLine); // Master Model must implement GridModelInterface
Loading history...
264
            }
265
266
            // save master document
267
            if (!$this->model->save()) {
268
                throw new Exception($this->toolBox()->i18n()->trans('parent-document-save-error'));
269
            }
270
271
            // confirm save data into database
272
            $dataBase->commit();
273
274
            // URL for refresh data
275
            $result['url'] = $this->model->url('edit') . '&action=save-ok';
276
        } catch (Exception $err) {
277
            $result['error'] = true;
278
            $result['message'] = implode("\n", array_merge([$err->getMessage()], $this->getErrors()));
279
        } finally {
280
            if ($dataBase->inTransaction()) {
281
                $dataBase->rollback();
282
            }
283
            return $result;
284
        }
285
    }
286
287
    protected function assets()
288
    {
289
        AssetManager::add('css', \FS_ROUTE . '/node_modules/handsontable/dist/handsontable.full.min.css');
290
        AssetManager::add('js', \FS_ROUTE . '/node_modules/handsontable/dist/handsontable.full.min.js');
291
        AssetManager::add('js', \FS_ROUTE . '/Dinamic/Assets/JS/GridView.js');
292
    }
293
294
    /**
295
     * Removes from the database the non-existent detail
296
     *
297
     * @param ModelClass[] $linesOld
298
     * @param array        $linesNew
299
     *
300
     * @return bool
301
     */
302
    private function deleteLinesOld(&$linesOld, &$linesNew): bool
303
    {
304
        if (empty($linesOld)) {
305
            return true;
306
        }
307
308
        $fieldPK = $this->detailModel->primaryColumn();
309
        foreach ($linesOld as $lineOld) {
310
            $found = false;
311
            foreach ($linesNew as $lineNew) {
312
                if ($lineOld->{$fieldPK} == $lineNew[$fieldPK]) {
313
                    $found = true;
314
                    break;
315
                }
316
            }
317
318
            if (!$found && !$lineOld->delete()) {
319
                return false;
320
            }
321
        }
322
323
        return true;
324
    }
325
326
    /**
327
     * Configure autocomplete column with data to Grid component
328
     *
329
     * @param WidgetAutocomplete $widget
330
     *
331
     * @return array
332
     */
333
    private function getAutocompleteSource($widget): array
334
    {
335
        $url = $this->model->url('edit');
336
        $datasource = $widget->getDataSource();
337
338
        return [
339
            'url' => $url,
340
            'source' => $datasource['source'],
341
            'field' => $datasource['fieldcode'],
342
            'title' => $datasource['fieldtitle']
343
        ];
344
    }
345
346
    /**
347
     *
348
     * @return array
349
     */
350
    private function getErrors(): array
351
    {
352
        $errors = [];
353
        foreach ($this->toolBox()->log()->readAll() as $log) {
354
            $errors[] = $log['message'];
355
        }
356
357
        return $errors;
358
    }
359
360
    /**
361
     * Return grid columns configuration
362
     * from pages_options of columns
363
     *
364
     * @return array
365
     */
366
    private function getGridColumns(): array
367
    {
368
        $data = [
369
            'headers' => [],
370
            'columns' => [],
371
            'hidden' => [],
372
            'colwidths' => []
373
        ];
374
375
        foreach ($this->getDetailColumns('detail') as $col) {
376
            $item = $this->getItemForColumn($col);
377
            if ($col->hidden()) {
378
                $data['hidden'][] = $item;
379
            } else {
380
                $data['columns'][] = $item;
381
                $data['colwidths'][] = $col->htmlWidth();
382
                $data['headers'][] = $this->toolBox()->i18n()->trans($col->title);
383
            }
384
        }
385
386
        return $data;
387
    }
388
389
    /**
390
     * Return grid column configuration
391
     *
392
     * @param ColumnItem $column
393
     *
394
     * @return array
395
     */
396
    private function getItemForColumn($column): array
397
    {
398
        $item = [
399
            'data' => $column->widget->fieldname,
400
            'type' => $column->widget->getType()
401
        ];
402
        switch ($item['type']) {
403
            case 'autocomplete':
404
                $item['visibleRows'] = 5;
405
                $item['allowInvalid'] = true;
406
                $item['trimDropdown'] = false;
407
                $item['strict'] = $column->widget->strict;
408
                $item['data-source'] = $this->getAutocompleteSource($column->widget);
409
                break;
410
411
            case 'number':
412
            case 'money':
413
                $item['type'] = 'numeric';
414
                $item['numericFormat'] = $this->toolBox()->coins()->gridMoneyFormat();
415
                break;
416
417
            case 'select':
418
                $item['editor'] = 'select';
419
                $item['selectOptions'] = $this->getSelectSource($column->widget);
420
                break;
421
        }
422
423
        return $item;
424
    }
425
426
    /**
427
     * Return array of values to select
428
     *
429
     * @param WidgetSelect $widget
430
     *
431
     * @return array
432
     */
433
    private function getSelectSource($widget): array
434
    {
435
        $result = [];
436
        if (!$widget->required) {
437
            $result[] = '';
438
        }
439
440
        foreach ($widget->values as $value) {
441
            $result[] = $value['title'];
442
        }
443
        return $result;
444
    }
445
446
    /**
447
     * Set initial values for columns into a new line
448
     *
449
     * @param array|string $data
450
     */
451
    private function initLineData(&$data)
452
    {
453
        foreach ($this->getDetailColumns('detail') as $col) {
454
            if (!isset($data[$col->widget->fieldname])) {
455
                $data[$col->widget->fieldname] = null; // TODO: maybe the widget can have a default value method instead of null
456
            }
457
        }
458
    }
459
460
    /**
461
     * Load data of master document and set data from array
462
     *
463
     * @param string $field
464
     * @param array  $data
465
     *
466
     * @return bool
467
     */
468
    private function loadDocumentDataFromArray($field, &$data): bool
469
    {
470
        if ($this->model->loadFromCode($data[$field])) {    // old data
471
            $this->model->loadFromData($data, ['action', 'activetab', 'code']);  // new data (the web form may be not have all the fields)
472
            return $this->model->test();
473
        }
474
        return false;
475
    }
476
477
    /**
478
     *
479
     * @param string $documentFieldKey
480
     * @param int    $documentFieldValue
481
     * @param array  $data
482
     *
483
     * @return bool
484
     */
485
    private function saveLines($documentFieldKey, $documentFieldValue, &$data)
486
    {
487
        // load old data, if exits
488
        $field = $this->detailModel->primaryColumn();
489
        $this->detailModel->loadFromCode($data[$field]);
490
491
        // set new data from user form
492
        $this->detailModel->loadFromData($data);
493
494
        // if new record, save field relation with master document
495
        if (empty($this->detailModel->primaryColumnValue())) {
496
            $this->detailModel->{$documentFieldKey} = $documentFieldValue;
497
        }
498
499
        return $this->detailModel->save();
500
    }
501
}
502