7 public function add($data)
9 $this->_checkFields($data);
11 $db = DB
::getInstance();
13 if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices WHERE
14 (debut <= :debut AND fin >= :debut) OR (debut <= :fin AND fin >= :fin);', false,
15 ['debut' => $data['debut'], 'fin' => $data['fin']]))
17 throw new UserException('La date de début ou de fin se recoupe avec un autre exercice.');
20 if ($db->querySingle('SELECT 1 FROM compta_exercices WHERE cloture = 0;'))
22 throw new UserException('Il n\'est pas possible de créer un nouvel exercice tant qu\'il existe un exercice non-clôturé.');
25 $db->simpleInsert('compta_exercices', [
26 'libelle' => trim($data['libelle']),
27 'debut' => $data['debut'],
28 'fin' => $data['fin'],
31 return $db->lastInsertRowId();
34 public function edit($id, $data)
36 $db = DB
::getInstance();
38 $this->_checkFields($data);
40 // Evitons que les exercices se croisent
41 if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices WHERE id != :id AND
42 ((debut <= :debut AND fin >= :debut) OR (debut <= :fin AND fin >= :fin));', false,
43 ['debut' => $data['debut'], 'fin' => $data['fin'], 'id' => (int) $id]))
45 throw new UserException('La date de début ou de fin se recoupe avec un autre exercice.');
48 // On vérifie qu'on ne va pas mettre des opérations en dehors de tout exercice
49 if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ?
50 AND date < ? LIMIT 1;', false, (int)$id, $data['debut']))
52 throw new UserException('Des opérations de cet exercice ont une date antérieure à la date de début de l\'exercice.');
55 if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ?
56 AND date > ? LIMIT 1;', false, (int)$id, $data['fin']))
58 throw new UserException('Des opérations de cet exercice ont une date postérieure à la date de fin de l\'exercice.');
61 $db->simpleUpdate('compta_exercices', [
62 'libelle' => trim($data['libelle']),
63 'debut' => $data['debut'],
64 'fin' => $data['fin'],
65 ], 'id = \''.(int)$id.'\'');
71 * Clôturer un exercice et en ouvrir un nouveau
72 * Le report à nouveau n'est pas effectué automatiquement par cette fonction, voir doReports pour ça.
73 * @param integer $id ID de l'exercice à clôturer
74 * @param string $end Date de clôture de l'exercice au format Y-m-d
75 * @return integer L'ID du nouvel exercice créé
77 public function close($id, $end)
79 $db = DB
::getInstance();
81 if (!utils
::checkDate($end))
83 throw new UserException('Date de fin vide ou invalide.');
88 // Clôture de l'exercice
89 $db->simpleUpdate('compta_exercices', [
92 ], 'id = \''.(int)$id.'\'');
94 // Date de début du nouvel exercice : lendemain de la clôture du précédent exercice
95 $new_begin = utils
::modifyDate($end, '+1 day');
97 // Date de fin du nouvel exercice : un an moins un jour après l'ouverture
98 $new_end = utils
::modifyDate($new_begin, '+1 year -1 day');
100 // Enfin sauf s'il existe déjà des opérations après cette date, auquel cas la date de fin
101 // est fixée à la date de la dernière opération, ceci pour ne pas avoir d'opération
102 // orpheline d'exercice
103 $last = $db->simpleQuerySingle('SELECT date FROM compta_journal WHERE id_exercice = ? AND date >= ? ORDER BY date DESC LIMIT 1;', false, $id, $new_end);
104 $new_end = $last ?
: $new_end;
106 // Création du nouvel exercice
107 $new_id = $this->add([
108 'debut' => $new_begin,
110 'libelle' => 'Nouvel exercice'
113 // Ré-attribution des opérations de l'exercice à clôturer qui ne sont pas dans son
114 // intervale au nouvel exercice
115 $db->simpleExec('UPDATE compta_journal SET id_exercice = ? WHERE id_exercice = ? AND date >= ?;',
116 $new_id, $id, $new_begin);
124 * Créer les reports à nouveau issus de l'exercice $old_id dans le nouvel exercice courant
125 * @param integer $old_id ID de l'ancien exercice
126 * @param integer $new_id ID du nouvel exercice
127 * @param string $date Date Y-m-d donnée aux opérations créées
128 * @return boolean true si succès
130 public function doReports($old_id, $date)
132 $db = DB
::getInstance();
136 $this->solderResultat($old_id, $date);
138 $report_crediteur = 110;
139 $report_debiteur = 119;
141 // Récupérer chacun des comptes de bilan et leurs soldes (uniquement les classes 1 à 5)
142 $statement = $db->simpleStatement('SELECT compta_comptes.id AS compte, compta_comptes.position AS position,
143 COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_debit = compta_comptes.id AND id_exercice = :id), 0)
144 - COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_credit = compta_comptes.id AND id_exercice = :id), 0) AS solde
146 INNER JOIN compta_journal ON compta_comptes.id = compta_journal.compte_debit
147 OR compta_comptes.id = compta_journal.compte_credit
148 WHERE id_exercice = :id AND solde != 0 AND CAST(substr(compta_comptes.id, 1, 1) AS INTEGER) <= 5
149 GROUP BY compta_comptes.id;', ['id' => $old_id]);
152 $journal = new Compta_Journal
;
154 while ($row = $statement->fetchArray(SQLITE3_ASSOC
))
156 $solde = ($row['position'] & Compta_Comptes
::ACTIF
) ?
abs($row['solde']) : -abs($row['solde']);
157 $solde = round($solde, 2);
166 // Chaque solde de compte est reporté dans le nouvel exercice
168 'libelle' => 'Report à nouveau',
170 'montant' => abs($solde),
171 'compte_debit' => ($solde < 0 ?
NULL : $row['compte']),
172 'compte_credit' => ($solde > 0 ?
NULL : $row['compte']),
173 'remarques' => 'Report de solde créé automatiquement à la clôture de l\'exercice précédent',
177 // FIXME utiliser $diff pour équilibrer
185 * Solder les comptes de charge et de produits de l'exercice N
186 * et les inscrire au résultat de l'exercice N+1
187 * @param integer $exercice ID de l'exercice à solder
188 * @param string $date Date de début de l'exercice Y-m-d
189 * @return boolean true en cas de succès
191 public function solderResultat($exercice, $date)
193 $db = DB
::getInstance();
195 $resultat_excedent = 120;
196 $resultat_debiteur = 129;
198 $resultat = $this->getCompteResultat($exercice);
199 $resultat = $resultat['resultat'];
203 $journal = new Compta_Journal
;
205 'libelle' => 'Résultat de l\'exercice précédent',
208 [ [ 'compte' => $resultat > 0 ?
129 : 120
209 , 'montant' => abs($resultat) ]
217 public function delete($id)
219 $db = DB
::getInstance();
221 // Ne pas supprimer un compte qui est utilisé !
222 if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ? LIMIT 1;', false, $id))
224 throw new UserException('Cet exercice ne peut être supprimé car des opérations comptables y sont liées.');
227 $db->simpleExec('DELETE FROM compta_exercices WHERE id = ?;', (int)$id);
232 public function get($id)
234 $db = DB
::getInstance();
235 return $db->simpleQuerySingle('SELECT *, strftime(\'%s\', debut) AS debut,
236 strftime(\'%s\', fin) AS fin FROM compta_exercices WHERE id = ?;', true, (int)$id);
239 public function getCurrent()
241 $db = DB
::getInstance();
242 return $db->querySingle('SELECT *, strftime(\'%s\', debut) AS debut, strftime(\'%s\', fin) FROM compta_exercices
243 WHERE cloture = 0 LIMIT 1;', true);
246 public function getCurrentId()
248 $db = DB
::getInstance();
249 return $db->querySingle('SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1;');
252 public function getList()
254 $db = DB
::getInstance();
255 return $db->simpleStatementFetchAssocKey('SELECT id, *, strftime(\'%s\', debut) AS debut,
256 strftime(\'%s\', fin) AS fin,
257 (SELECT COUNT(*) FROM compta_journal WHERE id_exercice = compta_exercices.id) AS nb_operations
258 FROM compta_exercices ORDER BY fin DESC;', SQLITE3_ASSOC
);
261 protected function _checkFields(&$data)
263 if (empty($data['libelle']) ||
!trim($data['libelle']))
265 throw new UserException('Le libellé ne peut rester vide.');
268 $data['libelle'] = trim($data['libelle']);
270 if (empty($data['debut']) ||
!checkdate(substr($data['debut'], 5, 2), substr($data['debut'], 8, 2), substr($data['debut'], 0, 4)))
272 throw new UserException('Date de début vide ou invalide.');
275 if (empty($data['fin']) ||
!checkdate(substr($data['fin'], 5, 2), substr($data['fin'], 8, 2), substr($data['fin'], 0, 4)))
277 throw new UserException('Date de fin vide ou invalide.');
284 public function getJournal($exercice)
286 $db = DB
::getInstance();
287 $query = 'SELECT *, strftime(\'%s\', date) AS date, -montant AS montant_oppose
289 LEFT JOIN compta_journal ON compta_journal.id = compta_flux.id_journal
290 WHERE compta_journal.id_exercice = '.(int)$exercice.'
292 return $db->simpleStatementFetch($query);
295 public function getGrandLivre($exercice)
297 $db = DB
::getInstance();
298 $livre = ['classes' => [], 'debit' => 0.0, 'credit' => 0.0];
300 $res = $db->prepare('SELECT compte FROM compta_flux
301 LEFT JOIN compta_journal ON compta_journal.id = compta_flux.id_journal
302 WHERE id_exercice = '.(int)$exercice.'
303 ORDER BY base64(compte) COLLATE BINARY ASC;'
306 while ($row = $res->fetchArray(SQLITE3_NUM
))
310 if (is_null($compte))
313 $classe = substr($compte, 0, 1);
314 $parent = substr($compte, 0, 2);
316 if (!array_key_exists($classe, $livre['classes']))
318 $livre['classes'][$classe] = [];
321 if (!array_key_exists($parent, $livre['classes'][$classe]))
323 $livre['classes'][$classe][$parent] = [
329 $livre['classes'][$classe][$parent]['comptes'][$compte] = ['debit' => 0.0, 'credit' => 0.0, 'journal' => []];
331 $livre['classes'][$classe][$parent]['comptes'][$compte]['journal'] = $db->simpleStatementFetch(
332 'SELECT *, strftime(\'%s\', date) AS date FROM compta_journal
333 LEFT JOIN compta_flux ON compta_journal.id = compta_flux.id_journal
334 WHERE compte = :compte AND id_exercice = '.(int)$exercice.'
335 ORDER BY date, numero_piece, id;', SQLITE3_ASSOC
, ['compte' => $compte]);
337 $debit = (float) $db->simpleQuerySingle(
338 'SELECT SUM(montant) FROM compta_journal
339 LEFT JOIN compta_flux ON compta_journal.id = compta_flux.id_journal
340 WHERE compte = ? AND montant > 0 AND id_exercice = '.(int)$exercice.';',
342 $credit = (float) $db->simpleQuerySingle(
343 'SELECT -SUM(montant) FROM compta_journal
344 LEFT JOIN compta_flux ON compta_journal.id = compta_flux.id_journal
345 WHERE compte = ? AND montant < 0 AND id_exercice = '.(int)$exercice.';',
348 $livre['classes'][$classe][$parent]['comptes'][$compte]['debit'] = $debit;
349 $livre['classes'][$classe][$parent]['comptes'][$compte]['credit'] = $credit;
351 $livre['classes'][$classe][$parent]['total'] +
= $debit;
352 $livre['classes'][$classe][$parent]['total'] -= $credit;
354 $livre['debit'] +
= $debit;
355 $livre['credit'] +
= $credit;
363 public function getCompteResultat($exercice)
365 $db = DB
::getInstance();
367 $charges = ['comptes' => [], 'total' => 0.0];
368 $produits = ['comptes' => [], 'total' => 0.0];
371 $res = $db->prepare('SELECT compte, SUM(debit), SUM(credit)
373 (SELECT compte, SUM(montant) AS debit, 0 AS credit
375 LEFT JOIN compta_flux ON compta_journal.id = compta_flux.id_journal
376 WHERE montant > 0 AND id_exercice = '.(int)$exercice.' GROUP BY compte
378 SELECT compte, 0 AS debit, -SUM(montant) AS credit
380 LEFT JOIN compta_flux ON compta_journal.id = compta_flux.id_journal
381 WHERE montant < 0 AND id_exercice = '.(int)$exercice.' GROUP BY compte)
382 WHERE compte LIKE \'6%\' OR compte LIKE \'7%\'
384 ORDER BY base64(compte) COLLATE BINARY ASC;'
388 while ($row = $res->fetchArray(SQLITE3_NUM
))
390 list($compte, $debit, $credit) = $row;
391 print_r([$compte, $debit, $credit]);
392 $classe = substr($compte, 0, 1);
393 $parent = substr($compte, 0, 2);
397 if (!isset($charges['comptes'][$parent]))
399 $charges['comptes'][$parent] = ['comptes' => [], 'solde' => 0.0];
402 $solde = round($debit - $credit, 2);
407 $charges['comptes'][$parent]['comptes'][$compte] = $solde;
408 $charges['total'] +
= $solde;
409 $charges['comptes'][$parent]['solde'] +
= $solde;
411 elseif ($classe == 7)
413 if (!isset($produits['comptes'][$parent]))
415 $produits['comptes'][$parent] = ['comptes' => [], 'solde' => 0.0];
418 $solde = round($credit - $debit, 2);
423 $produits['comptes'][$parent]['comptes'][$compte] = $solde;
424 $produits['total'] +
= $solde;
425 $produits['comptes'][$parent]['solde'] +
= $solde;
431 $resultat = $produits['total'] - $charges['total'];
433 return ['charges' => $charges, 'produits' => $produits, 'resultat' => $resultat];
437 * Calculer le bilan comptable pour l'exercice $exercice
438 * @param integer $exercice ID de l'exercice dont il faut produire le bilan
439 * @param boolean $resultat true s'il faut calculer le résultat de l'exercice (utile pour un exercice en cours)
440 * @return array Un tableau multi-dimensionnel avec deux clés : actif et passif
442 public function getBilan($exercice)
444 $db = DB
::getInstance();
446 $include = [Compta_Comptes
::ACTIF
, Compta_Comptes
::PASSIF
,
447 Compta_Comptes
::PASSIF | Compta_Comptes
::ACTIF
];
449 $actif = ['comptes' => [], 'total' => 0.0];
450 $passif = ['comptes' => [], 'total' => 0.0];
452 $resultat = $this->getCompteResultat($exercice);
454 if ($resultat['resultat'] >= 0)
456 $passif['comptes']['12'] = [
457 'comptes' => ['120' => $resultat['resultat']],
458 'solde' => $resultat['resultat']
461 $passif['total'] = $resultat['resultat'];
465 $passif['comptes']['12'] = [
466 'comptes' => ['129' => $resultat['resultat']],
467 'solde' => $resultat['resultat']
470 $passif['total'] = $resultat['resultat'];
473 // Y'a sûrement moyen d'améliorer tout ça pour que le maximum de travail
474 // soit fait au niveau du SQL, mais pour le moment ça marche
475 $res = $db->prepare('SELECT compte, debit, credit, (SELECT position FROM compta_comptes WHERE id = compte) AS position
477 (SELECT compte, SUM(montant) AS debit, NULL AS credit
479 LEFT JOIN compta_flux ON compta_journal.id = compta_flux.id_journal
480 WHERE montant > 0 AND id_exercice = '.(int)$exercice.' GROUP BY compte
482 SELECT compte, NULL AS debit, SUM(montant) AS credit
484 LEFT JOIN compta_flux ON compta_journal.id = compta_flux.id_journal
485 WHERE montant < 0 AND id_exercice = '.(int)$exercice.' GROUP BY compte)
486 WHERE compte IN (SELECT id FROM compta_comptes WHERE position IN ('.implode(', ', $include).'))
487 ORDER BY base64(compte) COLLATE BINARY ASC;'
490 while ($row = $res->fetchArray(SQLITE3_NUM
))
492 list($compte, $debit, $credit, $position) = $row;
493 $parent = substr($compte, 0, 2);
494 $classe = $compte[0];
496 if (($position & Compta_Comptes
::ACTIF
) && ($position & Compta_Comptes
::PASSIF
))
498 $solde = $debit - $credit;
503 $position = 'passif';
507 $solde = abs($solde);
509 else if ($position & Compta_Comptes
::ACTIF
)
512 $solde = $debit - $credit;
514 else if ($position & Compta_Comptes
::PASSIF
)
516 $position = 'passif';
517 $solde = $credit - $debit;
524 if (!isset($
{$position}['comptes'][$parent]))
526 $
{$position}['comptes'][$parent] = ['comptes' => [], 'solde' => 0];
529 if (!isset($
{$position}['comptes'][$parent]['comptes'][$compte]))
531 $
{$position}['comptes'][$parent]['comptes'][$compte] = 0;
534 $solde = round($solde, 2);
535 $
{$position}['comptes'][$parent]['comptes'][$compte] +
= $solde;
536 $
{$position}['total'] +
= $solde;
537 $
{$position}['comptes'][$parent]['solde'] +
= $solde;
542 // Suppression des soldes nuls
543 foreach ($passif['comptes'] as $parent=>$p)
545 if ($p['solde'] == 0)
547 unset($passif['comptes'][$parent]);
551 foreach ($p['comptes'] as $id=>$solde)
555 unset($passif['comptes'][$parent]['comptes'][$id]);
560 foreach ($actif['comptes'] as $parent=>$p)
562 if (empty($p['solde']))
564 unset($actif['comptes'][$parent]);
568 foreach ($p['comptes'] as $id=>$solde)
572 unset($actif['comptes'][$parent]['comptes'][$id]);
577 return ['actif' => $actif, 'passif' => $passif];