Passed
Pull Request — dev (#6)
by Rafael
79:24 queued 24:08
created

Lettering::bookkeepingLetteringAll()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 16
nc 4
nop 2
dl 0
loc 22
rs 9.7333
c 0
b 0
f 0
1
<?php
2
3
/* Copyright (C) 2004-2005  Rodolphe Quiedeville        <[email protected]>
4
 * Copyright (C) 2013       Olivier Geffroy             <[email protected]>
5
 * Copyright (C) 2013-2024  Alexandre Spangaro          <[email protected]>
6
 * Copyright (C) 2018       Frédéric France             <[email protected]>
7
 * Copyright (C) 2024       Rafael San José             <[email protected]>
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 3 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 */
22
23
namespace Dolibarr\Code\Accountancy\Classes;
24
25
/**
26
 * \file        htdocs/accountancy/class/lettering.class.php
27
 * \ingroup     Accountancy (Double entries)
28
 * \brief       File of class for lettering
29
 */
30
31
include_once DOL_DOCUMENT_ROOT . "/accountancy/class/bookkeeping.class.php";
32
include_once DOL_DOCUMENT_ROOT . "/societe/class/societe.class.php";
33
include_once DOL_DOCUMENT_ROOT . "/core/lib/date.lib.php";
34
35
/**
36
 * Class Lettering
37
 */
38
class Lettering extends BookKeeping
39
{
40
    public static $doc_type_infos = array(
41
        'customer_invoice' => array(
42
            'payment_table' => 'paiement',
43
            'payment_table_fk_bank' => 'fk_bank',
44
            'doc_payment_table' => 'paiement_facture',
45
            'doc_payment_table_fk_payment' => 'fk_paiement',
46
            'doc_payment_table_fk_doc' => 'fk_facture',
47
            'linked_info' => array(
48
                array(
49
                    'table' => 'paiement_facture',
50
                    'fk_doc' => 'fk_facture',
51
                    'fk_link' => 'fk_paiement',
52
                    'prefix' => 'p',
53
                ),
54
                array(
55
                    'table' => 'societe_remise_except',
56
                    'fk_doc' => 'fk_facture_source',
57
                    'fk_link' => 'fk_facture',
58
                    'fk_line_link' => 'fk_facture_line',
59
                    'table_link_line' => 'facturedet',
60
                    'fk_table_link_line' => 'rowid',
61
                    'fk_table_link_line_parent' => 'fk_facture',
62
                    'prefix' => 'a',
63
                    'is_fk_link_is_also_fk_doc' => true,
64
                ),
65
            ),
66
        ),
67
        'supplier_invoice' => array(
68
            'payment_table' => 'paiementfourn',
69
            'payment_table_fk_bank' => 'fk_bank',
70
            'doc_payment_table' => 'paiementfourn_facturefourn',
71
            'doc_payment_table_fk_payment' => 'fk_paiementfourn',
72
            'doc_payment_table_fk_doc' => 'fk_facturefourn',
73
            'linked_info' => array(
74
                array(
75
                    'table' => 'paiementfourn_facturefourn',
76
                    'fk_doc' => 'fk_facturefourn',
77
                    'fk_link' => 'fk_paiementfourn',
78
                    'prefix' => 'p',
79
                ),
80
                array(
81
                    'table' => 'societe_remise_except',
82
                    'fk_doc' => 'fk_invoice_supplier_source',
83
                    'fk_link' => 'fk_invoice_supplier',
84
                    'fk_line_link' => 'fk_invoice_supplier_line',
85
                    'table_link_line' => 'facture_fourn_det',
86
                    'fk_table_link_line' => 'rowid',
87
                    'fk_table_link_line_parent' => 'fk_facture_fourn',
88
                    'prefix' => 'a',
89
                    'is_fk_link_is_also_fk_doc' => true,
90
                ),
91
            ),
92
        ),
93
    );
94
95
    /**
96
     * letteringThirdparty
97
     *
98
     * @param int $socid Thirdparty id
99
     * @return int 1 OK, <0 error
100
     */
101
    public function letteringThirdparty($socid)
102
    {
103
        global $conf;
104
105
        $error = 0;
106
107
        $object = new Societe($this->db);
108
        $object->id = $socid;
109
        $object->fetch($socid);
110
111
112
        if ($object->code_compta_client == '411CUSTCODE') {
113
            $object->code_compta_client = '';
114
        }
115
116
        if ($object->code_compta_fournisseur == '401SUPPCODE') {
117
            $object->code_compta_fournisseur = '';
118
        }
119
120
        /**
121
         * Support for complex lettering with debit, credit transfer
122
         */
123
        $sql = "SELECT DISTINCT bk.rowid, bk.doc_date, bk.doc_type, bk.doc_ref, bk.subledger_account, ";
124
        $sql .= " bk.numero_compte , bk.label_compte, bk.debit , bk.credit, bk.montant ";
125
        $sql .= " , bk.sens , bk.code_journal , bk.piece_num, bk.date_lettering, bu.url_id , bu.type ";
126
        $sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping as bk";
127
        $sql .= " LEFT JOIN  " . MAIN_DB_PREFIX . "bank_url as bu ON(bk.fk_doc = bu.fk_bank AND bu.type IN ('payment', 'payment_supplier') ) ";
128
        $sql .= " WHERE ( ";
129
        if ($object->code_compta_client != "") {
130
            $sql .= " bk.subledger_account = '" . $this->db->escape($object->code_compta_client) . "'  ";
131
        }
132
        if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
133
            $sql .= " OR ";
134
        }
135
        if ($object->code_compta_fournisseur != "") {
136
            $sql .= " bk.subledger_account = '" . $this->db->escape($object->code_compta_fournisseur) . "' ";
137
        }
138
139
        $sql .= " ) AND (bk.date_lettering ='' OR bk.date_lettering IS NULL) ";
140
        $sql .= "  AND (bk.lettering_code != '' OR bk.lettering_code IS NULL) ";
141
        $sql .= ' AND bk.date_validated IS NULL ';
142
        $sql .= $this->db->order('bk.doc_date', 'DESC');
143
144
        // echo $sql;
145
        //
146
        $resql = $this->db->query($sql);
147
        if ($resql) {
148
            $num = $this->db->num_rows($resql);
149
150
            while ($obj = $this->db->fetch_object($resql)) {
151
                $ids = array();
152
                $ids_fact = array();
153
154
                if ($obj->type == 'payment_supplier') {
155
                    $sql = 'SELECT DISTINCT bk.rowid, facf.ref, facf.ref_supplier, payf.fk_bank, facf.rowid as fact_id';
156
                    $sql .= " FROM " . MAIN_DB_PREFIX . "facture_fourn facf ";
157
                    $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "paiementfourn_facturefourn as payfacf ON  payfacf.fk_facturefourn=facf.rowid";
158
                    $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "paiementfourn as payf ON  payfacf.fk_paiementfourn=payf.rowid";
159
                    $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "accounting_bookkeeping as bk ON (bk.fk_doc = payf.fk_bank AND bk.code_journal='" . $this->db->escape($obj->code_journal) . "')";
160
                    $sql .= " WHERE payfacf.fk_paiementfourn = '" . $this->db->escape($obj->url_id) . "' ";
161
                    $sql .= " AND facf.entity = " . $conf->entity;
162
                    $sql .= " AND code_journal IN (SELECT code FROM " . MAIN_DB_PREFIX . "accounting_journal WHERE nature=4 AND entity=" . $conf->entity . ") ";
163
                    $sql .= " AND ( ";
164
                    if ($object->code_compta_client != "") {
165
                        $sql .= "  bk.subledger_account = '" . $this->db->escape($object->code_compta_client) . "'  ";
166
                    }
167
                    if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
168
                        $sql .= "  OR  ";
169
                    }
170
                    if ($object->code_compta_fournisseur != "") {
171
                        $sql .= "   bk.subledger_account = '" . $this->db->escape($object->code_compta_fournisseur) . "' ";
172
                    }
173
                    $sql .= " )  ";
174
175
                    $resql2 = $this->db->query($sql);
176
                    if ($resql2) {
177
                        while ($obj2 = $this->db->fetch_object($resql2)) {
178
                            $ids[$obj2->rowid] = $obj2->rowid;
179
                            $ids_fact[] = $obj2->fact_id;
180
                        }
181
                        $this->db->free($resql2);
182
                    } else {
183
                        $this->errors[] = $this->db->lasterror;
184
                        return -1;
185
                    }
186
                    if (count($ids_fact)) {
187
                        $sql = 'SELECT bk.rowid, facf.ref, facf.ref_supplier ';
188
                        $sql .= " FROM " . MAIN_DB_PREFIX . "facture_fourn facf ";
189
                        $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "accounting_bookkeeping as bk ON(  bk.fk_doc = facf.rowid AND facf.rowid IN (" . $this->db->sanitize(implode(',', $ids_fact)) . "))";
190
                        $sql .= " WHERE bk.code_journal IN (SELECT code FROM " . MAIN_DB_PREFIX . "accounting_journal WHERE nature=3 AND entity=" . $conf->entity . ") ";
191
                        $sql .= " AND facf.entity = " . $conf->entity;
192
                        $sql .= " AND ( ";
193
                        if ($object->code_compta_client != "") {
194
                            $sql .= " bk.subledger_account = '" . $this->db->escape($object->code_compta_client) . "'  ";
195
                        }
196
                        if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
197
                            $sql .= " OR ";
198
                        }
199
                        if ($object->code_compta_fournisseur != "") {
200
                            $sql .= " bk.subledger_account = '" . $this->db->escape($object->code_compta_fournisseur) . "' ";
201
                        }
202
                        $sql .= ") ";
203
204
                        $resql2 = $this->db->query($sql);
205
                        if ($resql2) {
206
                            while ($obj2 = $this->db->fetch_object($resql2)) {
207
                                $ids[$obj2->rowid] = $obj2->rowid;
208
                            }
209
                            $this->db->free($resql2);
210
                        } else {
211
                            $this->errors[] = $this->db->lasterror;
212
                            return -1;
213
                        }
214
                    }
215
                } elseif ($obj->type == 'payment') {
216
                    $sql = 'SELECT DISTINCT bk.rowid, fac.ref, fac.ref, pay.fk_bank, fac.rowid as fact_id';
217
                    $sql .= " FROM " . MAIN_DB_PREFIX . "facture fac ";
218
                    $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "paiement_facture as payfac ON  payfac.fk_facture=fac.rowid";
219
                    $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "paiement as pay ON  payfac.fk_paiement=pay.rowid";
220
                    $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "accounting_bookkeeping as bk ON (bk.fk_doc = pay.fk_bank AND bk.code_journal='" . $this->db->escape($obj->code_journal) . "')";
221
                    $sql .= " WHERE payfac.fk_paiement = '" . $this->db->escape($obj->url_id) . "' ";
222
                    $sql .= " AND bk.code_journal IN (SELECT code FROM " . MAIN_DB_PREFIX . "accounting_journal WHERE nature=4 AND entity=" . $conf->entity . ") ";
223
                    $sql .= " AND fac.entity IN (" . getEntity('invoice', 0) . ")"; // We don't share object for accountancy
224
                    $sql .= " AND ( ";
225
                    if ($object->code_compta_client != "") {
226
                        $sql .= "  bk.subledger_account = '" . $this->db->escape($object->code_compta_client) . "'  ";
227
                    }
228
                    if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
229
                        $sql .= "  OR  ";
230
                    }
231
                    if ($object->code_compta_fournisseur != "") {
232
                        $sql .= "   bk.subledger_account = '" . $this->db->escape($object->code_compta_fournisseur) . "' ";
233
                    }
234
                    $sql .= " )";
235
236
                    $resql2 = $this->db->query($sql);
237
                    if ($resql2) {
238
                        while ($obj2 = $this->db->fetch_object($resql2)) {
239
                            $ids[$obj2->rowid] = $obj2->rowid;
240
                            $ids_fact[] = $obj2->fact_id;
241
                        }
242
                    } else {
243
                        $this->errors[] = $this->db->lasterror;
244
                        return -1;
245
                    }
246
                    if (count($ids_fact)) {
247
                        $sql = 'SELECT bk.rowid, fac.ref, fac.ref_supplier ';
248
                        $sql .= " FROM " . MAIN_DB_PREFIX . "facture fac ";
249
                        $sql .= " INNER JOIN " . MAIN_DB_PREFIX . "accounting_bookkeeping as bk ON(  bk.fk_doc = fac.rowid AND fac.rowid IN (" . $this->db->sanitize(implode(',', $ids_fact)) . "))";
250
                        $sql .= " WHERE code_journal IN (SELECT code FROM " . MAIN_DB_PREFIX . "accounting_journal WHERE nature=2 AND entity=" . $conf->entity . ") ";
251
                        $sql .= " AND fac.entity IN (" . getEntity('invoice', 0) . ")"; // We don't share object for accountancy
252
                        $sql .= " AND ( ";
253
                        if ($object->code_compta_client != "") {
254
                            $sql .= "  bk.subledger_account = '" . $this->db->escape($object->code_compta_client) . "'  ";
255
                        }
256
                        if ($object->code_compta_client != "" && $object->code_compta_fournisseur != "") {
257
                            $sql .= "  OR  ";
258
                        }
259
                        if ($object->code_compta_fournisseur != "") {
260
                            $sql .= "   bk.subledger_account = '" . $this->db->escape($object->code_compta_fournisseur) . "' ";
261
                        }
262
                        $sql .= " )  ";
263
264
                        $resql2 = $this->db->query($sql);
265
                        if ($resql2) {
266
                            while ($obj2 = $this->db->fetch_object($resql2)) {
267
                                $ids[$obj2->rowid] = $obj2->rowid;
268
                            }
269
                            $this->db->free($resql2);
270
                        } else {
271
                            $this->errors[] = $this->db->lasterror;
272
                            return -1;
273
                        }
274
                    }
275
                }
276
277
                if (count($ids) > 1) {
278
                    $result = $this->updateLettering($ids);
279
                }
280
            }
281
            $this->db->free($resql);
282
        }
283
        if ($error) {
284
            foreach ($this->errors as $errmsg) {
285
                dol_syslog(__METHOD__ . ' ' . $errmsg, LOG_ERR);
286
                $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
287
            }
288
            return -1 * $error;
289
        } else {
290
            return 1;
291
        }
292
    }
293
294
    /**
295
     *
296
     * @param   array       $ids            ids array
297
     * @param   int         $notrigger      no trigger
298
     * @param   bool        $partial        Partial lettering
299
     * @return  int
300
     */
301
    public function updateLettering($ids = array(), $notrigger = 0, $partial = false)
302
    {
303
        $now = dol_now();
304
        $error = 0;
305
        $affected_rows = 0;
306
307
        // Generate a string with n char 'A' (for manual/auto reconcile) or 'a' (for partial reconcile) where n is ACCOUNTING_LETTERING_NBLETTERS (So 'AA'/'aa', 'AAA'/'aaa', ...) @phan-suppress-next-line PhanParamSuspiciousOrder
308
        $letter = str_pad("", getDolGlobalInt('ACCOUNTING_LETTERING_NBLETTERS', 3), $partial ? 'a' : 'A');
309
310
        $this->db->begin();
311
312
        // Check partial / normal lettering case
313
        $sql = "SELECT ab.lettering_code, GROUP_CONCAT(DISTINCT ab.rowid SEPARATOR ',') AS bookkeeping_ids";
314
        $sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
315
        $sql .= " WHERE ab.rowid IN (" . $this->db->sanitize(implode(',', $ids)) . ")";
316
        $sql .= " GROUP BY ab.lettering_code";
317
        $sql .= " ORDER BY ab.lettering_code DESC";
318
319
        dol_syslog(__METHOD__ . " - Check partial / normal lettering case", LOG_DEBUG);
320
        $resql = $this->db->query($sql);
321
        if ($resql) {
322
            while ($obj = $this->db->fetch_object($resql)) {
323
                if (empty($obj->lettering_code)) continue;
324
325
                // Remove normal lettering code if set partial lettering
326
                if ($partial && preg_match('/^[A-Z]+$/', $obj->lettering_code)) {
327
                    if (!empty($obj->bookkeeping_ids)) $ids = array_diff($ids, explode(',', $obj->bookkeeping_ids));
328
                } elseif (!$partial && preg_match('/^[a-z]+$/', $obj->lettering_code)) {
329
                    // Delete partial lettering code if set normal lettering
330
                    $sql2 = "UPDATE " . MAIN_DB_PREFIX . "accounting_bookkeeping SET";
331
                    $sql2 .= " lettering_code = NULL";
332
                    $sql2 .= ", date_lettering = NULL";
333
                    $sql2 .= " WHERE entity IN (" . getEntity('accountancy') . ")";
334
                    $sql2 .= " AND lettering_code = '" . $this->db->escape($obj->lettering_code) . "'";
335
336
                    dol_syslog(__METHOD__ . " - Remove partial lettering", LOG_DEBUG);
337
                    $resql2 = $this->db->query($sql2);
338
                    if (!$resql2) {
339
                        $this->errors[] = 'Error' . $this->db->lasterror();
340
                        $error++;
341
                        break;
342
                    }
343
                }
344
            }
345
            $this->db->free($resql);
346
        } else {
347
            $this->errors[] = 'Error' . $this->db->lasterror();
348
            $error++;
349
        }
350
351
        if (!$error && !empty($ids)) {
352
            // Get next code
353
            $sql = "SELECT DISTINCT ab2.lettering_code";
354
            $sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
355
            $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab2 ON ab2.subledger_account = ab.subledger_account";
356
            $sql .= " WHERE ab.rowid IN (" . $this->db->sanitize(implode(',', $ids)) . ")";
357
            $sql .= " AND ab2.lettering_code != ''";
358
            $sql .= " ORDER BY ab2.lettering_code DESC";
359
360
            dol_syslog(__METHOD__ . " - Get next code", LOG_DEBUG);
361
            $resql = $this->db->query($sql);
362
            if ($resql) {
363
                while ($obj = $this->db->fetch_object($resql)) {
364
                    if (
365
                        !empty($obj->lettering_code) &&
366
                        (($partial && preg_match('/^[a-z]+$/', $obj->lettering_code)) ||
367
                            (!$partial && preg_match('/^[A-Z]+$/', $obj->lettering_code)))
368
                    ) {
369
                        $letter = $obj->lettering_code;
370
                        $letter++;
371
                        break;
372
                    }
373
                }
374
                $this->db->free($resql);
375
            } else {
376
                $this->errors[] = 'Error' . $this->db->lasterror();
377
                $error++;
378
            }
379
380
            // Test amount integrity
381
            if (!$error && !$partial) {
382
                $sql = "SELECT SUM(ABS(debit)) as deb, SUM(ABS(credit)) as cred FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping WHERE ";
383
                $sql .= " rowid IN (" . $this->db->sanitize(implode(',', $ids)) . ") AND lettering_code IS NULL AND subledger_account != ''";
384
385
                dol_syslog(__METHOD__ . " - Test amount integrity", LOG_DEBUG);
386
                $resql = $this->db->query($sql);
387
                if ($resql) {
388
                    if ($obj = $this->db->fetch_object($resql)) {
389
                        if (!(round(abs($obj->deb), 2) === round(abs($obj->cred), 2))) {
390
                            $this->errors[] = 'Total not exacts ' . round(abs($obj->deb), 2) . ' vs ' . round(abs($obj->cred), 2);
391
                            $error++;
392
                        }
393
                    }
394
                    $this->db->free($resql);
395
                } else {
396
                    $this->errors[] = 'Erreur sql' . $this->db->lasterror();
397
                    $error++;
398
                }
399
            }
400
401
            // Update lettering code
402
            if (!$error) {
403
                $sql = "UPDATE " . MAIN_DB_PREFIX . "accounting_bookkeeping SET";
404
                $sql .= " lettering_code='" . $this->db->escape($letter) . "'";
405
                $sql .= ", date_lettering = '" . $this->db->idate($now) . "'"; // todo correct date it's false
406
                $sql .= "  WHERE rowid IN (" . $this->db->sanitize(implode(',', $ids)) . ") AND lettering_code IS NULL AND subledger_account != ''";
407
408
                dol_syslog(__METHOD__ . " - Update lettering code", LOG_DEBUG);
409
                $resql = $this->db->query($sql);
410
                if (!$resql) {
411
                    $error++;
412
                    $this->errors[] = "Error " . $this->db->lasterror();
413
                } else {
414
                    $affected_rows = $this->db->affected_rows($resql);
415
                }
416
            }
417
        }
418
419
        // Commit or rollback
420
        if ($error) {
421
            $this->db->rollback();
422
            foreach ($this->errors as $errmsg) {
423
                dol_syslog(get_class($this) . "::update " . $errmsg, LOG_ERR);
424
                $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
425
            }
426
            return -1 * $error;
427
        } else {
428
            $this->db->commit();
429
            return $affected_rows;
430
        }
431
    }
432
433
    /**
434
     *
435
     * @param   array       $ids            ids array
436
     * @param   int         $notrigger      no trigger
437
     * @return  int
438
     */
439
    public function deleteLettering($ids, $notrigger = 0)
440
    {
441
        $error = 0;
442
443
        $sql = "UPDATE " . MAIN_DB_PREFIX . "accounting_bookkeeping SET";
444
        $sql .= " lettering_code = NULL";
445
        $sql .= ", date_lettering = NULL";
446
        $sql .= " WHERE rowid IN (" . $this->db->sanitize(implode(',', $ids)) . ")";
447
        $sql .= " AND subledger_account != ''";
448
449
        dol_syslog(get_class($this) . "::update", LOG_DEBUG);
450
        $resql = $this->db->query($sql);
451
        if (!$resql) {
452
            $error++;
453
            $this->errors[] = "Error " . $this->db->lasterror();
454
        }
455
456
        // Commit or rollback
457
        if ($error) {
458
            foreach ($this->errors as $errmsg) {
459
                dol_syslog(get_class($this) . "::update " . $errmsg, LOG_ERR);
460
                $this->error .= ($this->error ? ', ' . $errmsg : $errmsg);
461
            }
462
            return -1 * $error;
463
        } else {
464
            return $this->db->affected_rows($resql);
465
        }
466
    }
467
468
    /**
469
     * Lettering bookkeeping lines all types
470
     *
471
     * @param   array       $bookkeeping_ids        Lettering specific list of bookkeeping id
472
     * @param   bool        $unlettering            Do unlettering
473
     * @return  int                                 Return integer <0 if error (nb lettered = result -1), 0 if noting to lettering, >0 if OK (nb lettered)
474
     */
475
    public function bookkeepingLetteringAll($bookkeeping_ids, $unlettering = false)
476
    {
477
        dol_syslog(__METHOD__ . " - ", LOG_DEBUG);
478
479
        $error = 0;
480
        $errors = array();
481
        $nb_lettering = 0;
482
483
        $result = $this->bookkeepingLettering($bookkeeping_ids, $unlettering);
484
        if ($result < 0) {
485
            $error++;
486
            $errors = array_merge($errors, $this->errors);
487
            $nb_lettering += abs($result) - 2;
488
        } else {
489
            $nb_lettering += $result;
490
        }
491
492
        if ($error) {
493
            $this->errors = $errors;
494
            return -2 - $nb_lettering;
495
        } else {
496
            return $nb_lettering;
497
        }
498
    }
499
500
    /**
501
     * Lettering bookkeeping lines
502
     *
503
     * @param   array       $bookkeeping_ids        Lettering specific list of bookkeeping id
504
     * @param   bool        $unlettering            Do unlettering
505
     * @return  int                                 Return integer <0 if error (nb lettered = result -1), 0 if noting to lettering, >0 if OK (nb lettered)
506
     */
507
    public function bookkeepingLettering($bookkeeping_ids, $unlettering = false)
508
    {
509
        global $langs;
510
511
        $this->errors = array();
512
513
        // Clean parameters
514
        $bookkeeping_ids = is_array($bookkeeping_ids) ? $bookkeeping_ids : array();
515
516
        $error = 0;
517
        $nb_lettering = 0;
518
        $grouped_lines = $this->getLinkedLines($bookkeeping_ids);
519
        if (!is_array($grouped_lines)) {
520
            return -2;
521
        }
522
523
        foreach ($grouped_lines as $lines) {
524
            $group_error = 0;
525
            $total = 0;
526
            $do_it = !$unlettering;
527
            $lettering_code = null;
528
            $piece_num_lines = array();
529
            $bookkeeping_lines = array();
530
            foreach ($lines as $line_infos) {
531
                $bookkeeping_lines[$line_infos['id']] = $line_infos['id'];
532
                $piece_num_lines[$line_infos['piece_num']] = $line_infos['piece_num'];
533
                $total += ($line_infos['credit'] > 0 ? $line_infos['credit'] : -$line_infos['debit']);
534
535
                // Check lettering code
536
                if ($unlettering) {
537
                    if (isset($lettering_code) && $lettering_code != $line_infos['lettering_code']) {
538
                        $this->errors[] = $langs->trans('AccountancyErrorMismatchLetteringCode');
539
                        $group_error++;
540
                        break;
541
                    }
542
                    if (!isset($lettering_code)) {
543
                        $lettering_code = (string) $line_infos['lettering_code'];
544
                    }
545
                    if (!empty($line_infos['lettering_code'])) {
546
                        $do_it = true;
547
                    }
548
                } elseif (!empty($line_infos['lettering_code'])) {
549
                    $do_it = false;
550
                }
551
            }
552
553
            // Check balance amount
554
            if (!$group_error && !$unlettering && price2num($total) != 0) {
555
                $this->errors[] = $langs->trans('AccountancyErrorMismatchBalanceAmount', $total);
556
                $group_error++;
557
            }
558
559
            // Lettering/Unlettering the group of bookkeeping lines
560
            if (!$group_error && $do_it) {
561
                if ($unlettering) {
562
                    $result = $this->deleteLettering($bookkeeping_lines);
563
                } else {
564
                    $result = $this->updateLettering($bookkeeping_lines);
565
                }
566
                if ($result < 0) {
567
                    $group_error++;
568
                } elseif ($result > 0) {
569
                    $nb_lettering++;
570
                }
571
            }
572
573
            if ($group_error) {
574
                $this->errors[] = $langs->trans('AccountancyErrorLetteringBookkeeping', implode(', ', $piece_num_lines));
575
                $error++;
576
            }
577
        }
578
579
        if ($error) {
580
            return -2 - $nb_lettering;
581
        } else {
582
            return $nb_lettering;
583
        }
584
    }
585
586
    /**
587
     * Lettering bookkeeping lines
588
     *
589
     * @param   array           $bookkeeping_ids                Lettering specific list of bookkeeping id
590
     * @param   bool            $only_has_subledger_account     Get only lines who have subledger account
591
     * @return  array|int                                       Return integer <0 if error otherwise all linked lines by block
592
     */
593
    public function getLinkedLines($bookkeeping_ids, $only_has_subledger_account = true)
594
    {
595
        global $conf, $langs;
596
        $this->errors = array();
597
598
        // Clean parameters
599
        $bookkeeping_ids = is_array($bookkeeping_ids) ? $bookkeeping_ids : array();
600
601
        // Get all bookkeeping lines
602
        $sql = "SELECT DISTINCT ab.doc_type, ab.fk_doc";
603
        $sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
604
        $sql .= " WHERE ab.entity IN (" . getEntity('accountancy') . ")";
605
        $sql .= " AND ab.fk_doc > 0";
606
        if (!empty($bookkeeping_ids)) {
607
            // Get all bookkeeping lines of piece number
608
            $sql .= " AND EXISTS (";
609
            $sql .= "  SELECT rowid";
610
            $sql .= "  FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS pn";
611
            $sql .= "  WHERE pn.entity IN (" . getEntity('accountancy') . ")";
612
            $sql .= "  AND pn.rowid IN (" . $this->db->sanitize(implode(',', $bookkeeping_ids)) . ")";
613
            $sql .= "  AND pn.piece_num = ab.piece_num";
614
            $sql .= " )";
615
        }
616
        if ($only_has_subledger_account) {
617
            $sql .= " AND ab.subledger_account != ''";
618
        }
619
620
        dol_syslog(__METHOD__ . " - Get all bookkeeping lines", LOG_DEBUG);
621
        $resql = $this->db->query($sql);
622
        if (!$resql) {
623
            $this->errors[] = "Error " . $this->db->lasterror();
624
            return -1;
625
        }
626
627
        $bookkeeping_lines_by_type = array();
628
        while ($obj = $this->db->fetch_object($resql)) {
629
            $bookkeeping_lines_by_type[$obj->doc_type][$obj->fk_doc] = $obj->fk_doc;
630
        }
631
        $this->db->free($resql);
632
633
        if (empty($bookkeeping_lines_by_type)) {
634
            return array();
635
        }
636
637
        if (!empty($bookkeeping_lines_by_type['bank'])) {
638
            $new_bookkeeping_lines_by_type = $this->getDocTypeAndFkDocFromBankLines($bookkeeping_lines_by_type['bank']);
639
            if (!is_array($new_bookkeeping_lines_by_type)) {
640
                return -1;
641
            }
642
            foreach ($new_bookkeeping_lines_by_type as $doc_type => $fk_docs) {
643
                foreach ($fk_docs as $fk_doc) {
644
                    $bookkeeping_lines_by_type[$doc_type][$fk_doc] = $fk_doc;
645
                }
646
            }
647
        }
648
649
        $grouped_lines = array();
650
        foreach (self::$doc_type_infos as $doc_type => $doc_type_info) {
651
            if (!is_array($bookkeeping_lines_by_type[$doc_type])) {
652
                continue;
653
            }
654
655
            // Get all document ids grouped
656
            $doc_grouped = $this->getLinkedDocumentByGroup($bookkeeping_lines_by_type[$doc_type], $doc_type);
657
            if (!is_array($doc_grouped)) {
658
                return -1;
659
            }
660
661
            // Group all lines by document/piece number
662
            foreach ($doc_grouped as $doc_ids) {
663
                $bank_ids = $this->getBankLinesFromFkDocAndDocType($doc_ids, $doc_type);
664
                if (!is_array($bank_ids)) {
665
                    return -1;
666
                }
667
668
                // Get all bookkeeping lines linked
669
                $sql = "SELECT DISTINCT ab.rowid, ab.piece_num, ab.debit, ab.credit, ab.lettering_code";
670
                $sql .= " FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS ab";
671
                $sql .= " WHERE ab.entity IN (" . getEntity('accountancy') . ")";
672
                $sql .= " AND (";
673
                if (!empty($bank_ids)) {
674
                    $sql .= " EXISTS (";
675
                    $sql .= "  SELECT bpn.rowid";
676
                    $sql .= "  FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS bpn";
677
                    $sql .= "  WHERE bpn.entity IN (" . getEntity('accountancy') . ")";
678
                    $sql .= "  AND bpn.doc_type = 'bank'";
679
                    $sql .= "  AND bpn.fk_doc IN (" . $this->db->sanitize(implode(',', $bank_ids)) . ")";
680
                    $sql .= "  AND bpn.piece_num = ab.piece_num";
681
                    $sql .= " ) OR ";
682
                }
683
                $sql .= " EXISTS (";
684
                $sql .= "  SELECT dpn.rowid";
685
                $sql .= "  FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping AS dpn";
686
                $sql .= "  WHERE dpn.entity IN (" . getEntity('accountancy') . ")";
687
                $sql .= "  AND dpn.doc_type = '" . $this->db->escape($doc_type) . "'";
688
                $sql .= "  AND dpn.fk_doc IN (" . $this->db->sanitize(implode(',', $doc_ids)) . ")";
689
                $sql .= "  AND dpn.piece_num = ab.piece_num";
690
                $sql .= " )";
691
                $sql .= ")";
692
                if ($only_has_subledger_account) {
693
                    $sql .= " AND ab.subledger_account != ''";
694
                }
695
696
                dol_syslog(__METHOD__ . " - Get all bookkeeping lines linked", LOG_DEBUG);
697
                $resql = $this->db->query($sql);
698
                if (!$resql) {
699
                    $this->errors[] = "Error " . $this->db->lasterror();
700
                    return -1;
701
                }
702
703
                $group = array();
704
                while ($obj = $this->db->fetch_object($resql)) {
705
                    $group[$obj->rowid] = array(
706
                        'id' => $obj->rowid,
707
                        'piece_num' => $obj->piece_num,
708
                        'debit' => $obj->debit,
709
                        'credit' => $obj->credit,
710
                        'lettering_code' => $obj->lettering_code,
711
                    );
712
                }
713
                $this->db->free($resql);
714
715
                if (!empty($group)) {
716
                    $grouped_lines[] = $group;
717
                }
718
            }
719
        }
720
721
        return $grouped_lines;
722
    }
723
724
    /**
725
     * Get all fk_doc by doc_type from list of bank ids
726
     *
727
     * @param   array           $bank_ids       List of bank ids
728
     * @return  array|int                       Return integer <0 if error otherwise all fk_doc by doc_type
729
     */
730
    public function getDocTypeAndFkDocFromBankLines($bank_ids)
731
    {
732
        dol_syslog(__METHOD__ . " - bank_ids=" . json_encode($bank_ids), LOG_DEBUG);
733
734
        // Clean parameters
735
        $bank_ids = is_array($bank_ids) ? $bank_ids : array();
736
737
        if (empty($bank_ids)) {
738
            return array();
739
        }
740
741
        $bookkeeping_lines_by_type = array();
742
        foreach (self::$doc_type_infos as $doc_type => $doc_type_info) {
743
            // Get all fk_doc by doc_type from bank ids
744
            $sql = "SELECT DISTINCT dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_doc']) . " AS fk_doc";
745
            $sql .= " FROM " . MAIN_DB_PREFIX . $this->db->sanitize($doc_type_info['payment_table']) . " AS p";
746
            $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($doc_type_info['doc_payment_table']) . " AS dp ON dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_payment']) . " = p.rowid";
747
            $sql .= " WHERE p." . $this->db->sanitize($doc_type_info['payment_table_fk_bank']) . " IN (" . $this->db->sanitize(implode(',', $bank_ids)) . ")";
748
            $sql .= " AND dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_doc']) . " > 0";
749
750
            dol_syslog(__METHOD__ . " - Get all fk_doc by doc_type from list of bank ids for '" . $doc_type . "'", LOG_DEBUG);
751
            $resql = $this->db->query($sql);
752
            if (!$resql) {
753
                $this->errors[] = "Error " . $this->db->lasterror();
754
                return -1;
755
            }
756
757
            while ($obj = $this->db->fetch_object($resql)) {
758
                $bookkeeping_lines_by_type[$doc_type][$obj->fk_doc] = $obj->fk_doc;
759
            }
760
            $this->db->free($resql);
761
        }
762
763
        return $bookkeeping_lines_by_type;
764
    }
765
766
    /**
767
     * Get all bank ids from list of document ids of a type
768
     *
769
     * @param   array           $document_ids   List of document id
770
     * @param   string          $doc_type       Type of document ('customer_invoice' or 'supplier_invoice', ...)
771
     * @return  array|int                       Return integer <0 if error otherwise all all bank ids from list of document ids of a type
772
     */
773
    public function getBankLinesFromFkDocAndDocType($document_ids, $doc_type)
774
    {
775
        global $langs;
776
777
        dol_syslog(__METHOD__ . " - bank_ids=" . json_encode($document_ids) . ", doc_type=$doc_type", LOG_DEBUG);
778
779
        // Clean parameters
780
        $document_ids = is_array($document_ids) ? $document_ids : array();
781
        //remove empty entries
782
        $document_ids = array_filter($document_ids);
783
784
        $doc_type = trim($doc_type);
785
786
        if (empty($document_ids)) {
787
            return array();
788
        }
789
        if (!is_array(self::$doc_type_infos[$doc_type])) {
790
            $langs->load('errors');
791
            $this->errors[] = $langs->trans('ErrorBadParameters');
792
            return -1;
793
        }
794
795
        $doc_type_info = self::$doc_type_infos[$doc_type];
796
        $bank_ids = array();
797
798
        // Get all fk_doc by doc_type from bank ids
799
        $sql = "SELECT DISTINCT p." . $this->db->sanitize($doc_type_info['payment_table_fk_bank']) . " AS fk_doc";
800
        $sql .= " FROM " . MAIN_DB_PREFIX . $this->db->sanitize($doc_type_info['payment_table']) . " AS p";
801
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($doc_type_info['doc_payment_table']) . " AS dp ON dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_payment']) . " = p.rowid";
802
        $sql .= " WHERE dp." . $this->db->sanitize($doc_type_info['doc_payment_table_fk_doc']) . " IN (" . $this->db->sanitize(implode(',', $document_ids)) . ")";
803
        $sql .= " AND p." . $this->db->sanitize($doc_type_info['payment_table_fk_bank']) . " > 0";
804
805
        dol_syslog(__METHOD__ . " - Get all bank ids from list of document ids of a type '" . $doc_type . "'", LOG_DEBUG);
806
        $resql = $this->db->query($sql);
807
        if (!$resql) {
808
            $this->errors[] = "Error " . $this->db->lasterror();
809
            return -1;
810
        }
811
812
        while ($obj = $this->db->fetch_object($resql)) {
813
            $bank_ids[$obj->fk_doc] = $obj->fk_doc;
814
        }
815
        $this->db->free($resql);
816
817
        return $bank_ids;
818
    }
819
820
    /**
821
     * Get all linked document ids by group and type
822
     *
823
     * @param   array           $document_ids   List of document id
824
     * @param   string          $doc_type       Type of document ('customer_invoice' or 'supplier_invoice', ...)
825
     * @return  array|int                       Return integer <0 if error otherwise all linked document ids by group and type [ [ 'doc_type' => [ doc_id, ... ], ... ], ... ]
826
     */
827
    public function getLinkedDocumentByGroup($document_ids, $doc_type)
828
    {
829
        global $langs;
830
831
        // Clean parameters
832
        $document_ids = is_array($document_ids) ? $document_ids : array();
833
        $doc_type = trim($doc_type);
834
        //remove empty entries
835
        $document_ids = array_filter($document_ids);
836
837
        if (empty($document_ids)) {
838
            return array();
839
        }
840
841
        if (!is_array(self::$doc_type_infos[$doc_type])) {
842
            $langs->load('errors');
843
            $this->errors[] = $langs->trans('ErrorBadParameters');
844
            return -1;
845
        }
846
847
        $doc_type_info = self::$doc_type_infos[$doc_type];
848
849
        // Get document lines
850
        $current_document_ids = array();
851
        $link_by_element = array();
852
        $element_by_link = array();
853
        foreach ($doc_type_info['linked_info'] as $linked_info) {
854
            if (empty($linked_info['fk_line_link'])) {
855
                $sql = "SELECT DISTINCT tl2." . $this->db->sanitize($linked_info['fk_link']) . " AS fk_link, tl2." . $this->db->sanitize($linked_info['fk_doc']) . " AS fk_doc";
856
                $sql .= " FROM " . MAIN_DB_PREFIX . $this->db->sanitize($linked_info['table']) . " AS tl";
857
                $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($linked_info['table']) . " AS tl2 ON tl2." . $this->db->sanitize($linked_info['fk_link']) . " = tl." . $this->db->sanitize($linked_info['fk_link']);
858
                $sql .= " WHERE tl." . $this->db->sanitize($linked_info['fk_doc']) . " IN (" . $this->db->sanitize(implode(',', $document_ids)) . ")";
859
            } else {
860
                $sql = "SELECT DISTINCT tl2.fk_link, tl2.fk_doc";
861
                $sql .= " FROM (";
862
                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
863
                $sql .= "   SELECT DISTINCT " . $this->db->ifsql("tll." . $this->db->sanitize($linked_info['fk_table_link_line_parent']), "tll." . $this->db->sanitize($linked_info['fk_table_link_line_parent']), "tl." . $this->db->sanitize($linked_info['fk_link'])) . " AS fk_link, tl." . $this->db->sanitize($linked_info['fk_doc']) . " AS fk_doc";
864
                $sql .= "   FROM " . MAIN_DB_PREFIX . $this->db->sanitize($linked_info['table']) . " AS tl";
865
                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
866
                $sql .= "   LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($linked_info['table_link_line']) . " AS tll ON tll." . $this->db->sanitize($linked_info['fk_table_link_line']) . " = tl." . $this->db->sanitize($linked_info['fk_line_link']);
867
                $sql .= ") AS tl";
868
                $sql .= " LEFT JOIN (";
869
                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
870
                $sql .= "   SELECT DISTINCT " . $this->db->ifsql("tll." . $this->db->sanitize($linked_info['fk_table_link_line_parent']), "tll." . $this->db->sanitize($linked_info['fk_table_link_line_parent']), "tl." . $this->db->sanitize($linked_info['fk_link'])) . " AS fk_link, tl." . $this->db->sanitize($linked_info['fk_doc']) . " AS fk_doc";
871
                $sql .= "   FROM " . MAIN_DB_PREFIX . $this->db->sanitize($linked_info['table']) . " AS tl";
872
                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
873
                $sql .= "   LEFT JOIN " . MAIN_DB_PREFIX . $this->db->sanitize($linked_info['table_link_line']) . " AS tll ON tll." . $this->db->sanitize($linked_info['fk_table_link_line']) . " = tl." . $this->db->sanitize($linked_info['fk_line_link']);
874
                $sql .= ") AS tl2 ON tl2.fk_link = tl.fk_link";
875
                $sql .= " WHERE tl.fk_doc IN (" . $this->db->sanitize(implode(',', $document_ids)) . ")";
876
                $sql .= " AND tl2.fk_doc IS NOT NULL";
877
            }
878
879
            dol_syslog(__METHOD__ . " - Get document lines", LOG_DEBUG);
880
            $resql = $this->db->query($sql);
881
            if (!$resql) {
882
                $this->errors[] = "Error " . $this->db->lasterror();
883
                return -1;
884
            }
885
886
            $is_fk_link_is_also_fk_doc = !empty($linked_info['is_fk_link_is_also_fk_doc']);
887
            while ($obj = $this->db->fetch_object($resql)) {
888
                $current_document_ids[$obj->fk_doc] = $obj->fk_doc;
889
890
                $link_key = $linked_info['prefix'] . $obj->fk_link;
891
                $element_by_link[$link_key][$obj->fk_doc] = $obj->fk_doc;
892
                $link_by_element[$obj->fk_doc][$link_key] = $link_key;
893
                if ($is_fk_link_is_also_fk_doc) {
894
                    $element_by_link[$link_key][$obj->fk_link] = $obj->fk_link;
895
                    $link_by_element[$obj->fk_link][$link_key] = $link_key;
896
                }
897
            }
898
            $this->db->free($resql);
899
        }
900
901
        if (count(array_diff($document_ids, $current_document_ids))) {
902
            return $this->getLinkedDocumentByGroup($current_document_ids, $doc_type);
903
        }
904
905
        return $this->getGroupElements($link_by_element, $element_by_link);
906
    }
907
908
    /**
909
     * Get element ids grouped by link or element in common
910
     *
911
     * @param   array   $link_by_element    List of payment ids by link key
912
     * @param   array   $element_by_link    List of element ids by link key
913
     * @param   string  $link_key           Link key (used for recursive function)
914
     * @param   array   $current_group      Current group (used for recursive function)
915
     * @return  array                       List of element ids grouped by link or element in common
916
     */
917
    public function getGroupElements(&$link_by_element, &$element_by_link, $link_key = '', &$current_group = array())
918
    {
919
        $grouped_elements = array();
920
        if (!empty($link_key) && !isset($element_by_link[$link_key])) {
921
            // Return if specific link key not found
922
            return $grouped_elements;
923
        }
924
925
        if (empty($link_key)) {
926
            // Save list when is the begin of recursive function
927
            $save_link_by_element = $link_by_element;
928
            $save_element_by_link = $element_by_link;
929
        }
930
931
        do {
932
            // Get current element id, get this payment id list and delete the entry
933
            $current_link_key = !empty($link_key) ? $link_key : array_keys($element_by_link)[0];
934
            $element_ids = $element_by_link[$current_link_key];
935
            unset($element_by_link[$current_link_key]);
936
937
            foreach ($element_ids as $element_id) {
938
                // Continue if element id in not found
939
                if (!isset($link_by_element[$element_id])) {
940
                    continue;
941
                }
942
943
                // Set the element in the current group
944
                $current_group[$element_id] = $element_id;
945
946
                // Get current link keys, get this element id list and delete the entry
947
                $link_keys = $link_by_element[$element_id];
948
                unset($link_by_element[$element_id]);
949
950
                // Set element id on the current group for each link key of the element
951
                foreach ($link_keys as $key) {
952
                    $this->getGroupElements($link_by_element, $element_by_link, $key, $current_group);
953
                }
954
            }
955
956
            if (empty($link_key)) {
957
                // Save current group and reset the current group when is the begin of recursive function
958
                $grouped_elements[] = $current_group;
959
                $current_group = array();
960
            }
961
        } while (!empty($element_by_link) && empty($link_key));
962
963
        if (empty($link_key)) {
964
            // Restore list when is the begin of recursive function
965
            $link_by_element = $save_link_by_element;
966
            $element_by_link = $save_element_by_link;
967
        }
968
969
        return $grouped_elements;
970
    }
971
}
972