fix: SonarQube - ComptaExportService split 24->14 methodes + DocuSeal constantes
ComptaHelperService (nouveau) : - 12 methodes extraites de ComptaExportService : resolvePeriod, exportResponse, getFactures, resolveCompteBanque, resolveLibelleBanque, resolveTrancheAge, resolveCustomerInfo, getServiceGroups, aggregateServiceGroup, resolveStatutRentabilite ComptaExportService : - 24 -> 14 methodes (sous la limite de 20) - Injection ComptaHelperService dans constructeur - Delegation des appels utilitaires vers helper DocuSealService : - PDF_BASE64_PREFIX constante (3 occurrences) - ROLE_FIRST_PARTY constante (3 occurrences) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Controller\Admin;
|
||||
use App\Entity\AdvertPayment;
|
||||
use App\Entity\Facture;
|
||||
use App\Service\ComptaExportService;
|
||||
use App\Service\ComptaHelperService;
|
||||
use App\Service\DocuSealService;
|
||||
use App\Service\MailerService;
|
||||
use App\Service\Pdf\ComptaPdf;
|
||||
@@ -31,6 +32,7 @@ class ComptabiliteController extends AbstractController
|
||||
#[Autowire(env: 'DOCUSEAL_URL')]
|
||||
private string $docuSealUrl,
|
||||
private ComptaExportService $exportService,
|
||||
private ComptaHelperService $helper,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -46,10 +48,10 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/export/journal-ventes', name: 'export_journal_ventes')]
|
||||
public function exportJournalVentes(Request $request): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
$format = $request->query->getString('format', 'csv');
|
||||
|
||||
return $this->exportService->exportResponse(
|
||||
return $this->helper->exportResponse(
|
||||
$this->exportService->buildJournalVentesData($from, $to),
|
||||
'journal_ventes_'.$from->format('Ymd').'_'.$to->format('Ymd'),
|
||||
$format,
|
||||
@@ -62,10 +64,10 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/export/grand-livre', name: 'export_grand_livre')]
|
||||
public function exportGrandLivre(Request $request): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
$format = $request->query->getString('format', 'csv');
|
||||
|
||||
return $this->exportService->exportResponse(
|
||||
return $this->helper->exportResponse(
|
||||
$this->exportService->buildGrandLivreData($from, $to),
|
||||
'grand_livre_clients_'.$from->format('Ymd').'_'.$to->format('Ymd'),
|
||||
$format,
|
||||
@@ -78,10 +80,10 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/export/fec', name: 'export_fec')]
|
||||
public function exportFec(Request $request): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
$format = $request->query->getString('format', 'csv');
|
||||
|
||||
return $this->exportService->exportResponse(
|
||||
return $this->helper->exportResponse(
|
||||
$this->exportService->buildFecData($from, $to),
|
||||
'FEC_'.$from->format('Ymd').'_'.$to->format('Ymd'),
|
||||
$format,
|
||||
@@ -96,7 +98,7 @@ class ComptabiliteController extends AbstractController
|
||||
{
|
||||
$format = $request->query->getString('format', 'csv');
|
||||
|
||||
return $this->exportService->exportResponse(
|
||||
return $this->helper->exportResponse(
|
||||
$this->exportService->buildBalanceAgeeData(),
|
||||
'balance_agee_'.(new \DateTimeImmutable())->format('Ymd'),
|
||||
$format,
|
||||
@@ -109,10 +111,10 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/export/reglements', name: 'export_reglements')]
|
||||
public function exportReglements(Request $request): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
$format = $request->query->getString('format', 'csv');
|
||||
|
||||
return $this->exportService->exportResponse(
|
||||
return $this->helper->exportResponse(
|
||||
$this->exportService->buildReglementsData($from, $to),
|
||||
'reglements_'.$from->format('Ymd').'_'.$to->format('Ymd'),
|
||||
$format,
|
||||
@@ -125,7 +127,7 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/export-pdf/{type}', name: 'export_pdf', requirements: ['type' => 'journal-ventes|grand-livre|fec|balance-agee|reglements|commissions-stripe|couts-services'])]
|
||||
public function exportPdf(string $type, Request $request): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
|
||||
$titleMap = $this->getExportTitleMap();
|
||||
|
||||
@@ -152,7 +154,7 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/export-pdf/{type}/sign', name: 'export_pdf_sign', requirements: ['type' => 'journal-ventes|grand-livre|fec|balance-agee|reglements|commissions-stripe|couts-services'])]
|
||||
public function exportPdfSign(string $type, Request $request, DocuSealService $docuSeal): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
|
||||
$titleMap = $this->getExportTitleMap();
|
||||
|
||||
@@ -244,10 +246,10 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/export/commissions-stripe', name: 'export_commissions_stripe')]
|
||||
public function exportCommissionsStripe(Request $request): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
$format = $request->query->getString('format', 'csv');
|
||||
|
||||
return $this->exportService->exportResponse(
|
||||
return $this->helper->exportResponse(
|
||||
$this->exportService->buildCommissionsStripeData($from, $to),
|
||||
'commissions_stripe_'.$from->format('Ymd').'_'.$to->format('Ymd'),
|
||||
$format,
|
||||
@@ -260,10 +262,10 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/export/couts-services', name: 'export_couts_services')]
|
||||
public function exportCoutsServices(Request $request): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
$format = $request->query->getString('format', 'csv');
|
||||
|
||||
return $this->exportService->exportResponse(
|
||||
return $this->helper->exportResponse(
|
||||
$this->exportService->buildCoutsServicesData($from, $to),
|
||||
'couts_services_'.$from->format('Ymd').'_'.$to->format('Ymd'),
|
||||
$format,
|
||||
@@ -278,7 +280,7 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/rapport-financier', name: 'rapport_financier')]
|
||||
public function rapportFinancier(Request $request): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
$periodFrom = $from->format(ComptaExportService::DATE_FORMAT_FR);
|
||||
$periodTo = $to->format(ComptaExportService::DATE_FORMAT_FR);
|
||||
|
||||
@@ -313,7 +315,7 @@ class ComptabiliteController extends AbstractController
|
||||
#[Route('/rapport-financier/sign', name: 'rapport_financier_sign')]
|
||||
public function rapportFinancierSign(Request $request, DocuSealService $docuSeal): Response
|
||||
{
|
||||
[$from, $to] = $this->exportService->resolvePeriod($request);
|
||||
[$from, $to] = $this->helper->resolvePeriod($request);
|
||||
$periodFrom = $from->format(ComptaExportService::DATE_FORMAT_FR);
|
||||
$periodTo = $to->format(ComptaExportService::DATE_FORMAT_FR);
|
||||
|
||||
|
||||
@@ -7,11 +7,6 @@ use App\Entity\Facture;
|
||||
use App\Entity\FacturePrestataire;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ComptaExportService
|
||||
{
|
||||
public const LABEL_JOURNAL_VENTES = 'Journal des ventes';
|
||||
@@ -20,7 +15,6 @@ class ComptaExportService
|
||||
public const DATE_FORMAT_FR = 'd/m/Y';
|
||||
private const DQL_BETWEEN_DATES = 'f.createdAt BETWEEN :from AND :to';
|
||||
private const DQL_IS_PAID = 'f.isPaid = true';
|
||||
private const LABEL_CLIENT_DELETED = 'Client supprime';
|
||||
private const PREFIX_FACTURE = 'Facture ';
|
||||
|
||||
public const SERVICE_COSTS = [
|
||||
@@ -39,6 +33,7 @@ class ComptaExportService
|
||||
private EntityManagerInterface $em,
|
||||
#[Autowire('%env(bool:TVA_ENABLED)%')]
|
||||
private bool $tvaEnabled,
|
||||
private ComptaHelperService $helper,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -67,12 +62,12 @@ class ComptaExportService
|
||||
public function buildJournalVentesData(\DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||
{
|
||||
$tvaEnabled = $this->tvaEnabled;
|
||||
$factures = $this->getFactures($from, $to);
|
||||
$factures = $this->helper->getFactures($from, $to);
|
||||
$rows = [];
|
||||
|
||||
foreach ($factures as $facture) {
|
||||
$customer = $facture->getCustomer();
|
||||
[$codeComptable, $raisonSociale] = $this->resolveCustomerInfo($customer);
|
||||
[$codeComptable, $raisonSociale] = $this->helper->resolveCustomerInfo($customer);
|
||||
|
||||
$row = [
|
||||
'JournalCode' => 'VE',
|
||||
@@ -115,13 +110,13 @@ class ComptaExportService
|
||||
$rows[] = $rowClient;
|
||||
|
||||
if ($facture->isPaid() && null !== $facture->getPaidAt()) {
|
||||
$compteBanque = $this->resolveCompteBanque($facture->getPaidMethod());
|
||||
$compteBanque = $this->helper->resolveCompteBanque($facture->getPaidMethod());
|
||||
$rowPay = $row;
|
||||
$rowPay['JournalCode'] = 'BQ';
|
||||
$rowPay['JournalLib'] = 'Journal de banque';
|
||||
$rowPay['EcritureDate'] = $facture->getPaidAt()->format('Y-m-d');
|
||||
$rowPay['CompteNum'] = $compteBanque;
|
||||
$rowPay['CompteLib'] = $this->resolveLibelleBanque($facture->getPaidMethod());
|
||||
$rowPay['CompteLib'] = $this->helper->resolveLibelleBanque($facture->getPaidMethod());
|
||||
$rowPay['EcritureLib'] = 'Reglement Facture '.$facture->getInvoiceNumber().' - '.$raisonSociale;
|
||||
$rowPay['Debit'] = $tvaEnabled ? $facture->getTotalTtc() : $facture->getTotalHt();
|
||||
$rowPay['Credit'] = '0.00';
|
||||
@@ -148,12 +143,12 @@ class ComptaExportService
|
||||
public function buildGrandLivreData(\DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||
{
|
||||
$tvaEnabled = $this->tvaEnabled;
|
||||
$factures = $this->getFactures($from, $to);
|
||||
$factures = $this->helper->getFactures($from, $to);
|
||||
$rows = [];
|
||||
|
||||
foreach ($factures as $facture) {
|
||||
$customer = $facture->getCustomer();
|
||||
[$codeComptable, $raisonSociale] = $this->resolveCustomerInfo($customer);
|
||||
[$codeComptable, $raisonSociale] = $this->helper->resolveCustomerInfo($customer);
|
||||
$montant = $tvaEnabled ? $facture->getTotalTtc() : $facture->getTotalHt();
|
||||
|
||||
$rows[] = [
|
||||
@@ -184,13 +179,13 @@ class ComptaExportService
|
||||
public function buildFecData(\DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||
{
|
||||
$tvaEnabled = $this->tvaEnabled;
|
||||
$factures = $this->getFactures($from, $to);
|
||||
$factures = $this->helper->getFactures($from, $to);
|
||||
$rows = [];
|
||||
$ecritureNum = 1;
|
||||
|
||||
foreach ($factures as $facture) {
|
||||
$customer = $facture->getCustomer();
|
||||
[$codeComptable, $raisonSociale] = $this->resolveCustomerInfo($customer);
|
||||
[$codeComptable, $raisonSociale] = $this->helper->resolveCustomerInfo($customer);
|
||||
$numEcriture = sprintf('VE%06d', $ecritureNum);
|
||||
|
||||
$rows[] = [
|
||||
@@ -285,7 +280,7 @@ class ComptaExportService
|
||||
$rows = [];
|
||||
foreach ($factures as $facture) {
|
||||
$customer = $facture->getCustomer();
|
||||
[$codeComptable, $raisonSociale] = $this->resolveCustomerInfo($customer);
|
||||
[$codeComptable, $raisonSociale] = $this->helper->resolveCustomerInfo($customer);
|
||||
$joursRetard = $facture->getCreatedAt()->diff($now)->days;
|
||||
|
||||
$rows[] = [
|
||||
@@ -297,7 +292,7 @@ class ComptaExportService
|
||||
'MontantTVA' => $tvaEnabled ? $facture->getTotalTva() : '0.00',
|
||||
'MontantTTC' => $tvaEnabled ? $facture->getTotalTtc() : $facture->getTotalHt(),
|
||||
'JoursRetard' => (string) $joursRetard,
|
||||
'Tranche' => $this->resolveTrancheAge($joursRetard),
|
||||
'Tranche' => $this->helper->resolveTrancheAge($joursRetard),
|
||||
'Statut' => $facture->getState(),
|
||||
];
|
||||
}
|
||||
@@ -326,7 +321,7 @@ class ComptaExportService
|
||||
$rows = [];
|
||||
foreach ($factures as $facture) {
|
||||
$customer = $facture->getCustomer();
|
||||
[$codeComptable, $raisonSociale] = $this->resolveCustomerInfo($customer);
|
||||
[$codeComptable, $raisonSociale] = $this->helper->resolveCustomerInfo($customer);
|
||||
|
||||
$rows[] = [
|
||||
'DateReglement' => $facture->getPaidAt()->format('Y-m-d'),
|
||||
@@ -337,7 +332,7 @@ class ComptaExportService
|
||||
'MontantTVA' => $tvaEnabled ? $facture->getTotalTva() : '0.00',
|
||||
'MontantTTC' => $tvaEnabled ? $facture->getTotalTtc() : $facture->getTotalHt(),
|
||||
'MethodePaiement' => $facture->getPaidMethod() ?? 'inconnu',
|
||||
'CompteBanque' => $this->resolveCompteBanque($facture->getPaidMethod()),
|
||||
'CompteBanque' => $this->helper->resolveCompteBanque($facture->getPaidMethod()),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -389,14 +384,14 @@ class ComptaExportService
|
||||
public function buildCoutsServicesData(\DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||
{
|
||||
$grouped = $this->groupFactureLinesByType($from, $to);
|
||||
$serviceGroups = $this->getServiceGroups();
|
||||
$serviceGroups = $this->helper->getServiceGroups();
|
||||
|
||||
$rows = [];
|
||||
$totalCout = 0.0;
|
||||
$totalCa = 0.0;
|
||||
|
||||
foreach ($serviceGroups as $group) {
|
||||
[$caHt, $cout] = $this->aggregateServiceGroup($group, $grouped);
|
||||
[$caHt, $cout] = $this->helper->aggregateServiceGroup($group, $grouped);
|
||||
$marge = $caHt - $cout;
|
||||
$totalCout += $cout;
|
||||
$totalCa += $caHt;
|
||||
@@ -406,7 +401,7 @@ class ComptaExportService
|
||||
'CA_HT' => number_format($caHt, 2, '.', ''),
|
||||
'Cout_Ecosplay' => number_format($cout, 2, '.', ''),
|
||||
'Marge' => number_format($marge, 2, '.', ''),
|
||||
'Statut' => $this->resolveStatutRentabilite($caHt, $marge),
|
||||
'Statut' => $this->helper->resolveStatutRentabilite($caHt, $marge),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -528,49 +523,14 @@ class ComptaExportService
|
||||
$grouped = $this->groupFactureLinesByTypeFromList($factures);
|
||||
$cost = 0.0;
|
||||
|
||||
foreach ($this->getServiceGroups() as $group) {
|
||||
[, $cout] = $this->aggregateServiceGroup($group, $grouped);
|
||||
foreach ($this->helper->getServiceGroups() as $group) {
|
||||
[, $cout] = $this->helper->aggregateServiceGroup($group, $grouped);
|
||||
$cost += $cout;
|
||||
}
|
||||
|
||||
return $cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{types: list<string>, name: string}>
|
||||
*/
|
||||
public function getServiceGroups(): array
|
||||
{
|
||||
return [
|
||||
'esite' => ['types' => ['website', 'hosting', 'maintenance'], 'name' => 'E-Site'],
|
||||
'esymail' => ['types' => ['esymail'], 'name' => 'E-Mail'],
|
||||
'ndd' => ['types' => ['ndd'], 'name' => 'Nom de domaine'],
|
||||
'other' => ['types' => ['other'], 'name' => 'Autre'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le CA HT et le cout pour un groupe de services.
|
||||
*
|
||||
* @param array<string, mixed> $group
|
||||
* @param array<string, array{ca_ht: float, lines: int}> $grouped
|
||||
*
|
||||
* @return array{float, float} [caHt, cout]
|
||||
*/
|
||||
public function aggregateServiceGroup(array $group, array $grouped): array
|
||||
{
|
||||
$caHt = 0.0;
|
||||
$cout = 0.0;
|
||||
|
||||
foreach ($group['types'] as $type) {
|
||||
$caHt += $grouped[$type]['ca_ht'];
|
||||
$config = self::SERVICE_COSTS[$type];
|
||||
$cout += $config['cout'] + (($config['cout_par_ligne'] ?? 0.0) * $grouped[$type]['lines']);
|
||||
}
|
||||
|
||||
return [$caHt, $cout];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute les lignes prestataires aux resultats et met a jour le cout total.
|
||||
*
|
||||
@@ -609,154 +569,4 @@ class ComptaExportService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resout le statut de rentabilite (evite les ternaires imbriques).
|
||||
*/
|
||||
private function resolveStatutRentabilite(float $caHt, float $marge): string
|
||||
{
|
||||
if ($caHt <= 0) {
|
||||
return 'Inactif';
|
||||
}
|
||||
|
||||
return $marge >= 0 ? 'Rentable' : 'Negatif';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Facture>
|
||||
*/
|
||||
public function getFactures(\DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||
{
|
||||
return $this->em->createQueryBuilder()
|
||||
->select('f')
|
||||
->from(Facture::class, 'f')
|
||||
->where(self::DQL_BETWEEN_DATES)
|
||||
->andWhere('f.state != :cancel')
|
||||
->setParameter('from', $from)
|
||||
->setParameter('to', $to)
|
||||
->setParameter('cancel', Facture::STATE_CANCEL)
|
||||
->orderBy('f.createdAt', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{\DateTimeImmutable, \DateTimeImmutable}
|
||||
*/
|
||||
public function resolvePeriod(Request $request): array
|
||||
{
|
||||
$period = $request->query->getString('period', 'current');
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
if ('custom' === $period) {
|
||||
$dateFrom = $request->query->getString('from', $now->format('Y-m-01'));
|
||||
$dateTo = $request->query->getString('to', $now->format('Y-m-d'));
|
||||
} elseif ('previous' === $period) {
|
||||
$prev = $now->modify('first day of last month');
|
||||
$dateFrom = $prev->format('Y-m-01');
|
||||
$dateTo = $prev->modify('last day of this month')->format('Y-m-d');
|
||||
} else {
|
||||
// current
|
||||
$dateFrom = $now->format('Y-m-01');
|
||||
$dateTo = $now->format('Y-m-d');
|
||||
}
|
||||
|
||||
return [
|
||||
new \DateTimeImmutable($dateFrom),
|
||||
new \DateTimeImmutable($dateTo.' 23:59:59'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, string>> $rows
|
||||
*/
|
||||
public function exportResponse(array $rows, string $filename, string $format): Response
|
||||
{
|
||||
if ('json' === $format) {
|
||||
return new JsonResponse($rows, 200, [
|
||||
'Content-Disposition' => 'attachment; filename="'.$filename.'.json"',
|
||||
]);
|
||||
}
|
||||
|
||||
// CSV compatible SAGE (separateur ;, encodage UTF-8 BOM)
|
||||
// @codeCoverageIgnoreStart
|
||||
$response = new StreamedResponse(function () use ($rows) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
|
||||
// BOM UTF-8 pour Excel/SAGE
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
|
||||
if (!empty($rows)) {
|
||||
// En-tetes
|
||||
fputcsv($handle, array_keys($rows[0]), ';');
|
||||
|
||||
// Donnees
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($handle, $row, ';');
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
});
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="'.$filename.'.csv"');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function resolveCompteBanque(?string $method): string
|
||||
{
|
||||
return match ($method) {
|
||||
'card' => '512100',
|
||||
'sepa_debit' => '512100',
|
||||
'paypal' => '512200',
|
||||
'klarna' => '512300',
|
||||
'transfer', 'virement' => '512000',
|
||||
default => '512000',
|
||||
};
|
||||
}
|
||||
|
||||
public function resolveLibelleBanque(?string $method): string
|
||||
{
|
||||
return match ($method) {
|
||||
'card' => 'Stripe CB',
|
||||
'sepa_debit' => 'Stripe SEPA',
|
||||
'paypal' => 'PayPal',
|
||||
'klarna' => 'Klarna',
|
||||
'transfer', 'virement' => 'Virement bancaire',
|
||||
default => 'Banque',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resout le code comptable et la raison sociale d'un client de facture.
|
||||
*
|
||||
* @return array{string, string} [codeComptable, raisonSociale]
|
||||
*/
|
||||
private function resolveCustomerInfo(?object $customer): array
|
||||
{
|
||||
$codeComptable = $customer?->getCodeComptable() ?? 'DIVERS';
|
||||
$raisonSociale = $customer?->getRaisonSociale()
|
||||
?? ($customer ? $customer->getFirstName().' '.$customer->getLastName() : self::LABEL_CLIENT_DELETED);
|
||||
|
||||
return [$codeComptable, $raisonSociale];
|
||||
}
|
||||
|
||||
public function resolveTrancheAge(int $jours): string
|
||||
{
|
||||
$tranches = [
|
||||
30 => '0-30 jours',
|
||||
60 => '31-60 jours',
|
||||
90 => '61-90 jours',
|
||||
];
|
||||
|
||||
foreach ($tranches as $limit => $label) {
|
||||
if ($jours <= $limit) {
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
|
||||
return '+90 jours';
|
||||
}
|
||||
}
|
||||
|
||||
195
src/Service/ComptaHelperService.php
Normal file
195
src/Service/ComptaHelperService.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Facture;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* Helpers utilitaires pour les exports comptables (periodes, formats, labels).
|
||||
*/
|
||||
class ComptaHelperService
|
||||
{
|
||||
public const LABEL_CLIENT_DELETED = 'Client supprime';
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{\DateTimeImmutable, \DateTimeImmutable}
|
||||
*/
|
||||
public function resolvePeriod(Request $request): array
|
||||
{
|
||||
$period = $request->query->getString('period', 'current');
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
if ('custom' === $period) {
|
||||
$dateFrom = $request->query->getString('from', $now->format('Y-m-01'));
|
||||
$dateTo = $request->query->getString('to', $now->format('Y-m-d'));
|
||||
} elseif ('previous' === $period) {
|
||||
$prev = $now->modify('first day of last month');
|
||||
$dateFrom = $prev->format('Y-m-01');
|
||||
$dateTo = $prev->modify('last day of this month')->format('Y-m-d');
|
||||
} else {
|
||||
$dateFrom = $now->format('Y-m-01');
|
||||
$dateTo = $now->format('Y-m-d');
|
||||
}
|
||||
|
||||
return [
|
||||
new \DateTimeImmutable($dateFrom),
|
||||
new \DateTimeImmutable($dateTo.' 23:59:59'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, string>> $rows
|
||||
*/
|
||||
public function exportResponse(array $rows, string $filename, string $format): Response
|
||||
{
|
||||
if ('json' === $format) {
|
||||
return new JsonResponse($rows, 200, [
|
||||
'Content-Disposition' => 'attachment; filename="'.$filename.'.json"',
|
||||
]);
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
$response = new StreamedResponse(function () use ($rows) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
|
||||
if (!empty($rows)) {
|
||||
fputcsv($handle, array_keys($rows[0]), ';');
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($handle, $row, ';');
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
});
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="'.$filename.'.csv"');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Facture>
|
||||
*/
|
||||
public function getFactures(\DateTimeImmutable $from, \DateTimeImmutable $to): array
|
||||
{
|
||||
return $this->em->createQueryBuilder()
|
||||
->select('f')
|
||||
->from(Facture::class, 'f')
|
||||
->where('f.createdAt BETWEEN :from AND :to')
|
||||
->andWhere('f.state != :cancel')
|
||||
->setParameter('from', $from)
|
||||
->setParameter('to', $to)
|
||||
->setParameter('cancel', Facture::STATE_CANCEL)
|
||||
->orderBy('f.createdAt', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function resolveCompteBanque(?string $method): string
|
||||
{
|
||||
return match ($method) {
|
||||
'card' => '512100',
|
||||
'sepa_debit' => '512100',
|
||||
'paypal' => '512200',
|
||||
'klarna' => '512300',
|
||||
'transfer', 'virement' => '512000',
|
||||
default => '512000',
|
||||
};
|
||||
}
|
||||
|
||||
public function resolveLibelleBanque(?string $method): string
|
||||
{
|
||||
return match ($method) {
|
||||
'card' => 'Stripe CB',
|
||||
'sepa_debit' => 'Stripe SEPA',
|
||||
'paypal' => 'PayPal',
|
||||
'klarna' => 'Klarna',
|
||||
'transfer', 'virement' => 'Virement bancaire',
|
||||
default => 'Banque',
|
||||
};
|
||||
}
|
||||
|
||||
public function resolveTrancheAge(int $jours): string
|
||||
{
|
||||
$tranches = [
|
||||
30 => '0-30 jours',
|
||||
60 => '31-60 jours',
|
||||
90 => '61-90 jours',
|
||||
];
|
||||
|
||||
foreach ($tranches as $limit => $label) {
|
||||
if ($jours <= $limit) {
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
|
||||
return '+90 jours';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string} [codeComptable, raisonSociale]
|
||||
*/
|
||||
public function resolveCustomerInfo(?object $customer): array
|
||||
{
|
||||
$codeComptable = $customer?->getCodeComptable() ?? 'DIVERS';
|
||||
$raisonSociale = $customer?->getRaisonSociale()
|
||||
?? ($customer ? $customer->getFirstName().' '.$customer->getLastName() : self::LABEL_CLIENT_DELETED);
|
||||
|
||||
return [$codeComptable, $raisonSociale];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{types: list<string>, name: string}>
|
||||
*/
|
||||
public function getServiceGroups(): array
|
||||
{
|
||||
return [
|
||||
'esite' => ['types' => ['website', 'hosting', 'maintenance'], 'name' => 'E-Site'],
|
||||
'esymail' => ['types' => ['esymail'], 'name' => 'E-Mail'],
|
||||
'ndd' => ['types' => ['ndd'], 'name' => 'Nom de domaine'],
|
||||
'other' => ['types' => ['other'], 'name' => 'Autre'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $group
|
||||
* @param array<string, array{ca_ht: float, lines: int}> $grouped
|
||||
*
|
||||
* @return array{float, float} [caHt, cout]
|
||||
*/
|
||||
public function aggregateServiceGroup(array $group, array $grouped): array
|
||||
{
|
||||
$caHt = 0.0;
|
||||
$cout = 0.0;
|
||||
|
||||
foreach ($group['types'] as $type) {
|
||||
$caHt += $grouped[$type]['ca_ht'];
|
||||
$config = ComptaExportService::SERVICE_COSTS[$type];
|
||||
$cout += $config['cout'] + (($config['cout_par_ligne'] ?? 0.0) * $grouped[$type]['lines']);
|
||||
}
|
||||
|
||||
return [$caHt, $cout];
|
||||
}
|
||||
|
||||
public function resolveStatutRentabilite(float $caHt, float $marge): string
|
||||
{
|
||||
if ($caHt <= 0) {
|
||||
return 'Inactif';
|
||||
}
|
||||
|
||||
return $marge >= 0 ? 'Rentable' : 'Negatif';
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class DocuSealService
|
||||
{
|
||||
private const PDF_BASE64_PREFIX = 'data:application/pdf;base64,';
|
||||
private const ROLE_FIRST_PARTY = 'First Party';
|
||||
|
||||
private Api $api;
|
||||
|
||||
public function __construct(
|
||||
@@ -48,14 +51,14 @@ class DocuSealService
|
||||
'documents' => [
|
||||
[
|
||||
'name' => 'attestation-'.$attestation->getReference().'.pdf',
|
||||
'file' => 'data:application/pdf;base64,'.$pdfBase64,
|
||||
'file' => self::PDF_BASE64_PREFIX.$pdfBase64,
|
||||
],
|
||||
],
|
||||
'submitters' => [
|
||||
[
|
||||
'email' => 'contact@e-cosplay.fr',
|
||||
'name' => 'Association E-Cosplay',
|
||||
'role' => 'First Party',
|
||||
'role' => self::ROLE_FIRST_PARTY,
|
||||
'completed' => true,
|
||||
'send_email' => false,
|
||||
'values' => [
|
||||
@@ -118,7 +121,7 @@ class DocuSealService
|
||||
$submitter = [
|
||||
'email' => $customer->getEmail(),
|
||||
'name' => $customer->getFullName(),
|
||||
'role' => 'First Party',
|
||||
'role' => self::ROLE_FIRST_PARTY,
|
||||
'send_email' => false,
|
||||
'metadata' => [
|
||||
'doc_type' => 'devis',
|
||||
@@ -138,7 +141,7 @@ class DocuSealService
|
||||
'documents' => [
|
||||
[
|
||||
'name' => 'devis-'.str_replace('/', '-', $numOrder).'.pdf',
|
||||
'file' => 'data:application/pdf;base64,'.$pdfBase64,
|
||||
'file' => self::PDF_BASE64_PREFIX.$pdfBase64,
|
||||
],
|
||||
],
|
||||
'submitters' => [$submitter],
|
||||
@@ -331,7 +334,7 @@ class DocuSealService
|
||||
$submitter = [
|
||||
'email' => $signerEmail,
|
||||
'name' => $signerName,
|
||||
'role' => 'First Party',
|
||||
'role' => self::ROLE_FIRST_PARTY,
|
||||
'send_email' => false,
|
||||
'metadata' => [
|
||||
'doc_type' => 'comptabilite',
|
||||
@@ -352,7 +355,7 @@ class DocuSealService
|
||||
'documents' => [
|
||||
[
|
||||
'name' => str_replace(' ', '_', $documentName).'.pdf',
|
||||
'file' => 'data:application/pdf;base64,'.$pdfBase64,
|
||||
'file' => self::PDF_BASE64_PREFIX.$pdfBase64,
|
||||
],
|
||||
],
|
||||
'submitters' => [$submitter],
|
||||
|
||||
@@ -61,14 +61,23 @@ class ComptabiliteControllerTest extends TestCase
|
||||
return $kernel;
|
||||
}
|
||||
|
||||
private function buildHelper(?EntityManagerInterface $em = null): \App\Service\ComptaHelperService
|
||||
{
|
||||
return new \App\Service\ComptaHelperService($em ?? $this->buildEmWithQueryBuilder());
|
||||
}
|
||||
|
||||
private function buildExportService(?EntityManagerInterface $em = null): ComptaExportService
|
||||
{
|
||||
return new ComptaExportService($em ?? $this->buildEmWithQueryBuilder(), false);
|
||||
$emInstance = $em ?? $this->buildEmWithQueryBuilder();
|
||||
|
||||
return new ComptaExportService($emInstance, false, new \App\Service\ComptaHelperService($emInstance));
|
||||
}
|
||||
|
||||
private function buildExportServiceWithTva(?EntityManagerInterface $em = null): ComptaExportService
|
||||
{
|
||||
return new ComptaExportService($em ?? $this->buildEmWithQueryBuilder(), true);
|
||||
$emInstance = $em ?? $this->buildEmWithQueryBuilder();
|
||||
|
||||
return new ComptaExportService($emInstance, true, new \App\Service\ComptaHelperService($emInstance));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,7 +89,7 @@ class ComptabiliteControllerTest extends TestCase
|
||||
$em = $this->buildEmWithQueryBuilder();
|
||||
$kernel = $this->buildKernel();
|
||||
$svc = $exportService ?? $this->buildExportService($em);
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc);
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
|
||||
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$stack = $this->createStub(RequestStack::class);
|
||||
@@ -131,7 +140,7 @@ class ComptabiliteControllerTest extends TestCase
|
||||
$kernel = $this->buildKernel();
|
||||
$svc = $exportService ?? $this->buildExportService($em);
|
||||
|
||||
$controller = new ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc);
|
||||
$controller = new ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
|
||||
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$stack = $this->createStub(RequestStack::class);
|
||||
@@ -553,8 +562,8 @@ class ComptabiliteControllerTest extends TestCase
|
||||
{
|
||||
$em = !empty($otherData) ? $this->buildEmWithData($emData, $otherData) : $this->buildEmWithData($emData);
|
||||
$kernel = $this->buildKernel();
|
||||
$svc = new ComptaExportService($em, false);
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc);
|
||||
$svc = new ComptaExportService($em, false, new \App\Service\ComptaHelperService($em));
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
|
||||
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$stack = $this->createStub(\Symfony\Component\HttpFoundation\RequestStack::class);
|
||||
@@ -593,8 +602,8 @@ class ComptabiliteControllerTest extends TestCase
|
||||
{
|
||||
$em = $this->buildEmWithData($emData);
|
||||
$kernel = $this->buildKernel();
|
||||
$svc = new ComptaExportService($em, true);
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc);
|
||||
$svc = new ComptaExportService($em, true, new \App\Service\ComptaHelperService($em));
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
|
||||
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$stack = $this->createStub(\Symfony\Component\HttpFoundation\RequestStack::class);
|
||||
@@ -782,8 +791,8 @@ class ComptabiliteControllerTest extends TestCase
|
||||
$em->method('createQueryBuilder')->willReturn($qb);
|
||||
|
||||
$kernel = $this->buildKernel();
|
||||
$svc = new ComptaExportService($em, false);
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc);
|
||||
$svc = new ComptaExportService($em, false, new \App\Service\ComptaHelperService($em));
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
|
||||
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$stack = $this->createStub(\Symfony\Component\HttpFoundation\RequestStack::class);
|
||||
@@ -1397,8 +1406,8 @@ class ComptabiliteControllerTest extends TestCase
|
||||
$em->method('createQueryBuilder')->willReturn($qb);
|
||||
|
||||
$kernel = $this->buildKernel();
|
||||
$svc = new ComptaExportService($em, false);
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc);
|
||||
$svc = new ComptaExportService($em, false, new \App\Service\ComptaHelperService($em));
|
||||
$controller = new \App\Controller\Admin\ComptabiliteController($em, $kernel, 'http://docuseal.example', $svc, $this->buildHelper($em));
|
||||
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$stack = $this->createStub(\Symfony\Component\HttpFoundation\RequestStack::class);
|
||||
|
||||
Reference in New Issue
Block a user