Exemplo n.º 1
0
 /**
  * Liste des comptes bancaires.
  *
  * @return Response
  */
 public function indexAction()
 {
     // Repositories
     $doctrine = $this->getDoctrine();
     $compteRepository = $doctrine->getRepository('ComptesBundle:Compte');
     $mouvementRepository = $doctrine->getRepository('ComptesBundle:Mouvement');
     // Tous les comptes
     $comptes = $compteRepository->findAll();
     // Tous les mouvements
     $mouvements = $mouvementRepository->findBy(array(), array('date' => 'ASC'));
     $firstMouvement = reset($mouvements) ?: null;
     $lastMouvement = end($mouvements) ?: null;
     // Versements initiaux, à prendre en compte pour le calcul du solde cumulé
     $versementsInitiaux = array();
     foreach ($comptes as $key => $compte) {
         $soldeInitial = $compte->getSoldeInitial();
         if ($soldeInitial > 0) {
             $compteID = $compte->getId();
             $dateOuverture = $compte->getDateOuverture();
             $versementsInitiaux[$compteID] = array('date' => $dateOuverture, 'montant' => $soldeInitial);
         }
     }
     // On intercale les versements initiaux sous forme de faux mouvements
     foreach ($mouvements as $key => $mouvement) {
         $date = $mouvement->getDate();
         $compte = $mouvement->getCompte();
         $compteID = $compte->getId();
         if (isset($versementsInitiaux[$compteID]) && $date >= $versementsInitiaux[$compteID]['date']) {
             $fakeMouvement = new Mouvement();
             $fakeMouvement->setCompte($compte);
             $fakeMouvement->setDate($versementsInitiaux[$compteID]['date']);
             $fakeMouvement->setMontant($versementsInitiaux[$compteID]['montant']);
             $fakeMouvement->setDescription("Versement initial");
             array_splice($mouvements, $key, 0, array($fakeMouvement));
             // Le versement initial a été pris en compte
             unset($versementsInitiaux[$compteID]);
         }
     }
     // Les faux mouvements peuvent avoir été intercalés au mauvais endroit
     usort($mouvements, function ($mouvementA, $mouvementB) {
         $dateA = $mouvementA->getDate();
         $dateB = $mouvementB->getDate();
         return $dateA > $dateB;
     });
     // Suppression des comptes fermés
     foreach ($comptes as $key => $compte) {
         $dateFermeture = $compte->getDateFermeture();
         if ($dateFermeture !== null) {
             unset($comptes[$key]);
         }
     }
     return $this->render('ComptesBundle:Compte:index.html.twig', array('comptes' => $comptes, 'mouvements' => $mouvements, 'first_mouvement' => $firstMouvement, 'last_mouvement' => $lastMouvement));
 }
 /**
  * Parse les mouvements et remplit les tableaux de classification du handler.
  *
  * @param \SplFileObject $file Fichier CSV fourni par le CIC.
  */
 public function parse(\SplFileObject $file)
 {
     // Repository
     $compteRepository = $this->em->getRepository('ComptesBundle:Compte');
     // Configuration du handler
     $configuration = $this->configuration['cic.csv'];
     // Le compte bancaire dans lequel importer les mouvements
     $compteID = $configuration['compte'];
     $compte = $compteRepository->find($compteID);
     // Lignes du fichier CSV qui représentent des mouvements
     $rows = array();
     // Les en-têtes de colonnes
     $headers = array('date_operation', 'date_valeur', 'debit', 'credit', 'libelle', 'solde');
     // Numéros de ligne
     $currentLine = 0;
     $headersLine = 0;
     while (($cols = $file->fgetcsv(';')) !== null) {
         // Si on a dépassé la ligne d'en-têtes
         if ($currentLine > $headersLine) {
             // Si la date est valide et sans month shifting
             $date = \DateTime::createFromFormat('d/m/Y', $cols[0]);
             $isValidDate = $date !== false && !array_sum($date->getLastErrors());
             // Alors la ligne en cours est un mouvement
             if ($isValidDate) {
                 $row = array_combine($headers, $cols);
                 $rows[] = $row;
             }
         }
         $currentLine++;
     }
     foreach ($rows as $row) {
         $mouvement = new Mouvement();
         // Date
         $date = \DateTime::createFromFormat('d/m/Y', (string) $row['date_operation']);
         $mouvement->setDate($date);
         // Compte
         $mouvement->setCompte($compte);
         // Montant
         $montant = $row['debit'] !== '' ? $row['debit'] : $row['credit'];
         $montant = str_replace(',', '.', $montant);
         $montant = sprintf('%0.2f', $montant);
         $mouvement->setMontant($montant);
         // Description
         $description = $row['libelle'];
         $mouvement->setDescription($description);
         // Classification
         $classification = $this->getClassification($mouvement);
         $this->classify($mouvement, $classification);
     }
 }
 /**
  * Parse les mouvements et remplit les tableaux de classification du handler.
  *
  * @param \SplFileObject $file Fichier Excel fourni par le CIC.
  */
 public function parse(\SplFileObject $file)
 {
     // Repository
     $compteRepository = $this->em->getRepository('ComptesBundle:Compte');
     // Configuration du handler
     $configuration = $this->configuration['cic.excel'];
     // Tableau de correspondance entre l'index de la feuille et le compte bancaire
     $comptesBySheets = array();
     foreach ($configuration['sheets'] as $sheetIndex => $compteID) {
         $comptesBySheets[$sheetIndex] = $compteRepository->find($compteID);
     }
     foreach ($comptesBySheets as $sheetIndex => $compte) {
         $reader = new ExcelReader($file, 4, $sheetIndex);
         foreach ($reader as $row) {
             // Arrivée à la fin du tableau des mouvements
             if ($row["Solde"] === null) {
                 break;
             }
             $mouvement = new Mouvement();
             // Date, Excel la stocke comme un integer. 0 = 01/01/1900, 25569 = 01/01/1970
             $date = new \DateTime();
             $daysSince1970 = $row["Opération"] - 25569;
             $timestamp = strtotime("+{$daysSince1970} days", 0);
             $date->setTimestamp($timestamp);
             $mouvement->setDate($date);
             // Compte
             $mouvement->setCompte($compte);
             // Montant
             $montant = $row["Débit"] !== null ? $row["Débit"] : $row["Crédit"];
             $montant = sprintf('%0.2f', $montant);
             $mouvement->setMontant($montant);
             // Description
             $description = $row["Libellé"];
             $mouvement->setDescription($description);
             // Classification
             $classification = $this->getClassification($mouvement);
             $this->classify($mouvement, $classification);
         }
     }
 }
 /**
  * {@inheritdoc}
  */
 protected function execute(InputInterface $input, OutputInterface $output)
 {
     $dialog = $this->getHelperSet()->get('dialog');
     $filename = $input->getArgument('filename');
     if (!file_exists($filename)) {
         throw new \Symfony\Component\HttpKernel\Exception\BadRequestHttpException("Le fichier {$filename} n'existe pas.");
     }
     $em = $this->getContainer()->get('doctrine')->getManager();
     $compteRepository = $em->getRepository('ComptesBundle:Compte');
     $lines = file($filename);
     // Indicateurs
     $i = 0;
     // Nombre de mouvements importés
     $balance = 0;
     // Balance des mouvements (crédit ou débit)
     // Numéro de compte
     $numeroCompte = null;
     foreach ($lines as $lineNumber => $line) {
         // Recherche la présence du numéro de compte, signalé par "€ N° ###########"
         preg_match('/€.+[N°|N˚]\\s(\\d{11})/', $line, $matches);
         if (isset($matches[1])) {
             $numeroCompteRaw = $matches[1];
             $numeroCompte = ltrim($numeroCompteRaw, "0");
         }
         // Si le numéro de compte n'est pas déterminé, la ligne ne sera pas exploitable
         if ($numeroCompte === null) {
             continue;
         }
         $compte = $compteRepository->findOneBy(array('numero' => $numeroCompte));
         if (!$compte) {
             throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException("Le compte n°{$numeroCompte} est inconnu.");
         }
         // Recherche la présence des dates d'opération et de valeur, format "00-00-0000 00-00-0000"
         preg_match('/(\\d{2}\\/\\d{2}\\/\\d{4})\\s{1}\\d{2}\\/\\d{2}\\/\\d{4}/', $line, $matches, PREG_OFFSET_CAPTURE);
         // S'il n'y en a pas, la ligne ne concerne pas un mouvement
         if (!isset($matches[1])) {
             continue;
         }
         // La date brute et sa position dans la ligne
         $dateRaw = $matches[1][0];
         $datePos = $matches[1][1];
         // L'objet DateTime, utilisable
         $date = \DateTime::createFromFormat('d/m/Y', $dateRaw);
         // La description démarre 22 caractères après la date
         $descriptionPos = $datePos + 22;
         // 22 => "00-00-0000 00-00-0000 "
         // Recherche de la description et du montant sur la ligne de la date (0) et les 4 du dessous
         $descriptionRows = array();
         $montant = null;
         for ($nextLineOffset = 0; $nextLineOffset <= 4; $nextLineOffset++) {
             $nextLineNumber = $lineNumber + $nextLineOffset;
             if (!isset($lines[$nextLineNumber])) {
                 break;
             }
             $nextLine = $lines[$nextLineNumber];
             // Les lignes suivantes ne peuvent contenir que des espaces avant la description
             if ($nextLineOffset > 0) {
                 $nextLineLength = strlen($nextLine);
                 if ($nextLineLength < $descriptionPos) {
                     break;
                 }
                 $spacesCount = substr_count($nextLine, " ", 0, $descriptionPos);
                 if ($spacesCount !== $descriptionPos) {
                     break;
                 }
             }
             // La description se termine lorsqu'au moins ~15 espaces sont rencontrés ou à la fin de la ligne
             $subject = substr($nextLine, $descriptionPos);
             preg_match('/(.+?)(?=\\s{15,}|$)/', $subject, $matches);
             // Si elle n'a pas été trouvée, on passe à la ligne suivante
             if (!isset($matches[1])) {
                 continue;
             }
             // La description brute
             $descriptionRaw = $matches[1];
             $descriptionRows[] = $descriptionRaw;
             /* Le montant n'est présent que sur une des lignes.
              * Donc s'il n'a pas encore été défini... */
             if ($montant === null) {
                 $descriptionLength = strlen($descriptionRaw);
                 // Il se trouve en fin de ligne, après une série d'espaces
                 $subject = substr($nextLine, $descriptionPos + $descriptionLength);
                 preg_match('/([^\\s]+.+)$/', $subject, $matches);
                 if (isset($matches[1])) {
                     $montantRaw = $matches[1];
                     $montant = str_replace('.', '', $montantRaw);
                     // Séparateur milliers
                     $montant = str_replace(',', '.', $montant);
                     // Séparateur décimales
                 }
             }
         }
         if (!$descriptionRows) {
             throw new \Exception("La description n'a pas été trouvée à la ligne n°{$lineNumber}.");
         } elseif ($montant === null) {
             throw new \Exception("Le montant n'a pas été trouvé à la ligne n°{$lineNumber}.");
         }
         $description = implode(" ", $descriptionRows);
         $mouvement = new Mouvement();
         $mouvement->setCompte($compte);
         $mouvement->setDate($date);
         $mouvement->setDescription($description);
         $mouvement->setMontant($montant);
         $output->writeln("<comment>{$compte} {$mouvement}</comment>");
         // Réponse obligatoire
         $signe = null;
         while (!in_array(strtolower($signe), array("c", "d"))) {
             $signe = $dialog->ask($output, "<question>S'agit-il d'un crédit ou d'un débit (c/D) ?</question>", "d");
         }
         if (strtolower($signe) === "d") {
             // Réponse insensible à la casse
             $montant = -$montant;
             $mouvement->setMontant($montant);
         }
         // Service de catégorisation automatique des mouvements
         $mouvementCategorizer = $this->getContainer()->get('comptes_bundle.mouvement.categorizer');
         $categories = $mouvementCategorizer->getCategories($mouvement);
         if ($categories) {
             $categorieKey = 0;
             // La clé de la catégorie au sein du tableau $categories
             // S'il y a plus d'une catégorie, on laisse le choix
             if (count($categories) > 1) {
                 $question = "<question>Proposition de catégories :\n";
                 foreach ($categories as $key => $categorie) {
                     $question .= "\t({$key}) : {$categorie}\n";
                 }
                 $question .= "\t(n) : Ne pas catégoriser\n";
                 $question .= "Quel est votre choix (0, 1, ..., n) ?</question>";
                 // Réponse obligatoire
                 $categorieKey = null;
                 while (strtolower($categorieKey) !== "n" && !isset($categories[$categorieKey])) {
                     $categorieKey = $dialog->ask($output, $question);
                 }
             }
             if (strtolower($categorieKey) !== "n") {
                 // Réponse insensible à la casse
                 $categorie = $categories[$categorieKey];
                 $mouvement->setCategorie($categorie);
             }
         }
         $em->persist($mouvement);
         $em->flush();
         // Indicateurs
         $i++;
         $balance += $montant;
     }
     $output->writeln("<info>{$i} mouvements importés pour une balance de {$balance}</info>");
 }
 /**
  * Ajoute un mouvement à la liste des mouvements parsés pour lesquels
  * une vérification manuelle est nécessaire.
  *
  * @param Mouvement $mouvement
  *
  * @return self
  */
 public function addWaitingMouvement(Mouvement $mouvement)
 {
     $hash = $mouvement->getHash();
     $this->waitingMouvements[$hash] = $mouvement;
     return $this;
 }