Merge remote-tracking branch 'ChillBudgetBundle/sf4' into sf4

This commit is contained in:
Julien Fastré 2021-03-18 12:46:47 +01:00
commit 3ede278389
46 changed files with 2494 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?php
/*
*
*/
namespace Chill\AMLI\BudgetBundle\Calculator;
use Chill\AMLI\BudgetBundle\Entity\AbstractElement;
/**
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
interface CalculatorInterface
{
/**
*
* @param AbstractElement[] $elements
*/
public function calculate(array $elements) : ?CalculatorResult;
public function getAlias();
}

View File

@ -0,0 +1,67 @@
<?php
/*
*
*/
namespace Chill\AMLI\BudgetBundle\Calculator;
use Chill\AMLI\BudgetBundle\Entity\AbstractElement;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CalculatorManager
{
/**
*
* @var CalculatorInterface[]
*/
protected $calculators = [];
protected $defaultCalculator = [];
public function addCalculator(CalculatorInterface $calculator, bool $default)
{
$this->calculators[$calculator::getAlias()] = $calculator;
if ($default) {
$this->defaultCalculator[] = $calculator::getAlias();
}
}
/**
*
* @param string $alias
* @return CalculatorInterface
*/
public function getCalculator($alias)
{
if (FALSE === \array_key_exists($alias, $this->calculators)) {
throw new \OutOfBoundsException("The calculator with alias '$alias' does "
. "not exists. Possible values are ". \implode(", ", \array_keys($this->calculators)));
}
return $this->calculators[$alias];
}
/**
*
* @param AbstractElement[] $elements
* @return CalculatorResult[]
*/
public function calculateDefault(array $elements)
{
$results = [];
foreach ($this->defaultCalculator as $alias) {
$calculator = $this->calculators[$alias];
$result = $calculator->calculate($elements);
if ($result !== null) {
$results[$calculator::getAlias()] = $result;
}
}
return $results;
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
*
*/
namespace Chill\AMLI\BudgetBundle\Calculator;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CalculatorResult
{
const TYPE_RATE = 'rate';
const TYPE_CURRENCY = 'currency';
const TYPE_PERCENTAGE = 'percentage';
public $type;
public $result;
public $label;
}

View File

@ -0,0 +1,16 @@
<?php
namespace Chill\AMLI\BudgetBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Chill\AMLI\BudgetBundle\DependencyInjection\Compiler\CalculatorCompilerPass;
class ChillAMLIBudgetBundle extends Bundle
{
public function build(\Symfony\Component\DependencyInjection\ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new CalculatorCompilerPass());
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
*/
namespace Chill\AMLI\BudgetBundle\Config;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class ConfigRepository
{
/**
*
* @var array
*/
protected $resources;
/**
*
* @var array
*/
protected $charges;
public function __construct($resources, $charges)
{
$this->resources = $resources;
$this->charges = $charges;
}
/**
*
* @return array where keys are the resource'key and label the ressource label
*/
public function getResourcesLabels()
{
$resources = array();
foreach ($this->resources as $definition) {
$resources[$definition['key']] = $this->normalizeLabel($definition['labels']);
}
return $resources;
}
/**
*
* @return array where keys are the resource'key and label the ressource label
*/
public function getChargesLabels()
{
$charges = array();
foreach ($this->charges as $definition) {
$charges[$definition['key']] = $this->normalizeLabel($definition['labels']);
}
return $charges;
}
private function normalizeLabel($labels)
{
$normalizedLabels = array();
foreach ($labels as $labelDefinition) {
$normalizedLabels[$labelDefinition['lang']] = $labelDefinition['label'];
}
return $normalizedLabels;
}
}

View File

@ -0,0 +1,207 @@
<?php
namespace Chill\AMLI\BudgetBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use Chill\AMLI\BudgetBundle\Entity\AbstractElement;
use Chill\AMLI\BudgetBundle\Security\Authorization\BudgetElementVoter;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Translation\TranslatorInterface;
use Psr\Log\LoggerInterface;
abstract class AbstractElementController extends Controller
{
/**
*
* @var EntityManagerInterface
*/
protected $em;
/**
*
* @var TranslatorInterface
*/
protected $translator;
/**
*
* @var LoggerInterface
*/
protected $chillMainLogger;
public function __construct(
EntityManagerInterface $em,
TranslatorInterface $translator,
LoggerInterface $chillMainLogger
) {
$this->em = $em;
$this->translator = $translator;
$this->chillMainLogger = $chillMainLogger;
}
/**
* @return AbstractElement the newly created element
*/
abstract protected function createNewElement();
abstract protected function getType();
/**
*
*/
protected function _new(Person $person, Request $request, $template, $flashMessageOnSuccess)
{
/* @var $element \Chill\AMLI\BudgetBundle\Entity\AbstractElement */
$element = $this->createNewElement()
->setPerson($person)
;
$this->denyAccessUnlessGranted(BudgetElementVoter::CREATE, $element);
$form = $this->createForm($this->getType(), $element);
$form->add('submit', SubmitType::class);
$form->handleRequest($request);
if ($form->isSubmitted() and $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($element);
$em->flush();
$this->addFlash('success', $this->translator->trans($flashMessageOnSuccess));
return $this->redirectToRoute('chill_budget_elements_index', [
'id' => $person->getId()
]);
} elseif ($form->isSubmitted()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
return $this->render($template, array(
'form' => $form->createView(),
'person' => $person,
'element' => $element
));
}
/**
*
* @param AbstractElement $element
* @param Request $request
* @param string $template
* @param string $flashOnSuccess
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function _edit(AbstractElement $element, Request $request, $template, $flashOnSuccess)
{
$this->denyAccessUnlessGranted(BudgetElementVoter::UPDATE, $element);
$form = $this->createForm($this->getType(), $element);
$form->add('submit', SubmitType::class);
$form->handleRequest($request);
if ($form->isSubmitted() and $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->flush();
$this->addFlash('success', $this->translator->trans($flashOnSuccess));
return $this->redirectToRoute('chill_budget_elements_index', [
'id' => $element->getPerson()->getId()
]);
}
return $this->render($template, array(
'element' => $element,
'form' => $form->createView(),
'person' => $element->getPerson()
));
}
/**
*
* Route(
* "{_locale}/family-members/family-members/{id}/delete",
* name="chill_family_members_family_members_delete"
* )
*
* @param AbstractElement $element
* @param Request $request
* @return \Symfony\Component\BrowserKit\Response
*/
protected function _delete(AbstractElement $element, Request $request, $template, $flashMessage)
{
$this->denyAccessUnlessGranted(BudgetElementVoter::DELETE, $element, 'You are not '
. 'allowed to delete this family membership');
$form = $this->createDeleteForm();
if ($request->getMethod() === Request::METHOD_DELETE) {
$form->handleRequest($request);
if ($form->isValid()) {
$this->chillMainLogger->notice("A budget element has been removed", array(
'family_element' => get_class($element),
'by_user' => $this->getUser()->getUsername(),
'family_member_id' => $element->getId(),
'amount' => $element->getAmount(),
'type' => $element->getType()
));
$em = $this->getDoctrine()->getManager();
$em->remove($element);
$em->flush();
$this->addFlash('success', $this->translator
->trans($flashMessage));
return $this->redirectToRoute('chill_budget_elements_index', array(
'id' => $element->getPerson()->getId()
));
}
}
return $this->render($template, array(
'element' => $element,
'delete_form' => $form->createView()
));
}
/**
* Route(
* "{_locale}/family-members/family-members/{id}/view",
* name="chill_family_members_family_members_view"
* )
*/
protected function _view(AbstractElement $element, $template)
{
$this->denyAccessUnlessGranted(BudgetElementVoter::SHOW, $element);
return $this->render($template, array(
'element' => $element
));
}
/**
* Creates a form to delete a help request entity by id.
*
* @param mixed $id The entity id
*
* @return \Symfony\Component\Form\Form The form
*/
private function createDeleteForm()
{
return $this->createFormBuilder()
->setMethod(Request::METHOD_DELETE)
->add('submit', SubmitType::class, array('label' => 'Delete'))
->getForm()
;
}
}

View File

@ -0,0 +1,102 @@
<?php
/*
*
*/
namespace Chill\AMLI\BudgetBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Chill\AMLI\BudgetBundle\Controller\AbstractElementController;
use Chill\AMLI\BudgetBundle\Entity\Charge;
use Chill\AMLI\BudgetBundle\Form\ChargeType;
use Chill\PersonBundle\Entity\Person;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class ChargeController extends AbstractElementController
{
protected function getType()
{
return ChargeType::class;
}
protected function createNewElement()
{
return new Charge();
}
/**
*
* @Route(
* "{_locale}/budget/charge/{id}/view",
* name="chill_budget_charge_view"
* )
* @param Charge $charge
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewAction(Charge $charge)
{
return $this->_view($charge, '@ChillAMLIBudget/Charge/view.html.twig');
}
/**
* @Route(
* "{_locale}/budget/charge/by-person/{id}/new",
* name="chill_budget_charge_new"
* )
*
* @param Request $request
* @param Person $person
* @return \Symfony\Component\HttpFoundation\Response
*/
public function newAction(Request $request, Person $person)
{
return $this->_new(
$person,
$request,
'@ChillAMLIBudget/Charge/new.html.twig',
'Charge created');
}
/**
* @Route(
* "{_locale}/budget/charge/{id}/edit",
* name="chill_budget_charge_edit"
* )
*
* @param Request $request
* @param Charge $charge
* @return \Symfony\Component\HttpFoundation\Response
*/
public function editAction(Request $request, Charge $charge)
{
return $this->_edit(
$charge,
$request,
'@ChillAMLIBudget/Charge/edit.html.twig',
'Charge updated');
}
/**
*
* @Route(
* "{_locale}/budget/charge/{id}/delete",
* name="chill_budget_charge_delete"
* )
*
* @param Request $request
* @param Charge $charge
* @return \Symfony\Component\HttpFoundation\Response
*/
public function deleteAction(Request $request, Charge $charge)
{
return $this->_delete(
$charge,
$request,
'@ChillAMLIBudget/Charge/confirm_delete.html.twig',
'Charge deleted');
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Chill\AMLI\BudgetBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Translation\TranslatorInterface;
use Psr\Log\LoggerInterface;
use Chill\AMLI\BudgetBundle\Entity\Charge;
use Chill\AMLI\BudgetBundle\Entity\Resource;
use Chill\AMLI\BudgetBundle\Security\Authorization\BudgetElementVoter;
use Chill\AMLI\BudgetBundle\Calculator\CalculatorManager;
class ElementController extends Controller
{
/**
*
* @var EntityManagerInterface
*/
protected $em;
/**
*
* @var TranslatorInterface
*/
protected $translator;
/**
*
* @var LoggerInterface
*/
protected $chillMainLogger;
/**
*
* @var CalculatorManager
*/
protected $calculator;
public function __construct(
EntityManagerInterface $em,
TranslatorInterface $translator,
LoggerInterface $chillMainLogger,
CalculatorManager $calculator
) {
$this->em = $em;
$this->translator = $translator;
$this->chillMainLogger = $chillMainLogger;
$this->calculator = $calculator;
}
/**
* @Route(
* "{_locale}/budget/elements/by-person/{id}",
* name="chill_budget_elements_index"
* )
*/
public function indexAction(Person $person)
{
$this->denyAccessUnlessGranted(BudgetElementVoter::SHOW, $person);
$charges = $this->em
->getRepository(Charge::class)
->findByPerson($person);
$ressources = $this->em
->getRepository(Resource::class)
->findByPerson($person);
$now = new \DateTime('now');
$actualCharges = $this->em
->getRepository(Charge::class)
->findByPersonAndDate($person, $now);
$actualResources = $this->em
->getRepository(Resource::class)
->findByPersonAndDate($person, $now);
$elements = \array_merge($actualCharges, $actualResources);
if (count($elements) > 0) {
$results = $this->calculator->calculateDefault($elements);
}
return $this->render('ChillAMLIBudgetBundle:Element:index.html.twig', array(
'person' => $person,
'charges' => $charges,
'resources' => $ressources,
'results' => $results ?? []
));
}
}

View File

@ -0,0 +1,103 @@
<?php
/*
*
*/
namespace Chill\AMLI\BudgetBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Chill\AMLI\BudgetBundle\Controller\AbstractElementController;
use Chill\AMLI\BudgetBundle\Entity\Resource;
use Chill\AMLI\BudgetBundle\Form\ResourceType;
use Chill\PersonBundle\Entity\Person;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class ResourceController extends AbstractElementController
{
protected function getType()
{
return ResourceType::class;
}
protected function createNewElement()
{
return new Resource();
}
/**
*
* @Route(
* "{_locale}/budget/resource/{id}/view",
* name="chill_budget_resource_view"
* )
* @param Request $request
* @param Resource $resource
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewAction(Resource $resource)
{
return $this->_view($resource, '@ChillAMLIBudget/Resource/view.html.twig');
}
/**
* @Route(
* "{_locale}/budget/resource/by-person/{id}/new",
* name="chill_budget_resource_new"
* )
*
* @param Request $request
* @param Person $person
* @return \Symfony\Component\HttpFoundation\Response
*/
public function newAction(Request $request, Person $person)
{
return $this->_new(
$person,
$request,
'@ChillAMLIBudget/Resource/new.html.twig',
'Resource created');
}
/**
* @Route(
* "{_locale}/budget/resource/{id}/edit",
* name="chill_budget_resource_edit"
* )
*
* @param Request $request
* @param Resource $resource
* @return \Symfony\Component\HttpFoundation\Response
*/
public function editAction(Request $request, Resource $resource)
{
return $this->_edit(
$resource,
$request,
'@ChillAMLIBudget/Resource/edit.html.twig',
'Resource updated');
}
/**
*
* @Route(
* "{_locale}/budget/resource/{id}/delete",
* name="chill_budget_resource_delete"
* )
*
* @param Request $request
* @param Resource $resource
* @return \Symfony\Component\HttpFoundation\Response
*/
public function deleteAction(Request $request, Resource $resource)
{
return $this->_delete($resource,
$request,
'@ChillAMLIBudget/Resource/confirm_delete.html.twig',
'Resource deleted');
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Chill\AMLI\BudgetBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Chill\AMLI\BudgetBundle\Security\Authorization\BudgetElementVoter;
/**
* This is the class that loads and manages your bundle configuration.
*
* @link http://symfony.com/doc/current/cookbook/bundles/extension.html
*/
class ChillAMLIBudgetExtension extends Extension implements PrependExtensionInterface
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services/config.yaml');
$loader->load('services/form.yaml');
$loader->load('services/security.yaml');
$loader->load('services/controller.yaml');
$loader->load('services/templating.yaml');
$loader->load('services/menu.yaml');
$loader->load('services/calculator.yaml');
$this->storeConfig('resources', $config, $container);
$this->storeConfig('charges', $config, $container);
}
public function prepend(ContainerBuilder $container)
{
$this->prependAuthorization($container);
$this->prependRoutes($container);
}
protected function storeConfig($position, array $config, ContainerBuilder $container)
{
$container
->setParameter(sprintf('chill_budget.%s', $position), $config[$position])
;
}
protected function prependAuthorization(ContainerBuilder $container)
{
$container->prependExtensionConfig('security', array(
'role_hierarchy' => array(
BudgetElementVoter::UPDATE => [ BudgetElementVoter::SHOW ],
BudgetElementVoter::CREATE => [ BudgetElementVoter::SHOW ]
)
));
}
/* (non-PHPdoc)
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/
public function prependRoutes(ContainerBuilder $container)
{
//add routes for custom bundle
$container->prependExtensionConfig('chill_main', array(
'routing' => array(
'resources' => array(
'@ChillAMLIBudgetBundle/config/routing.yaml'
)
)
));
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
*
*/
namespace Chill\AMLI\BudgetBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CalculatorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$manager = $container->getDefinition('Chill\AMLI\BudgetBundle\Calculator\CalculatorManager');
foreach ($container->findTaggedServiceIds('chill_budget.calculator') as $id => $tags) {
foreach($tags as $tag) {
$reference = new Reference($id);
$manager->addMethodCall('addCalculator', [ $reference, $tag['default'] ?? false ]);
}
}
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Chill\AMLI\BudgetBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files.
*
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/configuration.html}
*/
class Configuration implements ConfigurationInterface
{
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('chill_amli_budget');
$rootNode = $treeBuilder->getRootNode('chill_amli_budget');
$rootNode
->children()
// ressources
->arrayNode('resources')->isRequired()->requiresAtLeastOneElement()
->arrayPrototype()
->children()
->scalarNode('key')->isRequired()->cannotBeEmpty()
->info('the key stored in database')
->example('salary')
->end()
->arrayNode('labels')->isRequired()->requiresAtLeastOneElement()
->arrayPrototype()
->children()
->scalarNode('lang')->isRequired()->cannotBeEmpty()
->example('fr')
->end()
->scalarNode('label')->isRequired()->cannotBeEmpty()
->example('Salaire')
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('charges')->isRequired()->requiresAtLeastOneElement()
->arrayPrototype()
->children()
->scalarNode('key')->isRequired()->cannotBeEmpty()
->info('the key stored in database')
->example('salary')
->end()
->arrayNode('labels')->isRequired()->requiresAtLeastOneElement()
->arrayPrototype()
->children()
->scalarNode('lang')->isRequired()->cannotBeEmpty()
->example('fr')
->end()
->scalarNode('label')->isRequired()->cannotBeEmpty()
->example('Salaire')
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
;
return $treeBuilder;
}
}

View File

@ -0,0 +1,225 @@
<?php
namespace Chill\AMLI\BudgetBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Validator\Constraints as Assert;
/**
* AbstractElement
*
* @ORM\MappedSuperclass()
*/
abstract class AbstractElement
{
/**
*
* @var Person
* @ORM\ManyToOne(
* targetEntity="\Chill\PersonBundle\Entity\Person"
* )
*/
private $person;
/**
* @var string
*
* @ORM\Column(name="type", type="string", length=255)
*/
private $type;
/**
* @var decimal
*
* @ORM\Column(name="amount", type="decimal", precision=10, scale=2)
* @Assert\GreaterThan(
* value=0
* )
* @Assert\NotNull(
* message="The amount cannot be empty"
* )
*
*/
private $amount;
/**
* @var string|null
*
* @ORM\Column(name="comment", type="text", nullable=true)
*/
private $comment;
/**
* @var \DateTimeImmutable
*
* @ORM\Column(name="startDate", type="datetime_immutable")
* @Assert\Date()
*/
private $startDate;
/**
* @var \DateTimeImmutable|null
*
* @ORM\Column(name="endDate", type="datetime_immutable", nullable=true)
* @Assert\GreaterThan(
* propertyPath="startDate",
* message="The budget element's end date must be after the start date"
* )
*/
private $endDate;
abstract public function isCharge(): bool;
abstract public function isResource(): bool;
public function getPerson(): Person
{
return $this->person;
}
public function setPerson(Person $person)
{
$this->person = $person;
return $this;
}
/**
* Set type.
*
* @param string $type
*
* @return AbstractElement
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* Get type.
*
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* Set amount.
*
* @param string $amount
*
* @return AbstractElement
*/
public function setAmount($amount)
{
$this->amount = $amount;
return $this;
}
/**
* Get amount.
*
* @return double
*/
public function getAmount()
{
return (double) $this->amount;
}
/**
* Set comment.
*
* @param string|null $comment
*
* @return AbstractElement
*/
public function setComment($comment = null)
{
$this->comment = $comment;
return $this;
}
/**
* Get comment.
*
* @return string|null
*/
public function getComment()
{
return $this->comment;
}
/**
* Set startDate.
*
* @param \DateTimeInterface $startDate
*
* @return AbstractElement
*/
public function setStartDate(\DateTimeInterface $startDate)
{
if ($startDate instanceof \DateTime) {
$this->startDate = \DateTimeImmutable::createFromMutable($startDate);
} elseif (NULL === $startDate) {
$this->startDate = null;
} else {
$this->startDate = $startDate;
}
return $this;
}
/**
* Get startDate.
*
* @return \DateTimeImmutable
*/
public function getStartDate()
{
return $this->startDate;
}
/**
* Set endDate.
*
* @param \DateTimeInterface|null $endDate
*
* @return AbstractElement
*/
public function setEndDate(\DateTimeInterface $endDate = null)
{
if ($endDate instanceof \DateTime) {
$this->endDate = \DateTimeImmutable::createFromMutable($endDate);
} elseif (NULL === $endDate) {
$this->endDate = null;
} else {
$this->endDate = $endDate;
}
return $this;
}
/**
* Get endDate.
*
* @return \DateTimeImmutable|null
*/
public function getEndDate()
{
return $this->endDate;
}
public function isEmpty()
{
return $this->amount == 0;
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace Chill\AMLI\BudgetBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Chill\MainBundle\Entity\HasCenterInterface;
/**
* Charge
*
* @ORM\Table(name="chill_budget.charge")
* @ORM\Entity(repositoryClass="Chill\AMLI\BudgetBundle\Repository\ChargeRepository")
*/
class Charge extends AbstractElement implements HasCenterInterface
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*
* @var string
* @ORM\Column(name="help", type="string", nullable=true)
*/
private $help = self::HELP_NOT_RELEVANT;
const HELP_ASKED = 'running';
const HELP_NO = 'no';
const HELP_YES = 'yes';
const HELP_NOT_RELEVANT = 'not-relevant';
const HELPS = [
self::HELP_ASKED,
self::HELP_NO,
self::HELP_YES,
self::HELP_NOT_RELEVANT
];
public function __construct()
{
$this->setStartDate(new \DateTimeImmutable('today'));
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
public function getHelp()
{
return $this->help;
}
public function setHelp($help)
{
$this->help = $help;
return $this;
}
public function getCenter(): \Chill\MainBundle\Entity\Center
{
return $this->getPerson()->getCenter();
}
public function isCharge(): bool
{
return true;
}
public function isResource(): bool
{
return false;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Chill\AMLI\BudgetBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Chill\MainBundle\Entity\HasCenterInterface;
/**
* Resource
*
* @ORM\Table(name="chill_budget.resource")
* @ORM\Entity(repositoryClass="Chill\AMLI\BudgetBundle\Repository\ResourceRepository")
*/
class Resource extends AbstractElement implements HasCenterInterface
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function __construct()
{
$this->setStartDate(new \DateTimeImmutable('today'));
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
public function getCenter(): \Chill\MainBundle\Entity\Center
{
return $this->getPerson()->getCenter();
}
public function isCharge(): bool
{
return false;
}
public function isResource(): bool
{
return true;
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace Chill\AMLI\BudgetBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\AMLI\BudgetBundle\Config\ConfigRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\AMLI\BudgetBundle\Entity\Charge;
class ChargeType extends AbstractType
{
/**
*
* @var ConfigRepository
*/
protected $configRepository;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
public function __construct(
ConfigRepository $configRepository,
TranslatableStringHelper $translatableStringHelper
) {
$this->configRepository = $configRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type', ChoiceType::class, [
'choices' => $this->getTypes(),
'placeholder' => 'Choose a charge type'
])
->add('amount', MoneyType::class)
->add('comment', TextAreaType::class, [
'required' => false
])
;
if ($options['show_start_date']) {
$builder->add('startDate', ChillDateType::class, [
'label' => 'Start of validity period'
]);
}
if ($options['show_end_date']) {
$builder->add('endDate', ChillDateType::class, [
'required' => false,
'label' => 'End of validity period'
]);
}
if ($options['show_help']) {
$builder->add('help', ChoiceType::class, [
'choices' => [
'charge.help.running' => Charge::HELP_ASKED,
'charge.help.no' => Charge::HELP_NO,
'charge.help.yes' => Charge::HELP_YES,
'charge.help.not-concerned' => Charge::HELP_NOT_RELEVANT
],
'placeholder' => 'Choose a status',
'required' => false,
'label' => 'Help to pay charges'
]);
}
}
private function getTypes()
{
$charges = $this->configRepository
->getChargesLabels();
// rewrite labels to filter in language
foreach ($charges as $key => $labels) {
$charges[$key] = $this->translatableStringHelper->localize($labels);
}
\asort($charges);
return \array_flip($charges);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Chill\AMLI\BudgetBundle\Entity\Charge',
'show_start_date' => true,
'show_end_date' => true,
'show_help' => true
));
$resolver
->setAllowedTypes('show_start_date', 'boolean')
->setAllowedTypes('show_end_date', 'boolean')
->setAllowedTypes('show_help', 'boolean')
;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'chill_amli_budgetbundle_charge';
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Chill\AMLI\BudgetBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Chill\AMLI\BudgetBundle\Entity\Resource;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\AMLI\BudgetBundle\Config\ConfigRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class ResourceType extends AbstractType
{
/**
*
* @var ConfigRepository
*/
protected $configRepository;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
public function __construct(
ConfigRepository $configRepository,
TranslatableStringHelper $translatableStringHelper
) {
$this->configRepository = $configRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type', ChoiceType::class, [
'choices' => $this->getTypes(),
'placeholder' => 'Choose a resource type',
'label' => 'Resource element type'
])
->add('amount', MoneyType::class)
->add('comment', TextAreaType::class, [
'required' => false
])
;
if ($options['show_start_date']) {
$builder->add('startDate', ChillDateType::class, [
'label' => 'Start of validity period'
]);
}
if ($options['show_end_date']) {
$builder->add('endDate', ChillDateType::class, [
'required' => false,
'label' => 'End of validity period'
]);
}
}
private function getTypes()
{
$resources = $this->configRepository
->getResourcesLabels();
// rewrite labels to filter in language
foreach ($resources as $key => $labels) {
$resources[$key] = $this->translatableStringHelper->localize($labels);
}
asort($resources);
return \array_flip($resources);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Resource::class,
'show_start_date' => true,
'show_end_date' => true
));
$resolver
->setAllowedTypes('show_start_date', 'boolean')
->setAllowedTypes('show_end_date', 'boolean')
;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'chill_amli_budgetbundle_resource';
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
*/
namespace Chill\AMLI\BudgetBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Chill\AMLI\BudgetBundle\Security\Authorization\BudgetElementVoter;
use Symfony\Component\Translation\TranslatorInterface;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class UserMenuBuilder implements LocalMenuBuilderInterface
{
/**
*
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
/**
*
* @var TranslatorInterface
*/
protected $translator;
public function __construct(
AuthorizationCheckerInterface $authorizationChecker,
TranslatorInterface $translator
) {
$this->authorizationChecker = $authorizationChecker;
$this->translator = $translator;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/* @var $person \Chill\PersonBundle\Entity\Person */
$person = $parameters['person'];
if ($this->authorizationChecker->isGranted(BudgetElementVoter::SHOW, $person)) {
$menu->addChild(
$this->translator->trans('Budget'), [
'route' => 'chill_budget_elements_index',
'routeParameters' => [ 'id' => $person->getId() ],
])
->setExtra('order', 460)
;
}
}
public static function getMenuIds(): array
{
return [ 'person' ];
}
}

View File

@ -0,0 +1,5 @@
Budget Bundle
=============
Add the possibility to register budget element (charges and resources) for people.

View File

@ -0,0 +1,35 @@
<?php
namespace Chill\AMLI\BudgetBundle\Repository;
use Chill\PersonBundle\Entity\Person;
/**
* ChargeRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class ChargeRepository extends \Doctrine\ORM\EntityRepository
{
public function findByPersonAndDate(Person $person, \DateTime $date, $sort = null)
{
$qb = $this->createQueryBuilder('c');
$qb->where('c.person = :person')
->andWhere('c.startDate < :date')
->andWhere('c.startDate < :date OR c.startDate IS NULL')
;
if ($sort !== null) {
$qb->orderBy($sort);
}
$qb->setParameters([
'person' => $person,
'date' => $date
]);
return $qb->getQuery()->getResult();
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Chill\AMLI\BudgetBundle\Repository;
use Chill\PersonBundle\Entity\Person;
/**
* ResourceRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class ResourceRepository extends \Doctrine\ORM\EntityRepository
{
public function findByPersonAndDate(Person $person, \DateTime $date, $sort = null)
{
$qb = $this->createQueryBuilder('c');
$qb->where('c.person = :person')
->andWhere('c.startDate < :date')
->andWhere('c.startDate < :date OR c.startDate IS NULL')
;
if ($sort !== null) {
$qb->orderBy($sort);
}
$qb->setParameters([
'person' => $person,
'date' => $date
]);
return $qb->getQuery()->getResult();
}
}

View File

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
namespace Application\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* create schema for chill budget
*/
final class Version20180522080432 extends AbstractMigration
{
public function up(Schema $schema)
{
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE SCHEMA chill_budget');
$this->addSql('DROP SEQUENCE report_id_seq CASCADE');
$this->addSql('CREATE SEQUENCE chill_budget.resource_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE SEQUENCE chill_budget.charge_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_budget.resource (id INT NOT NULL, person_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, amount NUMERIC(10, 2) NOT NULL, comment TEXT DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_5E0A5E97217BBB47 ON chill_budget.resource (person_id)');
$this->addSql('COMMENT ON COLUMN chill_budget.resource.startDate IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_budget.resource.endDate IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('CREATE TABLE chill_budget.charge (id INT NOT NULL, person_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, amount NUMERIC(10, 2) NOT NULL, comment TEXT DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, help VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_5C99D2C3217BBB47 ON chill_budget.charge (person_id)');
$this->addSql('COMMENT ON COLUMN chill_budget.charge.startDate IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_budget.charge.endDate IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_budget.resource ADD CONSTRAINT FK_5E0A5E97217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_budget.charge ADD CONSTRAINT FK_5C99D2C3217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema)
{
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE SCHEMA chill_budget CASCADE');
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace Application\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* autorise les valeurs nulles dans "HELP"
*/
final class Version20181219145631 extends AbstractMigration
{
public function up(Schema $schema) : void
{
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('ALTER TABLE chill_budget.charge ALTER help DROP NOT NULL');
}
public function down(Schema $schema) : void
{
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('ALTER TABLE chill_budget.charge ALTER help SET NOT NULL');
}
}

View File

@ -0,0 +1,60 @@
Budget: Budget
Budget for %name%: Budget de %name%
Resource element type: Nature de la ressource
Actual budget: Budget actuel
Actual resources: Ressources actuelles
Actual charges: Charges actuelles
Past budget: Éléments du budget passé
Past resources: Ressources passées
Past charges: Chargées passées
Future budget: Futurs éléments du budget
Future resources: Ressources futures
Future charges: Charges futures
Budget element type: Nature
Validity period: Période de validité
Start of validity period: Début de la période de validité
End of validity period: Fin de la période de validité
Total: Total
Create new resource: Créer une nouvelle ressource
Create new charge: Créer une nouvelle charge
There isn't any element recorded: Aucun élément enregistré
No resources registered: Aucune ressource enregistrée
No charges registered: Aucune charge enregistrée
No past resources registered: Aucune ressource passée
No past charges registered: Aucune charge passée
No future resources registered: Aucune ressource future enregistrée
No future charges registered: Aucune ressource future enregistrée
New Resource for %name%: Nouvelle ressource pour %name%
New Charge for %name%: Nouvelle charge pour %name%
Edit Resource for %name%: Modifier une ressource de %name%
Edit Charge for %name%: Modifier une charge de %name%
Remove resource: Supprimer la ressource
Remove charge: Supprimer la charge
Are you sure you want to remove the ressource "%type%" associated to "%name%" ?: Êtes-vous sûr·e de vouloir supprimer la ressource de nature "%type%" associée à %name% ?
Are you sure you want to remove the charge "%type%" associated to "%name%" ?: Êtes-vous sûr·e de vouloir supprimer la charge de nature "%type%" associée à %name% ?
Resource deleted: Ressource supprimée
Charge deleted: Charge supprimée
Charge created: Charge créée
Resource created: Ressource créée
Resource updated: Resource mise à jour
Charge updated: charge mise à jour
Choose a resource type: Choisissez un type de ressource
Choose a charge type: Choisissez un type de charge
Amount: Montant
Comment: Commentaire
Help to pay charges: Aide au payement des dépenses d'énergie
Choose a status: Choisissez un état
charge.help.running: En cours
charge.help.no: Non demandé
charge.help.yes: Oui
charge.help.not-concerned: Non concerné
Budget calculator: Calculs et indices sur le budget
Budget calculator result: Résultats
'Valid since %startDate% until %endDate%': Valide depuis le %startDate% jusqu'au %endDate%
'Valid since %startDate%': Valide depuis le %startDate%

View File

@ -0,0 +1,2 @@
The amount cannot be empty: Le montant ne peut pas être vide ou égal à zéro
The budget element's end date must be after the start date: La date de fin doit être après la date de début

View File

@ -0,0 +1,19 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set person = element.person %}
{% block title 'Remove resource'|trans %}
{% block personcontent %}
{{ include('ChillMainBundle:Util:confirmation_template.html.twig',
{
'title' : 'Remove charge'|trans,
'confirm_question' : 'Are you sure you want to remove the charge "%type%" associated to "%name%" ?'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname, '%type%': element.type|budget_element_type_display('charge') } ),
'cancel_route' : 'chill_budget_elements_index',
'cancel_parameters' : { 'id' : element.person.id },
'form' : delete_form
} ) }}
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set title = 'Edit Charge for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %}
{% block title title %}
{% block personcontent %}
<h1>{{ title }}</h1>
{{ form_start(form) }}
{{ form_row(form.type) }}
{{ form_row(form.amount) }}
{{ form_row(form.help) }}
{{ form_row(form.comment) }}
{{ form_row(form.startDate) }}
{{ form_row(form.endDate) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path("chill_budget_elements_index", { 'id': person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
{{ form_widget(form.submit, { 'attr' : { 'class': 'sc-button bt-create' }, 'label': 'Edit' } ) }}
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set title = 'New Charge for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %}
{% block title title %}
{% block personcontent %}
<h1>{{ title }}</h1>
{{ form_start(form) }}
{{ form_row(form.type) }}
{{ form_row(form.amount) }}
{{ form_row(form.help) }}
{{ form_row(form.comment) }}
{{ form_row(form.startDate) }}
{{ form_row(form.endDate) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path("chill_budget_elements_index", { 'id': person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
{{ form_widget(form.submit, { 'attr' : { 'class': 'sc-button bt-create' }, 'label': 'Create' } ) }}
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set person = element.person %}
{% set title = 'Charge for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %}
{% block title title %}
{% block personcontent %}
<h1>{{ title }}</h1>
<dl class="chill_view_data">
<dt>{{ 'Type'|trans }}</dt>
<dd>{{ element.type|budget_element_type_display('charge') }}</dd>
<dt>{{ 'Amount'|trans }}</dt>
<dd>{{ element.amount|localizedcurrency('EUR') }}</dd>
<dt>{{ 'Validity period'|trans }}</dt>
<dd>
{% if element.endDate is not null %}
{{ 'Valid since %startDate% until %endDate%'|trans( { '%startDate%': element.startDate|localizeddate('long', 'none'), '%endDate%': familyMember.endDate|localizeddate('long', 'none') } ) }}
{% else %}
{{ 'Valid since %startDate%'|trans( { '%startDate%': element.startDate|localizeddate('long', 'none') } ) }}
{% endif %}
</dd>
<dt>{{ 'Comment'|trans }}</dt>
<dd>
{%- if element.comment is not empty -%}
<blockquote class="chill-user-quote">
{{ element.comment }}
</blockquote>
{%- else -%}
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
{%- endif -%}
</dd>
</dl>
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path("chill_budget_elements_index", { 'id': person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
{% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::UPDATE'), element) %}
<li>
<a href="{{ path('chill_budget_charge_edit', { 'id': element.id } ) }}" class="sc-button bt-edit">{{ 'Edit'|trans }}</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -0,0 +1,228 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set title = 'Budget for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %}
{% block title title %}
{% set actualResources = [] %}
{% set futureResources = [] %}
{% set pastResources = [] %}
{% for r in resources %}
{% if r.startDate|date('U') <= 'now'|date('U') %}
{% if r.endDate is null or r.endDate|date('U') >= 'now'|date('U') %}
{% set actualResources = actualResources|merge([ r ]) %}
{% else %}
{% set pastResources = pastResources|merge([ r ]) %}
{% endif %}
{% else %}
{% set futureResources = futureResources|merge([ r ]) %}
{% endif %}
{% endfor %}
{% set actualCharges = [] %}
{% set futureCharges = [] %}
{% set pastCharges = [] %}
{% for c in charges %}
{% if c.startDate|date('U') <= 'now'|date('U') %}
{% if c.endDate is null or c.endDate|date('U') >= 'now'|date('U') %}
{% set actualCharges = actualCharges|merge([ c ]) %}
{% else %}
{% set pastCharges = pastCharges|merge([ c ]) %}
{% endif %}
{% else %}
{% set futureCharges = futureCharges|merge([ c ]) %}
{% endif %}
{% endfor %}
{% macro table_elements(elements, family) %}
<table>
<thead>
<tr>
<th>{{ 'Budget element type'|trans }}</th>
<th>{{ 'Amount'|trans }}</th>
<th>{{ 'Validity period'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% set total = 0 %}
{% for f in elements %}
{% set total = total + f.amount %}
<tr>
<td>
{{ f.type|budget_element_type_display(family) }}
</td>
<td>{{ f.amount|format_currency('EUR') }}</td>
<td>
{% if f.endDate is not null %}
{{ 'Valid since %startDate% until %endDate%'|trans( { '%startDate%': f.startDate|format_date('long'), '%endDate%': f.endDate|format_date('long') } ) }}
{% else %}
{{ 'Valid since %startDate%'|trans( { '%startDate%': f.startDate|format_date('long') } ) }}
{% endif %}
</td>
<td>
<ul class="record_actions">
{% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::SHOW'), f) %}
<li>
<a href="{{ path('chill_budget_' ~ family ~ '_view', { 'id': f.id } ) }}" class="sc-button bt-show"></a>
</li>
{% endif %}
{% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::UPDATE'), f) %}
<li>
<a href="{{ path('chill_budget_' ~ family ~'_edit', { 'id': f.id } ) }}" class="sc-button bt-edit"></a>
</li>
{% endif %}
{% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::DELETE'), f) %}
<li>
<a href="{{ path('chill_budget_' ~ family ~ '_delete', { 'id': f.id } ) }}" class="sc-button bt-delete"></a>
</li>
{% endif %}
</ul>
</td>
</tr>
{% endfor %}
<tr>
<td>
{{ 'Total'|trans }}
</td>
<td>
{{ total|format_currency('EUR') }}
</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
{% endmacro %}
{% macro table_results(results) %}
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>{{ 'Budget calculator result'|trans }}</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr>
<td>{{ result.label }}</td>
<td>
{% if result.type == constant('CHILL\\AMLI\\BudgetBundle\\Calculator\\CalculatorResult::TYPE_CURRENCY') %}
{{ result.result|format_currency('EUR') }}
{% elseif result.type == constant('CHILL\\AMLI\\BudgetBundle\\Calculator\\CalculatorResult::TYPE_PERCENTAGE') %}
{{ result.result|round(2, 'ceil') ~ '%' }}
{% else %}
{{ result.result|round(2, 'common') }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}
{% import _self as m %}
{% block personcontent %}
<h1>{{ title }}</h1>
<h2>{{ 'Actual budget'|trans }}</h2>
{% if resources|length == 0 and charges|length == 0 %}
<p><span class="chill-no-data-statement">{{ "There isn't any element recorded"|trans }}</span></p>
{% else %}
<h3>{{ 'Actual resources'|trans }}</h3>
{% if actualResources|length > 0 %}
{{ m.table_elements(actualResources, 'resource') }}
{% else %}
<span class="chill-no-data-statement">{{ 'No resources registered'|trans }}</span>
{% endif %}
<h3>{{ 'Actual charges'|trans }}</h3>
{% if actualCharges|length > 0 %}
{{ m.table_elements(actualCharges, 'charge') }}
{% else %}
<span class="chill-no-data-statement">{{ 'No charges registered'|trans }}</span>
{% endif %}
{% if results|length > 0 %}
<h2>{{ 'Budget calculator'|trans }}</h2>
{{ m.table_results(results) }}
{% endif %}
{% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::CREATE'), person) %}
<ul class="record_actions">
<li>
<a class="sc-button bt-create" href="{{ path('chill_budget_resource_new', { 'id': person.id} ) }}">{{ 'Create new resource'|trans }}</a>
</li>
<li>
<a class="sc-button bt-create" href="{{ path('chill_budget_charge_new', { 'id': person.id} ) }}">{{ 'Create new charge'|trans }}</a>
</li>
</ul>
{% endif %}
{% endif %}
{% if pastCharges|length > 0 or pastResources|length > 0 %}
<h2>{{ 'Past budget'|trans }}</h2>
<h3>{{ 'Past resources'|trans }}</h3>
{% if pastResources|length > 0 %}
{{ m.table_elements(pastResources, 'resource') }}
{% else %}
<span class="chill-no-data-statement">{{ 'No past resources registered'|trans }}</span>
{% endif %}
<h3>{{ 'Past charges'|trans }}</h3>
{% if pastCharges|length > 0 %}
{{ m.table_elements(pastCharges, 'charge') }}
{% else %}
<span class="chill-no-data-statement">{{ 'No past charges registered'|trans }}</span>
{% endif %}
{% endif %}
{% if futureCharges|length > 0 or futureResources|length > 0 %}
<h2>{{ 'Future budget'|trans }}</h2>
<h3>{{ 'Future resources'|trans }}</h3>
{% if futureResources|length > 0 %}
{{ m.table_elements(futureResources, 'resource') }}
{% else %}
<span class="chill-no-data-statement">{{ 'No future resources registered'|trans }}</span>
{% endif %}
<h3>{{ 'Future charges'|trans }}</h3>
{% if futureCharges|length > 0 %}
{{ m.table_elements(futureCharges, 'charge') }}
{% else %}
<span class="chill-no-data-statement">{{ 'No future charges registered'|trans }}</span>
{% endif %}
{% endif %}
{% if (resources|length + charges|length) == 0 or futureCharges|length > 0 or futureResources|length > 0 or pastCharges|length > 0 or pastResources|length > 0 %}
{% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::CREATE'), person) %}
<ul class="record_actions">
<li>
<a class="sc-button bt-create" href="{{ path('chill_budget_resource_new', { 'id': person.id} ) }}">{{ 'Create new resource'|trans }}</a>
</li>
<li>
<a class="sc-button bt-create" href="{{ path('chill_budget_charge_new', { 'id': person.id} ) }}">{{ 'Create new charge'|trans }}</a>
</li>
</ul>
{% endif %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set person = element.person %}
{% block title 'Remove resource'|trans %}
{% block personcontent %}
{{ include('ChillMainBundle:Util:confirmation_template.html.twig',
{
'title' : 'Remove resource'|trans,
'confirm_question' : 'Are you sure you want to remove the ressource "%type%" associated to "%name%" ?'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname, '%type%': element.type|budget_element_type_display('resource') } ),
'cancel_route' : 'chill_budget_elements_index',
'cancel_parameters' : { 'id' : element.person.id },
'form' : delete_form
} ) }}
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set title = 'Edit Resource for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %}
{% block title title %}
{% block personcontent %}
<h1>{{ title }}</h1>
{{ form_start(form) }}
{{ form_row(form.type) }}
{{ form_row(form.amount) }}
{{ form_row(form.comment) }}
{{ form_row(form.startDate) }}
{{ form_row(form.endDate) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path("chill_budget_elements_index", { 'id': person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
{{ form_widget(form.submit, { 'attr' : { 'class': 'sc-button bt-create' }, 'label': 'Edit' } ) }}
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set title = 'New Resource for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %}
{% block title title %}
{% block personcontent %}
<h1>{{ title }}</h1>
{{ form_start(form) }}
{{ form_row(form.type) }}
{{ form_row(form.amount) }}
{{ form_row(form.comment) }}
{{ form_row(form.startDate) }}
{{ form_row(form.endDate) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path("chill_budget_elements_index", { 'id': person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
{{ form_widget(form.submit, { 'attr' : { 'class': 'sc-button bt-create' }, 'label': 'Create' } ) }}
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "@ChillPerson/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% set person = element.person %}
{% set title = 'Resource for %name%'|trans({ '%name%' : person.firstName ~ " " ~ person.lastName } ) %}
{% block title title %}
{% block personcontent %}
<h1>{{ title }}</h1>
<dl class="chill_view_data">
<dt>{{ 'Type'|trans }}</dt>
<dd>{{ element.type|budget_element_type_display('resource') }}</dd>
<dt>{{ 'Amount'|trans }}</dt>
<dd>{{ element.amount|format_currency('EUR') }}</dd>
<dt>{{ 'Validity period'|trans }}</dt>
<dd>
{% if element.endDate is not null %}
{{ 'Valid since %startDate% until %endDate%'|trans( { '%startDate%': element.startDate|format_date('long'), '%endDate%': familyMember.endDate|format_date('long') } ) }}
{% else %}
{{ 'Valid since %startDate%'|trans( { '%startDate%': element.startDate|format_date('long') } ) }}
{% endif %}
</dd>
<dt>{{ 'Comment'|trans }}</dt>
<dd>
{%- if element.comment is not empty -%}
<blockquote class="chill-user-quote">
{{ element.comment }}
</blockquote>
{%- else -%}
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
{%- endif -%}
</dd>
</dl>
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path("chill_budget_elements_index", { 'id': person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
{% if is_granted(constant('Chill\\AMLI\\BudgetBundle\\Security\\Authorization\\BudgetElementVoter::UPDATE'), element) %}
<li>
<a href="{{ path('chill_budget_resource_edit', { 'id': element.id } ) }}" class="sc-button bt-edit">{{ 'Edit'|trans }}</a>
</li>
{% endif %}
</ul>
{% endblock %}

View File

@ -0,0 +1,79 @@
<?php
/*
*/
namespace Chill\AMLI\BudgetBundle\Security\Authorization;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\AMLI\BudgetBundle\Entity\AbstractElement;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Security\Core\Role\Role;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class BudgetElementVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
const CREATE = 'CHILL_BUDGET_ELEMENT_CREATE';
const DELETE = 'CHILL_BUDGET_ELEMENT_DELETE';
const UPDATE = 'CHILL_BUDGET_ELEMENT_UPDATE';
const SHOW = 'CHILL_BUDGET_ELEMENT_SHOW';
const ROLES = [
self::CREATE,
self::DELETE,
self::SHOW,
self::UPDATE
];
/**
*
* @var AuthorizationHelper
*/
protected $authorizationHelper;
public function __construct(AuthorizationHelper $authorizationHelper)
{
$this->authorizationHelper = $authorizationHelper;
}
protected function supports($attribute, $subject)
{
return (\in_array($attribute, self::ROLES) && $subject instanceof AbstractElement)
or
($subject instanceof Person && \in_array($attribute, [ self::SHOW, self::CREATE ]));
}
protected function voteOnAttribute($attribute, $subject, \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token)
{
$user = $token->getUser();
if (FALSE === $user instanceof User) {
return false;
}
return $this->authorizationHelper
->userHasAccess($user, $subject, new Role($attribute));
}
public function getRoles()
{
return self::ROLES;
}
public function getRolesWithHierarchy(): array
{
return [ 'Budget elements' => self::ROLES ];
}
public function getRolesWithoutScope()
{
return self::ROLES;
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
*
*/
namespace Chill\AMLI\BudgetBundle\Templating;
use Twig\Extension\AbstractExtension;
use Chill\AMLI\BudgetBundle\Config\ConfigRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Twig\TwigFilter;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class Twig extends AbstractExtension
{
/**
*
* @var ConfigRepository
*/
protected $configRepository;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
public function __construct(
ConfigRepository $configRepository,
TranslatableStringHelper $translatableStringHelper
) {
$this->configRepository = $configRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function getFilters()
{
return [
new TwigFilter('budget_element_type_display', [ $this, 'displayLink' ], [ 'is_safe' => [ 'html' ]])
];
}
public function displayLink($link, $family)
{
switch($family) {
case 'resource':
return $this->translatableStringHelper->localize(
$this->configRepository->getResourcesLabels()[$link]
);
case 'charge':
return $this->translatableStringHelper->localize(
$this->configRepository->getChargesLabels()[$link]
);
default:
throw new \UnexpectedValueException("This family of element: $family is not "
. "supported. Supported families are 'resource', 'charge'");
}
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Chill\AMLI\BudgetBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ElementControllerTest extends WebTestCase
{
public function testList()
{
$client = static::createClient();
$crawler = $client->request('GET', '/list');
}
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/index');
}
}

View File

@ -0,0 +1,30 @@
{
"name": "chill-project/budget",
"description": "This bundle extend chill for recording element of a budget for peoples",
"type": "symfony-bundle",
"license": "AGPL-3.0",
"keywords" : ["chill", "social work"],
"homepage" : "https://framagit.org/Chill-project/BudgetBundle",
"autoload": {
"psr-4": { "Chill\\AMLI\\BudgetBundle\\": "" }
},
"autoload-dev": {
"classmap": [ "Resources/test/Fixtures/App/app/AppKernel.php" ]
},
"authors": [
{
"name": "Champs-Libres",
"email": "info@champs-libres.coop"
}
],
"require": {
},
"require-dev": {
},
"extra": {
"app-migrations-dir": "Resources/test/Fixtures/App/app/DoctrineMigrations",
"symfony-app-dir": "Test/Fixtures/App/app/"
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@ -0,0 +1,3 @@
chill_amli_budget_controllers:
resource: "@ChillAMLIBudgetBundle/Controller"
type: annotation

View File

@ -0,0 +1,2 @@
services:
Chill\AMLI\BudgetBundle\Calculator\CalculatorManager: ~

View File

@ -0,0 +1,5 @@
services:
Chill\AMLI\BudgetBundle\Config\ConfigRepository:
arguments:
$resources: '%chill_budget.resources%'
$charges: '%chill_budget.charges%'

View File

@ -0,0 +1,5 @@
services:
Chill\AMLI\BudgetBundle\Controller\:
autowire: true
resource: '../../Controller'
tags: ['controller.service_arguments']

View File

@ -0,0 +1,14 @@
services:
Chill\AMLI\BudgetBundle\Form\ResourceType:
arguments:
$configRepository: '@Chill\AMLI\BudgetBundle\Config\ConfigRepository'
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
tags:
- { name: 'form.type' }
Chill\AMLI\BudgetBundle\Form\ChargeType:
arguments:
$configRepository: '@Chill\AMLI\BudgetBundle\Config\ConfigRepository'
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
tags:
- { name: 'form.type' }

View File

@ -0,0 +1,7 @@
services:
Chill\AMLI\BudgetBundle\Menu\UserMenuBuilder:
arguments:
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
tags:
- { name: 'chill.menu_builder' }

View File

@ -0,0 +1,7 @@
services:
Chill\AMLI\BudgetBundle\Security\Authorization\BudgetElementVoter:
arguments:
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
tags:
- { name: chill.role }
- { name: security.voter }

View File

@ -0,0 +1,7 @@
services:
Chill\AMLI\BudgetBundle\Templating\Twig:
arguments:
$configRepository: '@Chill\AMLI\BudgetBundle\Config\ConfigRepository'
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
tags:
- { name: 'twig.extension' }