/** * 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>"); }