Merge branch 'feature/bills' into develop

This commit is contained in:
Simon Vieille 2023-04-10 17:53:05 +02:00
commit 3f24d77863
43 changed files with 2172 additions and 2 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

@ -1 +1 @@
8.0
8.1

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

@ -41,3 +41,4 @@ core:
# path_uri: "/uploads"
path_locked:
- "%kernel.project_dir%/public/uploads/Notes de frais"
- "%kernel.project_dir%/public/uploads/Factures"

View file

@ -47,6 +47,7 @@ security:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
- { path: ^/admin/bill, roles: [ROLE_MANAGER, ROLE_TREASURER] }
- { path: ^/admin/user, roles: ROLE_ADMIN }
- { path: ^/admin/task, roles: ROLE_ADMIN }
- { path: ^/admin/setting, roles: ROLE_ADMIN }

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"
}
}

1
public/vendor/viewerjs vendored Symbolic link
View file

@ -0,0 +1 @@
../../manual_vendor/viewerjs-0.5.8/ViewerJS

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

@ -0,0 +1,81 @@
<?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 ls(string $directory): array
{
return $this->client->propfind($this->baseUrl.'/'.$directory, [
'{DAV:}displayname',
'{DAV:}getcontentlength',
], 1);
}
public function exists(string $file): bool
{
$response = $this->client->request('GET', $this->baseUrl.'/'.$file);
return 404 !== $response['statusCode'];
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Command;
use App\Api\Webdav\Client;
use App\Repository\BillRepositoryQuery;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: 'webdav:bill:sync',
description: 'Sync bills',
)]
class WebdavBillSyncCommand extends Command
{
protected Client $client;
protected BillRepositoryQuery $query;
public function __construct(Client $client, BillRepositoryQuery $query)
{
$this->client = $client;
$this->query = $query;
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
chdir(__DIR__.'/../../public');
$remoteFiles = $this->client->ls('/');
$localFiles = [];
foreach ($this->query->find() as $entity) {
$localFiles[basename($entity->getFile())] = $entity->getFile();
}
foreach ($localFiles as $basename => $file) {
if (!$this->client->exists($basename)) {
$this->client->sendFile($file, $basename);
$output->writeln(sprintf(
'Fichier <comment>%s</comment> envoyé',
$file
));
}
}
$isFirst = true;
foreach ($remoteFiles as $remoteFile) {
$name = $remoteFile['{DAV:}displayname'];
if (!isset($localFiles[$name]) && !$isFirst) {
$this->client->rm($name);
$output->writeln(sprintf(
'Fichier distant <comment>%s</comment> supprimé',
$name
));
}
$isFirst = false;
}
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,284 @@
<?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\Bill as Entity;
use App\Factory\BillFactory as Factory;
use App\Form\BillType as Type;
use App\Form\BillFilterType as FilterType;
use App\Repository\BillRepositoryQuery as RepositoryQuery;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use App\Core\Form\FileUploadHandler;
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;
use App\Event\EntityManagerEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class BillAdminController extends CrudController
{
#[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])]
#[Route(path: "/admin/bill/{page}", name: "admin_bill_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/new", name: "admin_bill_new", methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request, FileUploadHandler $fileUpload): Response
{
return $this->doNew(
$factory->create(),
$entityManager,
$request,
function(Entity $entity, Form $form, Request $request) use ($fileUpload) {
$directory = 'uploads/Factures/';
$fileUpload->handleForm(
$form->get('file')->getData(),
$directory,
function ($filename) use ($entity, $directory) {
$entity->setFile($directory.'/'.$filename);
}
);
}
);
}
#[Route(path: "/admin/bill/show/{entity}", name: "admin_bill_show", methods: ['GET'])]
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
#[Route(path: "/admin/bill/filter", name: "admin_bill_filter", methods: ['GET'])]
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
#[Route(path: "/admin/bill/edit/{entity}", name: "admin_bill_edit", methods: ['GET', 'POST'])]
public function edit(
Entity $entity,
EntityManager $entityManager,
BillVendorRepositoryQuery $vendorQuery,
BillVendorFactory $vendorFactory,
BillCategoryRepositoryQuery $categoryQuery,
BillCategoryFactory $categoryFactory,
Request $request
): Response
{
return $this->doEdit(
$entity,
$entityManager,
$request,
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 ($customValue !== null) {
if ($elements[2]) {
$customValue = u($customValue)->upper();
}
$value = $elements[0]->create()
->where('.label = :label')
->setParameter(':label', $customValue)
->findOne()
;
if ($value === null) {
$value = $elements[1]->create($customValue);
$entityManager->create($value);
}
}
$entity->{(string) u('set_'.$key)->camel()}($value);
}
}
);
}
#[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+'])]
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/batch/{page}", name: "admin_bill_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/delete/{entity}", name: "admin_bill_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', 'Factures')
->setPageTitle('edit', 'Facture #{id}')
->setPageTitle('new', 'Nouvelle facture')
->setPageTitle('show', 'Facture #{id}')
->setPageRoute('index', 'admin_bill_index')
->setPageRoute('new', 'admin_bill_new')
->setPageRoute('edit', 'admin_bill_edit')
->setPageRoute('show', 'admin_bill_show')
->setPageRoute('sort', 'admin_bill_sort')
->setPageRoute('batch', 'admin_bill_batch')
->setPageRoute('delete', 'admin_bill_delete')
->setPageRoute('filter', 'admin_bill_filter')
->setForm('edit', Type::class, [])
->setForm('new', Type::class)
->setForm('filter', FilterType::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)
->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')
->setDefaultSort('index', 'id', 'desc')
->setField('index', 'ID', Field\TextField::class, [
'property_builder' => function (EntityInterface $entity) {
return sprintf('#%d', $entity->getId());
},
'sort' => ['id', '.id'],
])
->setField('index', 'Rérérence', Field\ButtonField::class, [
'property' => 'reference',
'button_tag' => 'span',
'button_attr' => ['style' => 'user-select: all'],
'sort' => ['reference', '.reference'],
])
->setField('index', 'Fournisseur', Field\ButtonField::class, [
'property' => 'vendor',
'button_tag' => 'span',
'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'],
'format' => 'd/m/Y',
])
->setField('index', 'Date limite de paiement', Field\DateField::class, [
'property' => 'paymentDeadlineDate',
'sort' => ['paymentDeadlineDate', '.paymentDeadlineDate'],
'format' => 'd/m/Y',
])
->setField('index', 'Montant TTC', Field\TextField::class, [
'property_builder' => function(EntityInterface $entity) {
if ($entity->getAmountTtc() !== null) {
return sprintf('<span style="user-select: all">%01.2f</span> €', $entity->getAmountTtc());
}
},
'raw' => true,
'sort' => ['amountTtc', '.amountTtc'],
'attr' => ['class' => 'text-right'],
])
->setField('index', 'Montant HT', Field\TextField::class, [
'property_builder' => function(EntityInterface $entity) {
if ($entity->getAmountHt() !== null) {
return sprintf('<span style="user-select: all">%01.2f</span> €', $entity->getAmountHt());
}
},
'raw' => true,
'sort' => ['amountHt', '.amountHt'],
'attr' => ['class' => 'text-right'],
])
->setField('index', 'Statut', Field\ButtonField::class, [
'property' => 'humanStatus',
'button_attr_builder' => function(EntityInterface $entity) {
$classes = [
0 => 'btn btn-sm btn-light',
1 => 'btn btn-sm btn-warning',
2 => 'btn btn-sm btn-success',
];
return ['class' => $classes[$entity->getStatus()]];
},
'sort' => ['status', '.status'],
])
// ->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
// $manager->delete($entity);
// })
;
}
protected function getSection(): string
{
return 'bill';
}
}

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

@ -0,0 +1,130 @@
<?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\BillVendor as Entity;
use App\Factory\BillVendorFactory as Factory;
use App\Form\BillVendorType as Type;
use App\Form\BillVendorFilterType as FilterType;
use App\Repository\BillVendorRepositoryQuery 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 BillVendorAdminController extends CrudController
{
#[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])]
#[Route(path: "/admin/bill_vendor/{page}", name: "admin_bill_vendor_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_vendor/new", name: "admin_bill_vendor_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_vendor/show/{entity}", name: "admin_bill_vendor_show", methods: ['GET'])]
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
#[Route(path: "/admin/bill_vendor/filter", name: "admin_bill_vendor_filter", methods: ['GET'])]
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
#[Route(path: "/admin/bill_vendor/edit/{entity}", name: "admin_bill_vendor_edit", methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
#[Route(path: "/admin/bill_vendor/sort/{page}", name: "admin_bill_vendor_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_vendor/batch/{page}", name: "admin_bill_vendor_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_vendor/delete/{entity}", name: "admin_bill_vendor_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', 'Fournisseurs')
->setPageTitle('edit', '{label}')
->setPageTitle('new', 'Nouveau fournisseur')
->setPageTitle('show', '{label}')
->setPageRoute('index', 'admin_bill_vendor_index')
->setPageRoute('new', 'admin_bill_vendor_new')
->setPageRoute('edit', 'admin_bill_vendor_edit')
->setPageRoute('show', 'admin_bill_vendor_show')
->setPageRoute('sort', 'admin_bill_vendor_sort')
->setPageRoute('batch', 'admin_bill_vendor_batch')
->setPageRoute('delete', 'admin_bill_vendor_delete')
->setPageRoute('filter', 'admin_bill_vendor_filter')
->setForm('edit', Type::class, [])
->setForm('new', Type::class)
->setForm('filter', FilterType::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_vendor';
}
}

199
src/Entity/Bill.php Normal file
View file

@ -0,0 +1,199 @@
<?php
namespace App\Entity;
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]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $reference = null;
#[ORM\Column(nullable: true)]
private ?float $amountTtc = null;
#[ORM\Column(nullable: true)]
private ?float $amountHt = null;
#[ORM\Column(options: ["default" => 0])]
private ?int $status = 0;
#[ORM\Column(type: Types::DATE_MUTABLE, nullable: true)]
private ?\DateTimeInterface $date = null;
#[ORM\Column(type: Types::DATE_MUTABLE, nullable: true)]
private ?\DateTimeInterface $paymentDeadlineDate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $file = null;
#[ORM\ManyToOne(inversedBy: 'bills')]
#[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;
}
public function getReference(): ?string
{
return $this->reference;
}
public function setReference(?string $reference): self
{
$this->reference = $reference;
return $this;
}
public function getAmountTtc(): ?float
{
return $this->amountTtc;
}
public function setAmountTtc(?float $amountTtc): self
{
$this->amountTtc = $amountTtc;
return $this;
}
public function getAmountHt(): ?float
{
return $this->amountHt;
}
public function setAmountHt(?float $amountHt): self
{
$this->amountHt = $amountHt;
return $this;
}
public function getStatus(): ?int
{
return $this->status;
}
public function setStatus(int $status): self
{
$this->status = $status;
return $this;
}
public function getHumanStatus(): string
{
return BillPeer::value($this->getStatus());
}
public function getDate(): ?\DateTimeInterface
{
return $this->date;
}
public function setDate(\DateTimeInterface $date): self
{
$this->date = $date;
return $this;
}
public function getPaymentDeadlineDate(): ?\DateTimeInterface
{
return $this->paymentDeadlineDate;
}
public function setPaymentDeadlineDate(?\DateTimeInterface $paymentDeadlineDate): self
{
$this->paymentDeadlineDate = $paymentDeadlineDate;
return $this;
}
public function getFile(): ?string
{
return $this->file;
}
public function setFile(?string $file): self
{
$this->file = $file;
return $this;
}
public function getVendor(): ?BillVendor
{
return $this->vendor;
}
public function setVendor(?BillVendor $vendor): self
{
$this->vendor = $vendor;
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;
}
}

33
src/Entity/BillPeer.php Normal file
View file

@ -0,0 +1,33 @@
<?php
namespace App\Entity;
class BillPeer
{
public const STATUS = [
0 => 'Brouillon',
1 => 'En attente de paiement',
2 => 'Payée',
];
public static function all(): array
{
return self::STATUS;
}
public static function value(int $key): string
{
return self::STATUS[$key];
}
public static function choices(): array
{
$data = [];
foreach (self::STATUS as $k => $v) {
$data[$v] = $k;
}
return $data;
}
}

85
src/Entity/BillVendor.php Normal file
View file

@ -0,0 +1,85 @@
<?php
namespace App\Entity;
use App\Repository\BillVendorRepository;
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]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $label = null;
#[ORM\OneToMany(mappedBy: 'vendor', 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->setVendor($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->getVendor() === $this) {
$bill->setVendor(null);
}
}
return $this;
}
}

View 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());
}
}

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

@ -0,0 +1,14 @@
<?php
namespace App\Factory;
use App\Core\Factory\FactoryInterface;
use App\Entity\Bill as Entity;
class BillFactory implements FactoryInterface
{
public function create(): Entity
{
return new Entity();
}
}

View file

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

View file

@ -0,0 +1,54 @@
<?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;
class AmountRangeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('min', NumberType::class, [
'label' => 'Min',
'html5' => true,
'scale' => 2,
'required' => false,
'attr' => [
'step' => 0.01,
],
'row_attr' => [
'class' => 'col-md-6 pr-1',
],
'label_attr' => [
'class' => 'font-weight-normal',
],
])
->add('max', NumberType::class, [
'label' => 'Max',
'html5' => true,
'required' => false,
'scale' => 2,
'attr' => [
'step' => 0.01,
],
'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,
]);
}
}

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

@ -0,0 +1,87 @@
<?php
namespace App\Form;
use App\Entity\BillCategory;
use App\Entity\BillPeer;
use App\Entity\BillVendor;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Form\DateRangeType;
class BillFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('reference', null, [
'label' => 'Numéro de facture',
'required' => false,
])
->add('vendor', null, [
'label' => 'Fournisseur',
'required' => false,
])
->add('vendor', EntityType::class, [
'label' => 'Fournisseur',
'required' => false,
'class' => BillVendor::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('v')
->orderBy('v.label', 'ASC')
;
},
'attr' => [
'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('dateRange', DateRangeType::class, [
'label' => 'Date',
'attr' => [
'class' => 'row',
],
])
->add('amountTtcRange', AmountRangeType::class, [
'label' => 'Montant TTC',
'attr' => [
'class' => 'row',
],
])
->add('amountHtRange', AmountRangeType::class, [
'label' => 'Montant HT',
'attr' => [
'class' => 'row',
],
])
->add('status', ChoiceType::class, [
'choices' => BillPeer::choices(),
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => null,
'csrf_protection' => false,
]);
}
}

140
src/Form/BillType.php Normal file
View file

@ -0,0 +1,140 @@
<?php
namespace App\Form;
use App\Entity\Bill;
use App\Entity\BillVendor;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
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
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if (null === $builder->getData()->getId()) {
$builder->add('file', FileType::class, [
'label' => 'Fichier',
'constraints' => [
new File([
'mimeTypes' => [
'application/pdf',
'application/x-pdf',
'image/png',
'image/jpeg',
],
'mimeTypesMessage' => 'Le fichier doit être au format PDF ou une image.',
]),
],
]);
} else {
$builder
->add('reference', null, [
'label' => 'Numéro de facture',
'constraints' => [
new NotBlank(),
],
])
->add('vendor', EntityType::class, [
'label' => 'Fournisseur',
'required' => false,
'class' => BillVendor::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('v')
->orderBy('v.label', 'ASC')
;
},
'attr' => [
'data-jschoice' => '',
],
])
->add('customVendor', null, [
'label' => 'Ajouter un fournisseur',
'label_attr' => [
'class' => 'font-weight-normal',
],
'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',
'label_attr' => [
'class' => 'mt-3',
],
])
->add('paymentDeadlineDate', null, [
'label' => 'Date limite de paiement',
'html5' => true,
'widget' => 'single_text',
])
->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(),
'help' => 'Modifier cette valeur manuellement ne permet pas d\'envoyer des notifications par mail',
'label_attr' => [
'class' => 'mt-3',
],
])
;
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Bill::class,
]);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Form;
use App\Entity\BillVendor;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class BillVendorFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('label', null, [
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => null,
'csrf_protection' => false,
]);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Form;
use App\Entity\BillVendor;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class BillVendorType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('label', null, [
'constraints' => [
new NotBlank(),
],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => BillVendor::class,
]);
}
}

View 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,
]);
}
}

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

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

View file

@ -0,0 +1,69 @@
<?php
namespace App\Repository;
use App\Core\Repository\RepositoryQuery;
use App\Repository\BillRepository as Repository;
use Knp\Component\Pager\PaginatorInterface;
class BillRepositoryQuery extends RepositoryQuery
{
public function __construct(Repository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'b', $paginator);
}
protected function filterHandler(string $name, $value)
{
if ('vendor' === $name && null !== $value) {
$this
->andWhere('.vendor = :vendor')
->setParameter('vendor', $value)
;
} elseif ('amountTtcRange' === $name) {
if (null !== $value['min']) {
$this
->andWhere('.amountTtc >= :amountTtcMin')
->setParameter('amountTtcMin', $value['min'])
;
}
if (null !== $value['max']) {
$this
->andWhere('.amountTtc <= :amountTtcMax')
->setParameter('amountTtcMax', $value['max'])
;
}
} elseif ('amountHtRange' === $name) {
if (null !== $value['min']) {
$this
->andWhere('.amountHt >= :amountHtMin')
->setParameter('amountHtMin', $value['min'])
;
}
if (null !== $value['max']) {
$this
->andWhere('.amountHt <= :amountHtMax')
->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'])
;
}
}
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Repository;
use App\Entity\BillVendor;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<BillVendor>
*
* @method BillVendor|null find($id, $lockMode = null, $lockVersion = null)
* @method BillVendor|null findOneBy(array $criteria, array $orderBy = null)
* @method BillVendor[] findAll()
* @method BillVendor[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class BillVendorRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, BillVendor::class);
}
public function save(BillVendor $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(BillVendor $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return BillVendor[] Returns an array of BillVendor 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): ?BillVendor
// {
// 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\BillVendorRepository as Repository;
class BillVendorRepositoryQuery extends RepositoryQuery
{
public function __construct(Repository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'b', $paginator);
}
}

View file

@ -0,0 +1,46 @@
{% if entity.id %}
<div class="row">
<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} %}
{% else %}
{% include(configuration.view('form_widget', '@Core/admin/crud/_form_widget.html.twig')) with {form: item} %}
{% endif %}
{% endfor %}
</div>
<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 %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-info">
Seuls les fichiers au format PDF et les images sont autorisés.
</div>
<p>Après avoir déposé le fichier, merci de bien vouloir remplir toutes les informations.</p>
{% 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} %}
{% else %}
{% include(configuration.view('form_widget', '@Core/admin/crud/_form_widget.html.twig')) with {form: item} %}
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}

View file

@ -0,0 +1,42 @@
{% set values = {
'Référence': entity.reference,
'Fournisseur': entity.vendor,
'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 HT': entity.amountHt is not null ? (entity.amountHt|number_format(2, ',') ~ ' €') : '/',
'Status': entity.humanStatus,
} %}
<div class="row">
<div class="col-md-12 p-3">
<div class="row">
<div class="col-md-4">
<div class="list-group">
{% for label, value in values %}
<div class="list-group-item">
<div class="font-weight-bold">{{ label|trans }}</div>
<div>{{ value }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="col-md-8 pl-3">
{% 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>
</div>

View 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 %}

View 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 %}

View file

@ -66,6 +66,29 @@
{{ include('@Core/admin/module/_menu_section.html.twig', {label: 'Comptabilité'}) }}
<ul class="nav flex-column">
{% if is_granted('ROLE_MANAGER') or 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_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',
label: 'Notes de frais',

View 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 &amp; Cie" width="50">
</p>
<p style="text-align: center; font-size: 20px">Le statut de la <a href="{{ show_url }}">facture&nbsp;#{{ entity.id }}</a> a été modifié&nbsp;☺️</p>
<p style="text-align: center; font-size: 20px">Nouveau statut&nbsp;:&nbsp;<strong>{{ entity.humanStatus }}</strong></p>
{% endblock %}

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"