add bill categories

add webdav client

change pdf viewer
This commit is contained in:
Simon Vieille 2023-04-09 17:56:15 +02:00
parent cb4905b151
commit 688fdd49ee
Signed by: deblan
GPG key ID: 579388D585F70417
23 changed files with 646 additions and 36 deletions

5
.env
View file

@ -31,3 +31,8 @@ DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
# DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ###
WEBDAV_BILLING_SERVER=https://deblan.cloud
WEBDAV_BILLING_BASE_URL=/public.php/webdav/
WEBDAV_BILLING_USERNAME=
WEBDAV_BILLING_PASSWORD=

View file

@ -4,8 +4,10 @@ const AddressAutocomplete = require('./modules/address.js')
const FilesCollectionSorter = require('./modules/collection-sorter.js')
const Calendar = require('./modules/calendar.js')
const Masks = require('./modules/masks.js')
const PdfViewer = require('./modules/pdf-viewer.js')
new AddressAutocomplete()
new FilesCollectionSorter()
new Calendar()
new Masks()
new PdfViewer()

View file

@ -0,0 +1,17 @@
const viewer = require('pdfobject')
class PdfViewer {
constructor () {
this.showPdfs()
}
showPdfs () {
const elements = document.querySelectorAll('*[data-pdf]')
for (const element of elements) {
viewer.embed(element.getAttribute('data-pdf'), `#${element.getAttribute('id')}`)
}
}
}
module.exports = PdfViewer

View file

@ -10,7 +10,7 @@
"doctrine/orm": "2.11.*",
"knplabs/knp-snappy": "^1.4",
"murph/murph-core": "^1.18",
"sabre/dav": "^4.3"
"sabre/dav": "^4.4"
},
"require-dev": {
"symfony/browser-kit": "^5.4",

View file

@ -4,6 +4,10 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
webdav_billing_server: "%env(WEBDAV_BILLING_SERVER)%"
webdav_billing_base_url: "%env(WEBDAV_BILLING_BASE_URL)%"
webdav_billing_username: "%env(WEBDAV_BILLING_USERNAME)%"
webdav_billing_password: "%env(WEBDAV_BILLING_PASSWORD)%"
services:
# default configuration for services in *this* file
@ -65,5 +69,13 @@ services:
$privateKeyPath: '%kernel.project_dir%/config/secrets/prod.private.key'
$filesystem: '@filesystem'
App\Api\Webdav\Client:
public: true
arguments:
$server: '%webdav_billing_server%'
$baseUrl: '%webdav_billing_base_url%'
$username: '%webdav_billing_username%'
$password: '%webdav_billing_password%'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

View file

@ -11,6 +11,7 @@
"chunk": "^0.0.3",
"inputmask": "^5.0.8-beta.17",
"murph-project": "^1",
"pdfobject": "^2.2.8",
"vue-fragment": "^1.5.2"
}
}

73
src/Api/Webdav/Client.php Normal file
View file

@ -0,0 +1,73 @@
<?php
namespace App\Api\Webdav;
use Sabre\DAV\Client as BaseClient;
/**
* class Client.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class Client
{
protected BaseClient $client;
protected string $baseUrl;
public function __construct(string $server, string $baseUrl, ?string $username, ?string $password)
{
$settings = [
'baseUri' => $server,
'userName' => $username,
'password' => $password,
'authType' => 1,
];
$this->baseUrl = rtrim($baseUrl, '/');
$this->client = new BaseClient($settings);
}
public function sendFile(string $localFile, string $remoteFile): array
{
return $this->client->request(
'PUT',
$this->baseUrl.'/'.$remoteFile,
fopen($localFile, 'r')
);
}
public function mv(string $source, string $destination): array
{
return $this->client->request(
'MOVE',
$this->baseUrl.'/'.$source,
null,
[
'Destination' => $this->baseUrl.'/'.$destination,
]
);
}
public function mkdir(string $directory): array
{
return $this->client->request(
'MKCOL',
$this->baseUrl.'/'.$directory
);
}
public function rm(string $file): array
{
return $this->client->request(
'DELETE',
$this->baseUrl.'/'.$file
);
}
public function exists(string $file): bool
{
$response = $this->client->request('GET', $this->baseUrl.'/'.$file);
return 404 !== $response['statusCode'];
}
}

View file

@ -21,6 +21,8 @@ use Symfony\Component\Form\Form;
use function Symfony\Component\String\u;
use App\Repository\BillVendorRepositoryQuery;
use App\Factory\BillVendorFactory;
use App\Repository\BillCategoryRepositoryQuery;
use App\Factory\BillCategoryFactory;
class BillAdminController extends CrudController
{
@ -71,6 +73,8 @@ class BillAdminController extends CrudController
EntityManager $entityManager,
BillVendorRepositoryQuery $vendorQuery,
BillVendorFactory $vendorFactory,
BillCategoryRepositoryQuery $categoryQuery,
BillCategoryFactory $categoryFactory,
Request $request
): Response
{
@ -78,25 +82,39 @@ class BillAdminController extends CrudController
$entity,
$entityManager,
$request,
function(Entity $entity, Form $form, Request $request) use ($entityManager, $vendorQuery, $vendorFactory) {
$vendor = $form->get('vendor')->getData();
$customVendor = $form->get('customVendor')->getData();
function(Entity $entity, Form $form, Request $request) use (
$entityManager,
$vendorQuery,
$vendorFactory,
$categoryQuery,
$categoryFactory
) {
foreach ([
'vendor' => [$vendorQuery, $vendorFactory, true],
'category' => [$categoryQuery, $categoryFactory, false],
] as $key => $elements) {
$value = $form->get($key)->getData();
$customValue = $form->get(u('custom_'.$key)->camel())->getData();
if ($customVendor !== null) {
$customVendor = u($customVendor)->upper();
$vendor = $vendorQuery->create()
->where('.label = :label')
->setParameter(':label', $customVendor)
->findOne()
;
if ($customValue !== null) {
if ($elements[2]) {
$customValue = u($customValue)->upper();
}
if ($vendor === null) {
$vendor = $vendorFactory->create($customVendor);
$entityManager->create($vendor);
$value = $elements[0]->create()
->where('.label = :label')
->setParameter(':label', $customValue)
->findOne()
;
if ($value === null) {
$value = $elements[1]->create($customValue);
$entityManager->create($value);
}
}
}
$entity->setVendor($vendor);
$entity->{(string) u('set_'.$key)->camel()}($value);
}
}
);
}
@ -178,6 +196,12 @@ class BillAdminController extends CrudController
'button_attr' => ['style' => 'user-select: all'],
'sort' => ['vendor', '.vendor'],
])
->setField('index', 'Catégorie', Field\ButtonField::class, [
'property' => 'category',
'button_tag' => 'span',
'button_attr' => ['style' => 'user-select: all'],
'sort' => ['category', '.category'],
])
->setField('index', 'Date', Field\DateField::class, [
'property' => 'date',
'sort' => ['date', '.date'],

View file

@ -0,0 +1,129 @@
<?php
namespace App\Controller;
use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field;
use App\Core\Entity\EntityInterface;
use App\Core\Manager\EntityManager;
use App\Entity\BillCategory as Entity;
use App\Factory\BillCategoryFactory as Factory;
use App\Form\BillCategoryType as Type;
use App\Repository\BillCategoryRepositoryQuery as RepositoryQuery;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class BillCategoryAdminController extends CrudController
{
#[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])]
#[Route(path: "/admin/bill_category/{page}", name: "admin_bill_category_index", methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
#[Route(path: "/admin/bill_category/new", name: "admin_bill_category_new", methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
{
return $this->doNew($factory->create(), $entityManager, $request);
}
#[Route(path: "/admin/bill_category/show/{entity}", name: "admin_bill_category_show", methods: ['GET'])]
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
#[Route(path: "/admin/bill_category/filter", name: "admin_bill_category_filter", methods: ['GET'])]
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
#[Route(path: "/admin/bill_category/edit/{entity}", name: "admin_bill_category_edit", methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
#[Route(path: "/admin/bill_category/sort/{page}", name: "admin_bill_category_sort", methods: ['POST'], requirements: ['page' => '\d+'])]
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doSort($page, $query, $entityManager, $request, $session);
}
#[Route(path: "/admin/bill_category/batch/{page}", name: "admin_bill_category_batch", methods: ['POST'], requirements: ['page' => '\d+'])]
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doBatch($page, $query, $entityManager, $request, $session);
}
#[Route(path: "/admin/bill_category/delete/{entity}", name: "admin_bill_category_delete", methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);
}
protected function getConfiguration(): CrudConfiguration
{
return CrudConfiguration::create()
->setPageTitle('index', 'Catégories des factures')
->setPageTitle('edit', '{label}')
->setPageTitle('new', 'Nouvelle catégorie')
->setPageTitle('show', '{label}')
->setPageRoute('index', 'admin_bill_category_index')
->setPageRoute('new', 'admin_bill_category_new')
->setPageRoute('edit', 'admin_bill_category_edit')
->setPageRoute('show', 'admin_bill_category_show')
->setPageRoute('sort', 'admin_bill_category_sort')
->setPageRoute('batch', 'admin_bill_category_batch')
->setPageRoute('delete', 'admin_bill_category_delete')
->setPageRoute('filter', 'admin_bill_category_filter')
->setForm('edit', Type::class, [])
->setForm('new', Type::class)
// ->setForm('filter', Type::class)
// ->setMaxPerPage('index', 20)
// ->setIsSortableCollection('index', false)
// ->setSortableCollectionProperty('sortOrder')
// ->setAction('index', 'new', true)
// ->setAction('index', 'show', true)
// ->setAction('index', 'edit', true)
// ->setAction('index', 'delete', true)
// ->setAction('edit', 'back', true)
// ->setAction('edit', 'show', true)
// ->setAction('edit', 'delete', true)
// ->setAction('show', 'back', true)
// ->setAction('show', 'edit', true)
->setDefaultSort('index', 'label', 'ASC')
->setField('index', 'Libellé', Field\TextField::class, [
'property' => 'label',
'sort' => ['label', '.label'],
])
// ->setField('index', 'Foo', Field\TextField::class, [
// 'property' => 'foo',
// ])
// ->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
// $manager->delete($entity);
// })
;
}
protected function getSection(): string
{
return 'bill_category';
}
}

View file

@ -6,8 +6,10 @@ use App\Repository\BillRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface;
use App\Core\Doctrine\Timestampable;
#[ORM\Entity(repositoryClass: BillRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Bill implements EntityInterface
{
#[ORM\Id]
@ -40,6 +42,12 @@ class Bill implements EntityInterface
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?BillVendor $vendor = null;
#[ORM\ManyToOne(inversedBy: 'bills')]
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?BillCategory $category = null;
use Timestampable;
public function getId(): ?int
{
return $this->id;
@ -145,4 +153,47 @@ class Bill implements EntityInterface
return $this;
}
public function getCategory(): ?BillCategory
{
return $this->category;
}
public function setCategory(?BillCategory $category): self
{
$this->category = $category;
return $this;
}
public function isPdf(): bool
{
return (bool) preg_match('/\.pdf/', $this->getFile());
}
public function getRemoteFile(): ?string
{
if (!$this->getFile()) {
return null;
}
if (!$this->getDate()) {
return null;
}
if (!$this->getCategory()) {
return null;
}
if (!$this->getVendor()) {
return null;
}
return implode('/', [
$this->getDate()->format('Y'),
$this->getCategory()->getLabel(),
$this->getVendor()->getLabel(),
basename($this->getFile()),
]);
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace App\Entity;
use App\Repository\BillCategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface;
use App\Core\Doctrine\Timestampable;
#[ORM\Entity(repositoryClass: BillCategoryRepository::class)]
#[ORM\HasLifecycleCallbacks]
class BillCategory implements EntityInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $label = null;
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Bill::class)]
private Collection $bills;
use Timestampable;
public function __construct()
{
$this->bills = new ArrayCollection();
}
public function __toString()
{
return (string) $this->getLabel();
}
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
/**
* @return Collection<int, Bill>
*/
public function getBills(): Collection
{
return $this->bills;
}
public function addBill(Bill $bill): self
{
if (!$this->bills->contains($bill)) {
$this->bills->add($bill);
$bill->setCategory($this);
}
return $this;
}
public function removeBill(Bill $bill): self
{
if ($this->bills->removeElement($bill)) {
// set the owning side to null (unless already changed)
if ($bill->getCategory() === $this) {
$bill->setCategory(null);
}
}
return $this;
}
}

View file

@ -7,8 +7,10 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface;
use App\Core\Doctrine\Timestampable;
#[ORM\Entity(repositoryClass: BillVendorRepository::class)]
#[ORM\HasLifecycleCallbacks]
class BillVendor implements EntityInterface
{
#[ORM\Id]
@ -22,6 +24,8 @@ class BillVendor implements EntityInterface
#[ORM\OneToMany(mappedBy: 'vendor', targetEntity: Bill::class)]
private Collection $bills;
use Timestampable;
public function __construct()
{
$this->bills = new ArrayCollection();

View file

@ -0,0 +1,17 @@
<?php
namespace App\Factory;
use App\Core\Factory\FactoryInterface;
use App\Entity\BillCategory as Entity;
class BillCategoryFactory implements FactoryInterface
{
public function create(?string $label = null): Entity
{
$entity = new Entity();
$entity->setLabel((string) $label);
return $entity;
}
}

View file

@ -7,10 +7,10 @@ use App\Entity\BillVendor as Entity;
class BillVendorFactory implements FactoryInterface
{
public function create(string $label): Entity
public function create(?string $label = null): Entity
{
$entity = new Entity();
$entity->setLabel($label);
$entity->setLabel((string) $label);
return $entity;
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Form;
use App\Entity\BillCategory;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BillCategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => BillCategory::class,
]);
}
}

View file

@ -15,6 +15,7 @@ use App\Entity\BillPeer;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use App\Entity\BillVendor;
use Doctrine\ORM\EntityRepository;
use App\Entity\BillCategory;
class BillFilterType extends AbstractType
{
@ -42,6 +43,19 @@ class BillFilterType extends AbstractType
'data-jschoice' => '',
],
])
->add('category', EntityType::class, [
'label' => 'Catégorie',
'required' => false,
'class' => BillCategory::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('v')
->orderBy('v.label', 'ASC')
;
},
'attr' => [
'data-jschoice' => '',
],
])
->add('status', ChoiceType::class, [
'choices' => BillPeer::choices(),
'required' => false,

View file

@ -15,6 +15,7 @@ use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use App\Entity\BillPeer;
use App\Entity\BillCategory;
class BillType extends AbstractType
{
@ -64,6 +65,30 @@ class BillType extends AbstractType
'required' => false,
'mapped' => false,
])
->add('category', EntityType::class, [
'label' => 'Catégorie',
'required' => false,
'class' => BillCategory::class,
'label_attr' => [
'class' => 'mt-3',
],
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('v')
->orderBy('v.label', 'ASC')
;
},
'attr' => [
'data-jschoice' => '',
],
])
->add('customCategory', null, [
'label' => 'Ajouter une catégorie',
'label_attr' => [
'class' => 'font-weight-normal',
],
'required' => false,
'mapped' => false,
])
->add('date', null, [
'html5' => true,
'widget' => 'single_text',
@ -79,13 +104,21 @@ class BillType extends AbstractType
->add('amountTtc', NumberType::class, [
'label' => 'Montant TTC',
'html5' => true,
'scale' => 2,
'label_attr' => [
'class' => 'mt-3',
],
'attr' => [
'step' => 0.01,
],
])
->add('amountHt', NumberType::class, [
'label' => 'Montant HT',
'scale' => 2,
'html5' => true,
'attr' => [
'step' => 0.01,
],
])
->add('status', ChoiceType::class, [
'choices' => BillPeer::choices(),

View file

@ -0,0 +1,66 @@
<?php
namespace App\Repository;
use App\Entity\BillCategory;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<BillCategory>
*
* @method BillCategory|null find($id, $lockMode = null, $lockVersion = null)
* @method BillCategory|null findOneBy(array $criteria, array $orderBy = null)
* @method BillCategory[] findAll()
* @method BillCategory[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class BillCategoryRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, BillCategory::class);
}
public function save(BillCategory $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(BillCategory $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return BillCategory[] Returns an array of BillCategory objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('b.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?BillCategory
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Repository;
use App\Core\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
use App\Repository\BillCategoryRepository as Repository;
class BillCategoryRepositoryQuery extends RepositoryQuery
{
public function __construct(Repository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'b', $paginator);
}
}

View file

@ -1,6 +1,6 @@
{% if entity.id %}
<div class="row">
<div class="col-md-3">
<div class="col-md-4">
{% for item in form %}
{% if configuration.isI18n and item.vars.name == 'translations' %}
{% include(configuration.view('form_translations', '@Core/admin/crud/_form_translations.html.twig')) with {form: item} %}
@ -9,9 +9,20 @@
{% endif %}
{% endfor %}
</div>
<div class="col-md-9 pl-5">
<iframe src = "{{ asset('vendor/viewerjs/index.html#' ~ asset(entity.file)) }}" style="width: 100%; height: calc(100vh * 3 / 4)" u webkitallowfullscreen>
</iframe>
<div class="col-md-8 pl-5">
{% if entity.isPdf %}
<div data-pdf="{{ asset(entity.file) }}" style="height: calc(100vh / 3 * 2)" id="pdf"></div>
{% else %}
<div class="form-filepicker">
<div class="card">
<div class="card-img-top bg-tiles text-center">
<a href="{{ asset(entity.file) }}" target="_blank">
<img src="{{ asset(entity.file) }}" class="img-fluid">
</a>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% else %}

View file

@ -1,6 +1,7 @@
{% set values = {
'Référence': entity.reference,
'Fournisseur': entity.vendor,
'Catégorie': entity.category,
'Montant TTC': entity.amountTtc is not null ? (entity.amountTtc|number_format(2, ',') ~ ' €') : '/',
'Montant HT': entity.amountHt is not null ? (entity.amountHt|number_format(2, ',') ~ ' €') : '/',
'Status': entity.humanStatus,
@ -19,8 +20,19 @@
</div>
</div>
<div class="col-md-8 pl-3">
<iframe src = "{{ asset('vendor/viewerjs/index.html#' ~ asset(entity.file)) }}" style="width: 100%; height: calc(100vh * 2 / 3)" u webkitallowfullscreen>
</iframe>
{% if entity.isPdf %}
<div data-pdf="{{ asset(entity.file) }}" style="height: calc(100vh / 3 * 2)" id="pdf"></div>
{% else %}
<div class="form-filepicker">
<div class="card">
<div class="card-img-top bg-tiles text-center">
<a href="{{ asset(entity.file) }}" target="_blank">
<img src="{{ asset(entity.file) }}" class="img-fluid">
</a>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>

View file

@ -66,19 +66,28 @@
{{ include('@Core/admin/module/_menu_section.html.twig', {label: 'Comptabilité'}) }}
<ul class="nav flex-column">
{{ include('@Core/admin/module/_menu_item.html.twig', {
id: 'bill',
label: 'Factures',
route: path('admin_bill_index'),
icon: 'fa fa-file'
}) }}
{% if is_granted('ROLE_TREASURER') %}
{{ include('@Core/admin/module/_menu_item.html.twig', {
id: 'bill',
label: 'Factures',
route: path('admin_bill_index'),
icon: 'fa fa-file'
}) }}
{{ include('@Core/admin/module/_menu_item.html.twig', {
id: 'bill_vendor',
label: 'Fournisseurs',
route: path('admin_bill_vendor_index'),
icon: 'fa fa-store'
}) }}
{{ include('@Core/admin/module/_menu_item.html.twig', {
id: 'bill_vendor',
label: 'Fournisseurs',
route: path('admin_bill_vendor_index'),
icon: 'fa fa-store'
}) }}
{{ include('@Core/admin/module/_menu_item.html.twig', {
id: 'bill_category',
label: 'Catégories',
route: path('admin_bill_category_index'),
icon: 'fa fa-folder'
}) }}
{% endif %}
{{ include('@Core/admin/module/_menu_item.html.twig', {
id: 'expense_report',

View file

@ -6429,6 +6429,11 @@ path-type@^2.0.0:
dependencies:
pify "^2.0.0"
pdfobject@^2.2.8:
version "2.2.8"
resolved "https://registry.yarnpkg.com/pdfobject/-/pdfobject-2.2.8.tgz#956c8ce254883cdbc7c3cbee3d74d5a017f98d0b"
integrity sha512-dB/soWNMLtVGHfXERXnAtsKm0XwC6lyGVYegQcZxL4rw07rNOKvawc9kddBzlGr7TbiBZuGf4Drb3kyRbTf/QA==
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"