add status update with mail
This commit is contained in:
parent
ffca1b4dc4
commit
732f2dab51
|
@ -47,6 +47,7 @@ security:
|
||||||
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: ^/resetting, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
- { path: ^/resetting, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||||
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
|
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
|
||||||
|
- { path: ^/admin/bill, roles: [ROLE_MANAGER, ROLE_TREASURER] }
|
||||||
- { path: ^/admin/user, roles: ROLE_ADMIN }
|
- { path: ^/admin/user, roles: ROLE_ADMIN }
|
||||||
- { path: ^/admin/task, roles: ROLE_ADMIN }
|
- { path: ^/admin/task, roles: ROLE_ADMIN }
|
||||||
- { path: ^/admin/setting, roles: ROLE_ADMIN }
|
- { path: ^/admin/setting, roles: ROLE_ADMIN }
|
||||||
|
|
|
@ -23,6 +23,8 @@ use App\Repository\BillVendorRepositoryQuery;
|
||||||
use App\Factory\BillVendorFactory;
|
use App\Factory\BillVendorFactory;
|
||||||
use App\Repository\BillCategoryRepositoryQuery;
|
use App\Repository\BillCategoryRepositoryQuery;
|
||||||
use App\Factory\BillCategoryFactory;
|
use App\Factory\BillCategoryFactory;
|
||||||
|
use App\Event\EntityManagerEvent;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
class BillAdminController extends CrudController
|
class BillAdminController extends CrudController
|
||||||
{
|
{
|
||||||
|
@ -119,6 +121,27 @@ class BillAdminController extends CrudController
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: "/admin/bill/update_status/{entity}/{status}", name: "admin_bill_update_status", methods: ['GET'])]
|
||||||
|
public function updateStatus(Entity $entity, int $status, EntityManager $entityManager, Request $request, EventDispatcherInterface $eventDispatcher): Response
|
||||||
|
{
|
||||||
|
if (!$this->isCsrfTokenValid('update_status', $request->query->get('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity->setStatus($status);
|
||||||
|
$entityManager->update($entity);
|
||||||
|
|
||||||
|
$eventDispatcher->dispatch(new EntityManagerEvent($entity, [
|
||||||
|
'user' => $this->getUser(),
|
||||||
|
]), 'bill.status_updated');
|
||||||
|
|
||||||
|
$this->addFlash('success', 'The data has been saved.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_bill_show', [
|
||||||
|
'entity' => $entity->getId(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[Route(path: "/admin/bill/sort/{page}", name: "admin_bill_sort", methods: ['POST'], requirements: ['page' => '\d+'])]
|
#[Route(path: "/admin/bill/sort/{page}", name: "admin_bill_sort", methods: ['POST'], requirements: ['page' => '\d+'])]
|
||||||
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
|
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
|
||||||
{
|
{
|
||||||
|
@ -176,6 +199,8 @@ class BillAdminController extends CrudController
|
||||||
// ->setAction('show', 'edit', true)
|
// ->setAction('show', 'edit', true)
|
||||||
|
|
||||||
->setView('form', 'admin/bill/_form.html.twig')
|
->setView('form', 'admin/bill/_form.html.twig')
|
||||||
|
->setView('index', 'admin/bill/index.html.twig')
|
||||||
|
->setView('edit', 'admin/bill/edit.html.twig')
|
||||||
->setView('show_entity', 'admin/bill/_show.html.twig')
|
->setView('show_entity', 'admin/bill/_show.html.twig')
|
||||||
->setDefaultSort('index', 'id', 'desc')
|
->setDefaultSort('index', 'id', 'desc')
|
||||||
->setField('index', 'ID', Field\TextField::class, [
|
->setField('index', 'ID', Field\TextField::class, [
|
||||||
|
@ -214,7 +239,7 @@ class BillAdminController extends CrudController
|
||||||
])
|
])
|
||||||
->setField('index', 'Montant TTC', Field\TextField::class, [
|
->setField('index', 'Montant TTC', Field\TextField::class, [
|
||||||
'property_builder' => function(EntityInterface $entity) {
|
'property_builder' => function(EntityInterface $entity) {
|
||||||
if ($entity->getAmountTtc()) {
|
if ($entity->getAmountTtc() !== null) {
|
||||||
return sprintf('<span style="user-select: all">%01.2f</span> €', $entity->getAmountTtc());
|
return sprintf('<span style="user-select: all">%01.2f</span> €', $entity->getAmountTtc());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -224,7 +249,7 @@ class BillAdminController extends CrudController
|
||||||
])
|
])
|
||||||
->setField('index', 'Montant HT', Field\TextField::class, [
|
->setField('index', 'Montant HT', Field\TextField::class, [
|
||||||
'property_builder' => function(EntityInterface $entity) {
|
'property_builder' => function(EntityInterface $entity) {
|
||||||
if ($entity->getAmountHt()) {
|
if ($entity->getAmountHt() !== null) {
|
||||||
return sprintf('<span style="user-select: all">%01.2f</span> €', $entity->getAmountHt());
|
return sprintf('<span style="user-select: all">%01.2f</span> €', $entity->getAmountHt());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
126
src/EventSubscriber/BillEventSubscriber.php
Normal file
126
src/EventSubscriber/BillEventSubscriber.php
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Core\Entity\EntityInterface;
|
||||||
|
use App\Core\Event\EntityManager\EntityManagerEvent;
|
||||||
|
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
|
||||||
|
use App\Entity\Bill;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use App\Core\Slugify\Slugify;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use App\Core\Notification\MailNotifier;
|
||||||
|
use App\Repository\UserRepositoryQuery;
|
||||||
|
|
||||||
|
class BillEventSubscriber extends EntityManagerEventSubscriber
|
||||||
|
{
|
||||||
|
protected Slugify $slugger;
|
||||||
|
protected MailNotifier $notifier;
|
||||||
|
protected UrlGeneratorInterface $urlGenerator;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
Slugify $slugger,
|
||||||
|
MailNotifier $notifier,
|
||||||
|
UserRepositoryQuery $userQuery,
|
||||||
|
UrlGeneratorInterface $urlGenerator
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$this->slugger = $slugger;
|
||||||
|
$this->notifier = $notifier;
|
||||||
|
$this->userQuery = $userQuery;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return array_merge(parent::getSubscribedEvents(), [
|
||||||
|
'bill.status_updated' => ['onStatusUpdate', self::$priority],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(EntityInterface $entity)
|
||||||
|
{
|
||||||
|
return $entity instanceof Bill;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onStatusUpdate(EntityManagerEvent $event)
|
||||||
|
{
|
||||||
|
if (!$this->supports($event->getEntity())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->notifier
|
||||||
|
->setSubject('[Tinternet][Facture] '.$event->getEntity()->getHumanStatus())
|
||||||
|
->addRecipientsByUsers($this->userQuery
|
||||||
|
->where('.isTreasurer = true')
|
||||||
|
->orWhere('.isManager = true')
|
||||||
|
->find(),
|
||||||
|
true)
|
||||||
|
->notify('mail/bill/status_updated.html.twig', [
|
||||||
|
'entity' => $event->getEntity(),
|
||||||
|
'user' => $event->getParams()['user'],
|
||||||
|
'show_url' => $this->urlGenerator->generate(
|
||||||
|
'admin_bill_show',
|
||||||
|
[
|
||||||
|
'entity' => $event->getEntity()->getId(),
|
||||||
|
],
|
||||||
|
UrlGeneratorInterface::ABSOLUTE_URL
|
||||||
|
),
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onPreUpdate(EntityManagerEvent $event)
|
||||||
|
{
|
||||||
|
if (!$this->supports($event->getEntity())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity = $event->getEntity();
|
||||||
|
|
||||||
|
$splInfo = new \SplFileInfo($event->getEntity()->getFile());
|
||||||
|
|
||||||
|
$datas = [];
|
||||||
|
|
||||||
|
if ($entity->getDate()) {
|
||||||
|
$datas[] = $entity->getDate()->format('Ymd');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entity->getCategory()) {
|
||||||
|
$datas[] = $this->slugger->slugify($entity->getCategory()->getLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entity->getVendor()) {
|
||||||
|
$datas[] = $this->slugger->slugify($entity->getVendor()->getLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entity->getReference()) {
|
||||||
|
$datas[] = $entity->getReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
$datas[] = $entity->getId().'.'.$splInfo->getExtension();
|
||||||
|
|
||||||
|
$expectedFilename = implode('_', $datas);
|
||||||
|
$expectedPath = dirname($entity->getFile()).'/'.$expectedFilename;
|
||||||
|
|
||||||
|
if ($splInfo->getFilename() !== $expectedFilename) {
|
||||||
|
$fs = new Filesystem();
|
||||||
|
$fs->rename(
|
||||||
|
$event->getEntity()->getFile(),
|
||||||
|
$expectedPath
|
||||||
|
);
|
||||||
|
|
||||||
|
$event->getEntity()->setFile($expectedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onDelete(EntityManagerEvent $event)
|
||||||
|
{
|
||||||
|
if (!$this->supports($event->getEntity())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fs = new Filesystem();
|
||||||
|
$fs->remove($event->getEntity()->getFile());
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use App\Form\DateRangeType;
|
||||||
|
|
||||||
class BillFilterType extends AbstractType
|
class BillFilterType extends AbstractType
|
||||||
{
|
{
|
||||||
|
@ -51,9 +52,11 @@ class BillFilterType extends AbstractType
|
||||||
'data-jschoice' => '',
|
'data-jschoice' => '',
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->add('status', ChoiceType::class, [
|
->add('dateRange', DateRangeType::class, [
|
||||||
'choices' => BillPeer::choices(),
|
'label' => 'Date',
|
||||||
'required' => false,
|
'attr' => [
|
||||||
|
'class' => 'row',
|
||||||
|
],
|
||||||
])
|
])
|
||||||
->add('amountTtcRange', AmountRangeType::class, [
|
->add('amountTtcRange', AmountRangeType::class, [
|
||||||
'label' => 'Montant TTC',
|
'label' => 'Montant TTC',
|
||||||
|
@ -67,6 +70,10 @@ class BillFilterType extends AbstractType
|
||||||
'class' => 'row',
|
'class' => 'row',
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
->add('status', ChoiceType::class, [
|
||||||
|
'choices' => BillPeer::choices(),
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,6 +122,7 @@ class BillType extends AbstractType
|
||||||
])
|
])
|
||||||
->add('status', ChoiceType::class, [
|
->add('status', ChoiceType::class, [
|
||||||
'choices' => BillPeer::choices(),
|
'choices' => BillPeer::choices(),
|
||||||
|
'help' => 'Modifier cette valeur manuellement ne permet pas d\'envoyer des notifications par mail',
|
||||||
'label_attr' => [
|
'label_attr' => [
|
||||||
'class' => 'mt-3',
|
'class' => 'mt-3',
|
||||||
],
|
],
|
||||||
|
|
49
src/Form/DateRangeType.php
Normal file
49
src/Form/DateRangeType.php
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||||
|
|
||||||
|
class DateRangeType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('min', DateType::class, [
|
||||||
|
'label' => 'Min',
|
||||||
|
'html5' => true,
|
||||||
|
'widget' => 'single_text',
|
||||||
|
'required' => false,
|
||||||
|
'row_attr' => [
|
||||||
|
'class' => 'col-md-6 pr-1',
|
||||||
|
],
|
||||||
|
'label_attr' => [
|
||||||
|
'class' => 'font-weight-normal',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('max', DateType::class, [
|
||||||
|
'label' => 'Max',
|
||||||
|
'html5' => true,
|
||||||
|
'widget' => 'single_text',
|
||||||
|
'required' => false,
|
||||||
|
'row_attr' => [
|
||||||
|
'class' => 'col-md-6 pl-1',
|
||||||
|
],
|
||||||
|
'label_attr' => [
|
||||||
|
'class' => 'font-weight-normal',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,10 +44,26 @@ class BillRepositoryQuery extends RepositoryQuery
|
||||||
|
|
||||||
if (null !== $value['max']) {
|
if (null !== $value['max']) {
|
||||||
$this
|
$this
|
||||||
->andWhere('.amountHt <= :amountHtMin')
|
->andWhere('.amountHt <= :amountHtMax')
|
||||||
->setParameter('amountHtMax', $value['max'])
|
->setParameter('amountHtMax', $value['max'])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
} elseif ('dateRange' === $name) {
|
||||||
|
if (null !== $value['min']) {
|
||||||
|
$this
|
||||||
|
->andWhere('.date >= :dateMin')
|
||||||
|
->setParameter('dateMin', $value['min'])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $value['max']) {
|
||||||
|
$value['max']->add(new \DateInterval('PT'.(3600 * 24).'S'));
|
||||||
|
|
||||||
|
$this
|
||||||
|
->andWhere('.date <= :dateMax')
|
||||||
|
->setParameter('dateMax', $value['max'])
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
'Référence': entity.reference,
|
'Référence': entity.reference,
|
||||||
'Fournisseur': entity.vendor,
|
'Fournisseur': entity.vendor,
|
||||||
'Catégorie': entity.category,
|
'Catégorie': entity.category,
|
||||||
|
'Date': entity.date ? entity.date.format('d/m/Y') : '/',
|
||||||
|
'Date limite de paiement': entity.paymentDeadlineDate ? entity.paymentDeadlineDate.format('d/m/Y') : '/',
|
||||||
|
'Catégorie': entity.category,
|
||||||
'Montant TTC': entity.amountTtc is not null ? (entity.amountTtc|number_format(2, ',') ~ ' €') : '/',
|
'Montant TTC': entity.amountTtc is not null ? (entity.amountTtc|number_format(2, ',') ~ ' €') : '/',
|
||||||
'Montant HT': entity.amountHt is not null ? (entity.amountHt|number_format(2, ',') ~ ' €') : '/',
|
'Montant HT': entity.amountHt is not null ? (entity.amountHt|number_format(2, ',') ~ ' €') : '/',
|
||||||
'Status': entity.humanStatus,
|
'Status': entity.humanStatus,
|
||||||
|
@ -14,7 +17,7 @@
|
||||||
{% for label, value in values %}
|
{% for label, value in values %}
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<div class="font-weight-bold">{{ label|trans }}</div>
|
<div class="font-weight-bold">{{ label|trans }}</div>
|
||||||
<div class="mb-3">{{ value }}</div>
|
<div>{{ value }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
15
templates/admin/bill/edit.html.twig
Normal file
15
templates/admin/bill/edit.html.twig
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends '@Core/admin/crud/edit.html.twig' %}
|
||||||
|
|
||||||
|
{% block header_actions_after %}
|
||||||
|
{% if entity.status == 0 %}
|
||||||
|
<a href="{{ path('admin_bill_update_status', {entity: entity.id, status: 1, _token: csrf_token('update_status')}) }}" class="btn btn-success">
|
||||||
|
Demander le paiement
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entity.status == 1 %}
|
||||||
|
<a href="{{ path('admin_bill_update_status', {entity: entity.id, status: 2, _token: csrf_token('update_status')}) }}" class="btn btn-success">
|
||||||
|
Définir comme payée
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
7
templates/admin/bill/index.html.twig
Normal file
7
templates/admin/bill/index.html.twig
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends '@Core/admin/crud/index.html.twig' %}
|
||||||
|
|
||||||
|
{% block list_item_actions_before %}
|
||||||
|
<a href="{{ asset(item.file) }}" target="_blank" class="btn btn-sm btn-warning mr-1">
|
||||||
|
<span class="fa fa-download"></span>
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
|
@ -66,7 +66,7 @@
|
||||||
{{ include('@Core/admin/module/_menu_section.html.twig', {label: 'Comptabilité'}) }}
|
{{ include('@Core/admin/module/_menu_section.html.twig', {label: 'Comptabilité'}) }}
|
||||||
|
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
{% if is_granted('ROLE_TREASURER') %}
|
{% if is_granted('ROLE_MANAGER') or is_granted('ROLE_TREASURER') %}
|
||||||
{{ include('@Core/admin/module/_menu_item.html.twig', {
|
{{ include('@Core/admin/module/_menu_item.html.twig', {
|
||||||
id: 'bill',
|
id: 'bill',
|
||||||
label: 'Factures',
|
label: 'Factures',
|
||||||
|
|
17
templates/mail/bill/status_updated.html.twig
Normal file
17
templates/mail/bill/status_updated.html.twig
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends '@Core/mail/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
a {color: #1ab5dc}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<p style="text-align: center; margin-bottom: 20px">
|
||||||
|
<img src="{{ absolute_url(asset('build/images/tinternet.png')) }}" alt="Tinternet & Cie" width="50">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="text-align: center; font-size: 20px">Le statut de la <a href="{{ show_url }}">facture #{{ entity.id }}</a> a été modifié ☺️</p>
|
||||||
|
|
||||||
|
<p style="text-align: center; font-size: 20px">Nouveau statut : <strong>{{ entity.humanStatus }}</strong></p>
|
||||||
|
{% endblock %}
|
Loading…
Reference in a new issue