Artifact 7cc9fbed31cc815ddd10074cb07b7bb352ebd074:


<?php

namespace Garradin\Compta;

use Garradin\Entity;
use Garradin\DB;
use Garradin\Utils;
use Garradin\UserException;

class Comptes
{
    const PASSIF = 0x01;
    const ACTIF = 0x02;
    const PRODUIT = 0x04;
    const CHARGE = 0x08;

    /**
     * Importe un plan comptable
     * @param  string $source_file Chemin du fichier à importer.
     * @param  boolean $delete_all True active la suppression des tous les anciens comptes (peu importe plan_comptable)
     * @return boolean/array Retourne un array des comptes non-supprimés avec leur raison, s'il y en a. Sinon true.
     *
     * Accepte 0 ou 1 argument : soit un chemin, soit true.
     * Sans arguments : importe le plan par défaut et ne supprime que les comptes
     * plus présent appartenants au plan d'origine (WHERE plan_comptable = 1)
     */
    public function importPlan($source_file = null, $delete_all = false)
    {
        $reset = false;

        if(null == $source_file)
        {
            $reset = true;
            $source_file = \Garradin\ROOT . '/include/data/plan_comptable.json';
        }

        $plan = json_decode(file_get_contents($source_file));

        if(is_null($plan))
        {
            throw new UserException('Le fichier n\'est pas du JSON ou n\'a pas pu être décodé.');
        }

        $db = DB::getInstance();
        $db->begin();
        $codes = [];

        foreach ($plan as $code=>$compte)
        {
            $codes[$code] = $db->firstColumn('SELECT id FROM compta_comptes WHERE code = ?;', $code);

            if (0 === $compte->parent) {
                $parent = null;
            }
            else {
                $parent = $db->firstColumn('SELECT id FROM compta_comptes WHERE code = ?;', $compte->parent);

                if (!$parent) {
                    throw new UserException(sprintf('Le compte parent "%s" n\'existe pas', $compte->parent));
                }
            }

            if ($codes[$code])
            {
                $db->update('compta_comptes', [
                    'parent'    =>  $parent,
                    'libelle'   =>  $compte->nom,
                    'position'  =>  $compte->position,
                    'plan_comptable' => $reset || !empty($compte->plan_comptable) ? 1 : 0,
                ], 'code = :code AND id_exercice IS NULL', ['code' => $code]);
            }
            else
            {
                $db->insert('compta_comptes', [
                    'code'      =>  $code,
                    'parent'    =>  $parent,
                    'libelle'   =>  $compte->nom,
                    'position'  =>  $compte->position,
                    'plan_comptable' => $reset || !empty($compte->plan_comptable) ? 1 : 0,
                    'id_exercice' => null,
                ]);

                $codes[$code] = $db->lastInsertRowId();
            }
        }

        // Effacer les comptes du plan comptable s'ils ne sont pas utilisés ailleurs
        // et qu'ils ne sont pas dans le nouveau plan comptable qu'on vient d'importer
        $sql = 'DELETE FROM compta_comptes WHERE id_exercice IS NULL AND id NOT IN (
            SELECT id FROM compta_comptes_bancaires
            UNION SELECT compte FROM compta_mouvements_lignes
            UNION SELECT compte FROM compta_categories)
            AND '. $db->where('code', 'NOT IN', array_keys($codes));

        // Si on ne fait qu'importer une mise à jour du plan comptable,
        // ne supprimer que les comptes qui n'ont pas été créés par l'usager
        if (!$delete_all) {
            $sql .= ' AND ' . $db->where('plan_comptable', 1);
        }

        $db->commit();

        return true;
    }

    public function exportPlan()
    {
        $name = 'plan_comptable';

        header('Content-type: application/json');
        header(sprintf('Content-Disposition: attachment; filename="%s.json"', $name));

        $liste = $this->listTree(0, true);

        $export = [];

        foreach ($liste as $k => $v)
        {
            $export[$v->id] = [
                'code'           => $v->id,
                'nom'            => $v->libelle,
                'parent'         => $v->parent,
                'position'       => $v->position,
                'plan_comptable' => $v->plan_comptable,
                'desactive'      => $v->desactive,
            ];
        }

        file_put_contents('php://output', json_encode($export, JSON_PRETTY_PRINT));

        return true;
    }

    public function add($data)
    {
        $this->_checkFields($data, true);

        $db = DB::getInstance();

        if (empty($data['id']))
        {
            $new_id = $data['parent'];
            $letters = range('A', 'Z');
            $sub_accounts = $db->getAssoc('SELECT id, id FROM compta_comptes 
                WHERE parent = ? ORDER BY id COLLATE NOCASE ASC;', $data['parent']);

            foreach ($letters as $letter)
            {
                if (!in_array($new_id . $letter, $sub_accounts))
                {
                    $new_id .= $letter;
                    break;
                }
            }

            // On a exaucé le nombre de sous-comptes possibles
            if ($new_id == $data['parent'])
            {
                throw new UserException('Nombre de sous-comptes maximal atteint pour ce compte parent-ci.');
            }
        }
        else
        {
            $new_id = strtoupper($data['id']);

            $parent = false;
            $id = $new_id;

            // Vérification que c'est bien le bon parent !
            // Sinon risque par exemple d'avoir parent = 5 et id = 512A !
            while (!$parent && strlen($id))
            {
                // On enlève un caractère à la fin jusqu'à trouver un compte parent
                $id = substr($id, 0, -1);
                $parent = $db->firstColumn('SELECT id FROM compta_comptes WHERE id = ?;', $id);
            }

            if (!$parent || $parent != $data['parent'])
            {
                throw new UserException('Le compte parent sélectionné est incorrect, par exemple pour créer un compte 512A il faut sélectionner 512 comme compte parent.');
            }
        }

        // Vérification que le compte n'existe pas déjà
        if ($db->test('compta_comptes', 'id = ?', $new_id))
        {
            throw new UserException('Ce numéro de compte existe déjà dans le plan comptable : ' . $new_id);
        }

        if (isset($data['position']))
        {
            $position = (int) $data['position'];
        }
        else
        {
            $position = $db->firstColumn('SELECT position FROM compta_comptes WHERE id = ?;', $data['parent']);
        }

        $db->insert('compta_comptes', [
            'id'        =>  $new_id,
            'libelle'   =>  trim($data['libelle']),
            'parent'    =>  $data['parent'],
            'plan_comptable' => 0,
            'position'  =>  (int)$position,
        ]);

        return $new_id;
    }

    public function edit($id, $data)
    {
        $db = DB::getInstance();

        $id = trim($id);

        // Vérification que l'on peut éditer ce compte
        if ($db->firstColumn('SELECT plan_comptable FROM compta_comptes WHERE id = ?;', $id))
        {
            throw new UserException('Ce compte fait partie du plan comptable et n\'est pas modifiable.');
        }

        if (isset($data['position']) && empty($data['position']))
        {
            throw new UserException('Aucune position du compte n\'a été indiquée.');
        }

        $this->_checkFields($data);

        $update = [
            'libelle'   =>  trim($data['libelle']),
        ];

        if (isset($data['position']))
        {
            $update['position'] = (int) trim($data['position']);
        }

        $db->update('compta_comptes', $update, $db->where('id', $id));

        return true;
    }

    public function delete($id)
    {
        $db = DB::getInstance();

        $id = trim($id);

        // Ne pas supprimer un compte qui est utilisé !
        if ($db->test('compta_journal', 'compte_debit = ? OR compte_credit = ?', $id, $id))
        {
            throw new UserException('Ce compte ne peut être supprimé car des opérations comptables y sont liées.');
        }

        if ($db->test('compta_comptes_bancaires', $db->where('id', $id)))
        {
            throw new UserException('Ce compte ne peut être supprimé car il est lié à un compte bancaire.');
        }

        $db->delete('compta_comptes', $db->where('id', $id));

        return true;
    }

    /**
     * Peut-on supprimer ce compte ? (OUI s'il n'a pas d'écriture liée)
     * @param  string $id Numéro du compte
     * @return boolean TRUE si le compte n'a pas d'écriture liée
     */
    public function canDelete($id)
    {
        $db = DB::getInstance();

        $id = trim($id);

        if ($db->firstColumn('SELECT 1 FROM compta_journal
                WHERE compte_debit = ? OR compte_credit = ? LIMIT 1;', $id, $id))
        {
            return false;
        }

        if ($db->test('compta_categories', $db->where('compte', $id)))
        {
            return false;
        }

        return true;
    }

    /**
     * Peut-on désactiver ce compte ? (OUI s'il n'a pas d'écriture liée dans l'exercice courant)
     * @param  string $id Numéro du compte
     * @return boolean TRUE si le compte n'a pas d'écriture liée dans l'exercice courant
     */
    public function canDisable($id, &$code = 0)
    {
        $db = DB::getInstance();

        $id = trim($id);

        if ($db->firstColumn('SELECT 1 FROM compta_journal
                WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1) 
                AND (compte_debit = ? OR compte_credit = ?) LIMIT 1;', $id, $id))
        {
            $code = 1;
            return false;
        }

        if ($db->test('compta_categories', $db->where('compte', $id)))
        {
            $code = 2;
            return false;
        }

        return true;
    }

    /**
     * Désactiver un compte
     * Le compte ne sera plus utilisable pour les écritures ou les catégories mais restera en base de données
     * @param  string $id Numéro du compte
     * @return boolean TRUE si la désactivation a fonctionné, une exception utilisateur si
     * la désactivation n'est pas possible.
     */
    public function disable($id)
    {
        $db = DB::getInstance();

        $id = trim($id);

        if (!$this->canDisable($id, $code))
        {
            if ($code === 1)
            {
                throw new UserException('Ce compte ne peut être désactivé car des écritures y sont liées sur l\'exercice courant. '
                    . 'Il faut supprimer ou ré-attribuer ces écritures avant de pouvoir supprimer le compte.');
            }
            else
            {
                throw new UserException('Ce compte ne peut être désactivé car des catégories y sont liées.');
            }
        }

        return $db->update('compta_comptes', ['desactive' => 1], $db->where('id', $id));
    }

    /**
     * Renvoie si un compte existe et n'est pas désactivé
     * @param  string  $id Numéro de compte
     * @return boolean     TRUE si le compte existe et n'est pas désactivé
     */
    public function isActive($id)
    {
        $db = DB::getInstance();
        return $db->test('compta_comptes', $db->where('id', trim($id)) . ' AND ' . $db->where('desactive', '!=', 1));
    }

    public function get($id)
    {
        $db = DB::getInstance();
        return $db->first('SELECT * FROM compta_comptes WHERE id = ?;', trim($id));
    }

    public function getList($parent = 0)
    {
        $db = DB::getInstance();
        return $db->getGrouped('SELECT id, * FROM compta_comptes WHERE parent = ? ORDER BY id;', $parent);
    }

    public function getListAll()
    {
        $db = DB::getInstance();
        return $db->getAssoc('SELECT id, libelle FROM compta_comptes ORDER BY id;');
    }

    public function listTree($parent_id = 0, $include_children = true)
    {
        $db = DB::getInstance();

        if ($include_children && $parent_id)
        {
            $where = $db->where('parent', 'LIKE', $parent_id . '%');
        }
        elseif ($include_children && !$parent_id)
        {
            $where = '1';
        }
        else
        {
            $where = $db->where('parent', !$parent_id ? (int) $parent_id : (string) $parent_id);
        }

        $query = 'SELECT * FROM compta_comptes WHERE %s OR %s ORDER BY id;';
        $query = sprintf($query, $db->where('id', (string) $parent_id), $where);

        return $db->get($query);
    }

    protected function _checkFields(&$data, $force_parent_check = false)
    {
        $db = DB::getInstance();

        if (empty($data['libelle']) || !trim($data['libelle']))
        {
            throw new UserException('Le libellé ne peut rester vide.');
        }

        $data['libelle'] = trim($data['libelle']);

        if (isset($data['id']))
        {
            $force_parent_check = true;
            $data['id'] = trim($data['id']);

            if ($db->test('compta_comptes', $db->where('id', $data['id'])))
            {
                throw new UserException('Le compte numéro '.$data['id'].' existe déjà.');
            }
        }

        if (isset($data['parent']) || $force_parent_check)
        {
            if (empty($data['parent']) && !trim($data['parent']))
            {
                throw new UserException('Le compte ne peut pas ne pas avoir de compte parent.');
            }

            if (!($id = $db->firstColumn('SELECT id FROM compta_comptes WHERE id = ?;', $data['parent'])))
            {
                throw new UserException('Le compte parent indiqué n\'existe pas.');
            }

            $data['parent'] = trim($id);
        }

        if (isset($data['id']))
        {
            if (strncmp($data['id'], $data['parent'], strlen($data['parent'])) !== 0)
            {
                throw new UserException('Le compte '.$data['id'].' n\'est pas un sous-compte de '.$data['parent'].'.');
            }
        }

        return true;
    }

    public function listSimpleTargetAccounts()
    {
        $accounts = DB::getInstance()->get('SELECT id, parent, label, b.label AS parent_label
            FROM acc_accounts a
            INNER JOIN acc_accounts b ON b.id = a.parent
            WHERE type != 0 ORDER BY type, parent, code;');

        $out = [];

        foreach ($accounts as $account) {
            if (!isset($out[$account->parent_label])) {
                $out[$account->parent_label] = [];
            }

            $out[$account->parent_label][$account->id] = $account->label;
        }

        return $out;
    }

    public function getPositions()
    {
        return [
            self::ACTIF     =>  'Actif',
            self::PASSIF    =>  'Passif',
            self::ACTIF | self::PASSIF      =>  'Actif ou passif (déterminé automatiquement au bilan selon le solde du compte)',
            self::CHARGE    =>  'Charge',
            self::PRODUIT   =>  'Produit',
            self::CHARGE | self::PRODUIT    =>  'Charge et produit',
        ];
    }
}