@@ -8,10 +8,10 @@ Kimai v2 - the reloaded open source Time-Tracking application.
|
||||
|
||||
This is (or will be in the future, currently a lot of features are still missing) the reloaded version of the open source time-tracking application [Kimai](http://www.kimai.org).
|
||||
|
||||
It is based on the following PHP components:
|
||||
It is based on a lot of great PHP components. Special thanks to:
|
||||
- [Symfony Framework 4](https://github.com/symfony/symfony)
|
||||
- [AdminThemeBundle](https://github.com/avanzu/AdminThemeBundle/) (based on [AdminLTE](https://github.com/almasaeed2010/AdminLTE/))
|
||||
- [Doctrine](https://github.com/doctrine/)
|
||||
- [AdminThemeBundle](https://github.com/avanzu/AdminThemeBundle/) (based on [AdminLTE](https://github.com/almasaeed2010/AdminLTE/))
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
8
composer.lock
generated
8
composer.lock
generated
@@ -52,12 +52,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kevinpapst/AdminThemeBundle.git",
|
||||
"reference": "d31084eff7888d76f7976700f9ac878f40c3bacd"
|
||||
"reference": "d0d849ac59889ad6f4408fee84e23b09d7d65d36"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kevinpapst/AdminThemeBundle/zipball/d31084eff7888d76f7976700f9ac878f40c3bacd",
|
||||
"reference": "d31084eff7888d76f7976700f9ac878f40c3bacd",
|
||||
"url": "https://api.github.com/repos/kevinpapst/AdminThemeBundle/zipball/d0d849ac59889ad6f4408fee84e23b09d7d65d36",
|
||||
"reference": "d0d849ac59889ad6f4408fee84e23b09d7d65d36",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -111,7 +111,7 @@
|
||||
"support": {
|
||||
"source": "https://github.com/kevinpapst/AdminThemeBundle/tree/kevinpapst"
|
||||
},
|
||||
"time": "2018-01-17T12:31:37+00:00"
|
||||
"time": "2018-01-21T19:07:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "beberlei/DoctrineExtensions",
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
kimai:
|
||||
timesheet:
|
||||
rounding: 15
|
||||
invoice:
|
||||
renderer:
|
||||
default: 'App\Controller\InvoiceController::invoiceAction'
|
||||
calculator:
|
||||
default: 'App\Invoice\DefaultCalculator'
|
||||
number_generator:
|
||||
default: 'App\Invoice\DateNumberGenerator'
|
||||
#random: 'App\Invoice\RandomNumberGenerator'
|
||||
|
||||
twig:
|
||||
# form_themes:
|
||||
# - "bootstrap_3_layout.html.twig"
|
||||
# - "form/fields.html.twig"
|
||||
globals:
|
||||
kimai_context:
|
||||
date_1: "d.m.Y" # used for display in timesheets
|
||||
box_color: "green" # a color for ???
|
||||
active_warning: 3 # display a warning color if the user has at least X active recordings
|
||||
control_sidebar:
|
||||
control_sidebar: # all tabs in the control sidebar
|
||||
settings:
|
||||
icon: gears
|
||||
controller: 'App\Controller\SidebarController::settingsAction'
|
||||
@@ -12,6 +12,8 @@ services:
|
||||
public: false # Allows optimizing the container by removing unused services; this also means
|
||||
# fetching services directly from the container via $container->get() won't work.
|
||||
# The best practice is to be explicit about your dependencies anyway.
|
||||
bind:
|
||||
$projectDirectory: "%kernel.project_dir%"
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
@@ -25,13 +27,14 @@ services:
|
||||
resource: '../src/Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
# ================================================================================
|
||||
# APPLICATION CORE
|
||||
# ================================================================================
|
||||
|
||||
App\Invoice\ServiceInvoice:
|
||||
class: App\Invoice\ServiceInvoice
|
||||
arguments: ['%kimai.invoice%', '@service_container']
|
||||
|
||||
# a route listener, that injects the locale through a URL directory
|
||||
App\EventSubscriber\RedirectToLocaleSubscriber:
|
||||
class: App\EventSubscriber\RedirectToLocaleSubscriber
|
||||
@@ -44,34 +47,25 @@ services:
|
||||
# ================================================================================
|
||||
|
||||
# service that prefixes every database table
|
||||
app.tableprefix_subscriber:
|
||||
App\Doctrine\TablePrefixSubscriber:
|
||||
class: App\Doctrine\TablePrefixSubscriber
|
||||
arguments: ["%env(resolve:DATABASE_PREFIX)%"]
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber }
|
||||
|
||||
# updates timesheet records, to make sure they are calculated and can't be manipulated by the users
|
||||
app.database_listener.timesheet:
|
||||
App\Doctrine\TimesheetSubscriber:
|
||||
class: App\Doctrine\TimesheetSubscriber
|
||||
tags:
|
||||
- { name: doctrine.event_listener, event: prePersist, lazy: true }
|
||||
- { name: doctrine.event_listener, event: preUpdate, lazy: true }
|
||||
|
||||
# Uncomment the following lines to define a service for the Timesheet Doctrine repository.
|
||||
# It's not mandatory to create these services, but if you use repositories a lot,
|
||||
# these services can simplify your code:
|
||||
# Uncomment the following lines to define a service for the Timesheet Doctrine repository
|
||||
#
|
||||
# app.post_repository:
|
||||
# app.timesheet_repository:
|
||||
# class: Doctrine\ORM\EntityRepository
|
||||
# factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
# arguments: [App\Entity\Timesheet]
|
||||
#
|
||||
# // traditional code inside a controller
|
||||
# $entityManager = $this->getDoctrine()->getManager();
|
||||
# $posts = $entityManager->getRepository('KimaiBundle:User')->findAll();
|
||||
#
|
||||
# // same code using repository services
|
||||
# $posts = $this->get('app.user_repository')->findAll();
|
||||
|
||||
# ================================================================================
|
||||
# FORMS
|
||||
@@ -84,29 +78,6 @@ services:
|
||||
tags:
|
||||
- { name: form.type }
|
||||
|
||||
# ================================================================================
|
||||
# CONSOLE COMMANDS
|
||||
# ================================================================================
|
||||
|
||||
App\Command\RunCodeSnifferCommand:
|
||||
arguments:
|
||||
$projectDirectory: "%kernel.project_dir%"
|
||||
|
||||
App\Command\RunUnitTestsCommand:
|
||||
class: App\Command\RunUnitTestsCommand
|
||||
arguments:
|
||||
$projectDirectory: "%kernel.project_dir%"
|
||||
|
||||
App\Command\RunIntegrationTestsCommand:
|
||||
class: App\Command\RunIntegrationTestsCommand
|
||||
arguments:
|
||||
$projectDirectory: "%kernel.project_dir%"
|
||||
|
||||
App\Command\InstallCommand:
|
||||
class: App\Command\InstallCommand
|
||||
arguments:
|
||||
$projectDirectory: "%kernel.project_dir%"
|
||||
|
||||
# ================================================================================
|
||||
# THEME
|
||||
# ================================================================================
|
||||
@@ -128,9 +99,3 @@ services:
|
||||
# class: "%avanzu_admin_theme.navbar_msg_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.messages, method: onListMessages }
|
||||
#
|
||||
# avanzu_admin_theme.setup_menu_listener:
|
||||
# class: "%avanzu_admin_theme.setup_menu_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupMenu }
|
||||
# - { name: kernel.event_listener, event: theme.breadcrumb, method: onSetupMenu }
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
|
||||
.error-page {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* ================================ NAVBAR ================================ */
|
||||
|
||||
.navbar-nav>li>a.ddt-large {
|
||||
@@ -42,6 +47,7 @@ li.open .ticktac i.running{
|
||||
.control-sidebar select {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* ================================ TOOLBAR ================================ */
|
||||
|
||||
.data_table .box-header>.box-tools {
|
||||
@@ -61,7 +67,7 @@ li.open .ticktac i.running{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.box-header .tools-left {
|
||||
.tools-left {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@@ -84,6 +90,11 @@ footer.main-footer {
|
||||
.login-page form label {
|
||||
padding-left: 5px;
|
||||
}
|
||||
/* ================================ PRINT ================================ */
|
||||
|
||||
@media print{
|
||||
.sf-toolbar, .control-sidebar {display: none !important;}
|
||||
}
|
||||
|
||||
/* ================================ DATA-TABLES ================================ */
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
$(document).ready(function () {
|
||||
|
||||
$('.toolbar form input').change(function (event) {
|
||||
$('.toolbar form').submit();
|
||||
});
|
||||
|
||||
$('.toolbar form select').change(function (event) {
|
||||
switch (event.target.id) {
|
||||
case 'customer':
|
||||
|
||||
@@ -19,8 +19,6 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
@@ -45,47 +43,6 @@ class ActivityController extends AbstractController
|
||||
return $this->getDoctrine()->getRepository(Activity::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return ActivityQuery
|
||||
*/
|
||||
protected function getQueryForRequest(Request $request)
|
||||
{
|
||||
$visibility = $request->get('visibility', ActivityQuery::SHOW_VISIBLE);
|
||||
if (strlen($visibility) == 0 || (int)$visibility != $visibility) {
|
||||
$visibility = ActivityQuery::SHOW_BOTH;
|
||||
}
|
||||
$pageSize = (int) $request->get('pageSize');
|
||||
$customer = $request->get('customer');
|
||||
$customer = !empty(trim($customer)) ? trim($customer) : null;
|
||||
$project = $request->get('project');
|
||||
$project = !empty(trim($project)) ? trim($project) : null;
|
||||
|
||||
if ($project !== null) {
|
||||
$repo = $this->getDoctrine()->getRepository(Project::class);
|
||||
$project = $repo->getById($project);
|
||||
if ($project !== null) {
|
||||
$customer = $project->getCustomer();
|
||||
} else {
|
||||
$customer = null;
|
||||
}
|
||||
} elseif ($customer !== null) {
|
||||
$repo = $this->getDoctrine()->getRepository(Customer::class);
|
||||
$customer = $repo->getById($customer);
|
||||
}
|
||||
|
||||
$query = new ActivityQuery();
|
||||
$query
|
||||
->setPageSize($pageSize)
|
||||
->setVisibility($visibility)
|
||||
->setCustomer($customer)
|
||||
->setProject($project)
|
||||
->setExclusiveVisibility(true)
|
||||
;
|
||||
|
||||
return $query ;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="admin_activity")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_activity_paginated")
|
||||
@@ -94,22 +51,33 @@ class ActivityController extends AbstractController
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = $this->getQueryForRequest($request);
|
||||
$query = new ActivityQuery();
|
||||
$query->setExclusiveVisibility(true);
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ActivityQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/activity.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $this->getToolbarForm($query)->createView(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/create", name="admin_activity_create")
|
||||
* @Method({"GET", "POST"})
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function createAction(Request $request)
|
||||
{
|
||||
@@ -120,6 +88,10 @@ class ActivityController extends AbstractController
|
||||
* @Route("/{id}/edit", name="admin_activity_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
* @Security("is_granted('edit', activity)")
|
||||
*
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function editAction(Activity $activity, Request $request)
|
||||
{
|
||||
@@ -204,16 +176,12 @@ class ActivityController extends AbstractController
|
||||
*/
|
||||
protected function getToolbarForm(ActivityQuery $query)
|
||||
{
|
||||
return $this->createForm(
|
||||
ActivityToolbarForm::class,
|
||||
$query,
|
||||
[
|
||||
'action' => $this->generateUrl('admin_activity_paginated', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]
|
||||
);
|
||||
return $this->createForm(ActivityToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_activity', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,13 +196,9 @@ class ActivityController extends AbstractController
|
||||
$url = $this->generateUrl('admin_activity_edit', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
return $this->createForm(
|
||||
ActivityEditForm::class,
|
||||
$activity,
|
||||
[
|
||||
'action' => $url,
|
||||
'method' => 'POST'
|
||||
]
|
||||
);
|
||||
return $this->createForm(ActivityEditForm::class, $activity, [
|
||||
'action' => $url,
|
||||
'method' => 'POST'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ use App\Entity\Customer;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\Toolbar\CustomerToolbarForm;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
@@ -43,44 +42,30 @@ class CustomerController extends AbstractController
|
||||
return $this->getDoctrine()->getRepository(Customer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return CustomerQuery
|
||||
*/
|
||||
protected function getQueryForRequest(Request $request)
|
||||
{
|
||||
$visibility = $request->get('visibility', CustomerQuery::SHOW_VISIBLE);
|
||||
if (strlen($visibility) == 0 || (int)$visibility != $visibility) {
|
||||
$visibility = CustomerQuery::SHOW_BOTH;
|
||||
}
|
||||
$pageSize = (int) $request->get('pageSize');
|
||||
|
||||
$query = new CustomerQuery();
|
||||
$query
|
||||
->setPageSize($pageSize)
|
||||
->setVisibility($visibility);
|
||||
|
||||
return $query ;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="admin_customer")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated")
|
||||
* @Method("GET")
|
||||
* @Cache(smaxage="10")
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = $this->getQueryForRequest($request);
|
||||
$query = new CustomerQuery();
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var CustomerQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
return $this->render('admin/customer.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $this->getToolbarForm($query)->createView(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,36 +44,6 @@ class ProjectController extends AbstractController
|
||||
return $this->getDoctrine()->getRepository(Project::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return ProjectQuery
|
||||
*/
|
||||
protected function getQueryForRequest(Request $request)
|
||||
{
|
||||
$visibility = $request->get('visibility', ProjectQuery::SHOW_VISIBLE);
|
||||
if (strlen($visibility) == 0 || (int)$visibility != $visibility) {
|
||||
$visibility = ProjectQuery::SHOW_BOTH;
|
||||
}
|
||||
$pageSize = (int) $request->get('pageSize');
|
||||
$customer = $request->get('customer');
|
||||
$customer = !empty(trim($customer)) ? trim($customer) : null;
|
||||
|
||||
if ($customer !== null) {
|
||||
$repo = $this->getDoctrine()->getRepository(Customer::class);
|
||||
$customer = $repo->getById($customer);
|
||||
}
|
||||
|
||||
$query = new ProjectQuery();
|
||||
$query
|
||||
->setPageSize($pageSize)
|
||||
->setVisibility($visibility)
|
||||
->setCustomer($customer)
|
||||
->setExclusiveVisibility(true)
|
||||
;
|
||||
|
||||
return $query ;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="admin_project")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated")
|
||||
@@ -82,16 +52,24 @@ class ProjectController extends AbstractController
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = $this->getQueryForRequest($request);
|
||||
$query = new ProjectQuery();
|
||||
$query->setExclusiveVisibility(true);
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var ProjectQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getDoctrine()->getRepository(Project::class)->findByQuery($query);
|
||||
|
||||
return $this->render('admin/project.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $this->getToolbarForm($query)->createView(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Form\Toolbar\TimesheetAdminToolbarForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use App\Controller\TimesheetControllerTrait;
|
||||
@@ -48,9 +50,16 @@ class TimesheetController extends AbstractController
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = $this->getQueryForRequest($request);
|
||||
$query = new TimesheetQuery();
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var TimesheetQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
@@ -58,7 +67,7 @@ class TimesheetController extends AbstractController
|
||||
'entries' => $entries,
|
||||
'page' => $page,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $this->getToolbarForm($query, 'admin_timesheet')->createView(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -133,15 +142,11 @@ class TimesheetController extends AbstractController
|
||||
*/
|
||||
protected function getCreateForm(Timesheet $entry)
|
||||
{
|
||||
return $this->createForm(
|
||||
TimesheetAdminForm::class,
|
||||
$entry,
|
||||
[
|
||||
'action' => $this->generateUrl('admin_timesheet_create'),
|
||||
'method' => 'POST',
|
||||
'currency' => Customer::DEFAULT_CURRENCY,
|
||||
]
|
||||
);
|
||||
return $this->createForm(TimesheetAdminForm::class, $entry, [
|
||||
'action' => $this->generateUrl('admin_timesheet_create'),
|
||||
'method' => 'POST',
|
||||
'currency' => Customer::DEFAULT_CURRENCY,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,17 +156,27 @@ class TimesheetController extends AbstractController
|
||||
*/
|
||||
protected function getEditForm(Timesheet $entry, $page)
|
||||
{
|
||||
return $this->createForm(
|
||||
TimesheetAdminForm::class,
|
||||
$entry,
|
||||
[
|
||||
'action' => $this->generateUrl('admin_timesheet_edit', [
|
||||
'id' => $entry->getId(),
|
||||
'page' => $page
|
||||
]),
|
||||
'method' => 'POST',
|
||||
'currency' => $entry->getActivity()->getProject()->getCustomer()->getCurrency(),
|
||||
]
|
||||
);
|
||||
return $this->createForm(TimesheetAdminForm::class, $entry, [
|
||||
'action' => $this->generateUrl('admin_timesheet_edit', [
|
||||
'id' => $entry->getId(),
|
||||
'page' => $page
|
||||
]),
|
||||
'method' => 'POST',
|
||||
'currency' => $entry->getActivity()->getProject()->getCustomer()->getCurrency(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(TimesheetQuery $query)
|
||||
{
|
||||
return $this->createForm(TimesheetAdminToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_timesheet', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
@@ -35,29 +34,6 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
class UserController extends AbstractController
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return UserQuery
|
||||
*/
|
||||
protected function getQueryForRequest(Request $request)
|
||||
{
|
||||
$visibility = $request->get('visibility', UserQuery::SHOW_VISIBLE);
|
||||
if (strlen($visibility) == 0 || (int)$visibility != $visibility) {
|
||||
$visibility = UserQuery::SHOW_BOTH;
|
||||
}
|
||||
$pageSize = (int) $request->get('pageSize');
|
||||
$userRole = $request->get('role');
|
||||
|
||||
$query = new UserQuery();
|
||||
$query
|
||||
->setPageSize($pageSize)
|
||||
->setVisibility($visibility)
|
||||
->setRole($userRole)
|
||||
;
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="admin_user")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated")
|
||||
@@ -65,16 +41,23 @@ class UserController extends AbstractController
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = $this->getQueryForRequest($request);
|
||||
$query = new UserQuery();
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var UserQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getDoctrine()->getRepository(User::class)->findByQuery($query);
|
||||
|
||||
return $this->render('admin/user.html.twig', [
|
||||
'entries' => $entries,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $this->getToolbarForm($query)->createView(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -120,16 +103,12 @@ class UserController extends AbstractController
|
||||
*/
|
||||
protected function getToolbarForm(UserQuery $query)
|
||||
{
|
||||
return $this->createForm(
|
||||
UserToolbarForm::class,
|
||||
$query,
|
||||
[
|
||||
'action' => $this->generateUrl('admin_user_paginated', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]
|
||||
);
|
||||
return $this->createForm(UserToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('admin_user_paginated', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,13 +117,9 @@ class UserController extends AbstractController
|
||||
*/
|
||||
private function createEditForm(User $user)
|
||||
{
|
||||
return $this->createForm(
|
||||
UserCreateType::class,
|
||||
$user,
|
||||
[
|
||||
'action' => $this->generateUrl('admin_user_create'),
|
||||
'method' => 'POST'
|
||||
]
|
||||
);
|
||||
return $this->createForm(UserCreateType::class, $user, [
|
||||
'action' => $this->generateUrl('admin_user_create'),
|
||||
'method' => 'POST'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
263
src/Controller/InvoiceController.php
Normal file
263
src/Controller/InvoiceController.php
Normal file
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\InvoiceTemplateForm;
|
||||
use App\Form\Toolbar\InvoiceToolbarForm;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Model\InvoiceModel;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Controller used to manage invoices.
|
||||
*
|
||||
* @Route("/invoice")
|
||||
* @Security("has_role('ROLE_TEAMLEAD')")
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceController extends AbstractController
|
||||
{
|
||||
|
||||
/**
|
||||
* @var ServiceInvoice
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* InvoiceController constructor.
|
||||
* @param ServiceInvoice $service
|
||||
*/
|
||||
public function __construct(ServiceInvoice $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InvoiceQuery
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getDefaultQuery()
|
||||
{
|
||||
$begin = new \DateTime('first day of this month');
|
||||
$end = new \DateTime('last day of this month');
|
||||
|
||||
$query = new InvoiceQuery();
|
||||
$query->setBegin($begin);
|
||||
$query->setEnd($end);
|
||||
$query->setUser($this->getUser());
|
||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \App\Repository\InvoiceTemplateRepository
|
||||
*/
|
||||
protected function getRepository()
|
||||
{
|
||||
return $this->getDoctrine()->getRepository(InvoiceTemplate::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/", name="invoice")
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
{
|
||||
if (!$this->getRepository()->hasTemplate()) {
|
||||
return $this->redirectToRoute('admin_invoice_template_create');
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
|
||||
$query = $this->getDefaultQuery();
|
||||
$form = $this->getToolbarForm($query);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var InvoiceQuery $query */
|
||||
$query = $form->getData();
|
||||
$query->setResultType(TimesheetQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
|
||||
if ($query->getCustomer() !== null) {
|
||||
$query->getBegin()->setTime(0, 0, 0);
|
||||
$query->getEnd()->setTime(23, 59, 59);
|
||||
|
||||
$queryBuilder = $this->getDoctrine()->getRepository(Timesheet::class)->findByQuery($query);
|
||||
$entries = $queryBuilder->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
$model = new InvoiceModel();
|
||||
$model->setQuery($query);
|
||||
$model->setEntries($entries);
|
||||
$model->setCustomer($query->getCustomer());
|
||||
|
||||
$action = null;
|
||||
if ($query->getTemplate() !== null) {
|
||||
$generator = $this->service->getNumberGeneratorByName($query->getTemplate()->getNumberGenerator());
|
||||
if ($generator === null) {
|
||||
throw new \Exception('Unknown number generator: ' . $query->getTemplate()->getNumberGenerator());
|
||||
}
|
||||
|
||||
$calculator = $this->service->getCalculatorByName($query->getTemplate()->getCalculator());
|
||||
if ($calculator === null) {
|
||||
throw new \Exception('Unknown invoice calculator: ' . $query->getTemplate()->getCalculator());
|
||||
}
|
||||
|
||||
$model->setTemplate($query->getTemplate());
|
||||
$model->setCalculator($calculator);
|
||||
$model->setNumberGenerator($generator);
|
||||
$action = $this->service->getRendererActionByName($query->getTemplate()->getRenderer());
|
||||
}
|
||||
|
||||
return $this->render('invoice/index.html.twig', [
|
||||
'model' => $model,
|
||||
'action' => $action,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/template", defaults={"page": 1}, name="admin_invoice_template")
|
||||
* @Route("/template/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_invoice_template_paginated")
|
||||
* @Method({"GET", "POST"})
|
||||
*
|
||||
* TODO permission
|
||||
*
|
||||
* @param $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function listTemplateAction($page, Request $request)
|
||||
{
|
||||
$templates = $this->getRepository()->findByQuery(new BaseQuery());
|
||||
return $this->render('invoice/templates.html.twig', [
|
||||
'entries' => $templates,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/{id}/edit", name="admin_invoice_template_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
*
|
||||
* TODO permission
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function editTemplateAction(InvoiceTemplate $template, Request $request)
|
||||
{
|
||||
return $this->renderTemplateForm($template, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/create", name="admin_invoice_template_create")
|
||||
* @Method({"GET", "POST"})
|
||||
*
|
||||
* TODO permission
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createTemplateAction(Request $request)
|
||||
{
|
||||
if (!$this->getRepository()->hasTemplate()) {
|
||||
$this->flashWarning('invoice.first_template');
|
||||
}
|
||||
return $this->renderTemplateForm(new InvoiceTemplate(), $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function renderTemplateForm(InvoiceTemplate $template, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($template);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($template);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_invoice_template');
|
||||
}
|
||||
|
||||
return $this->render('invoice/template_edit.html.twig', [
|
||||
'template' => $template,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function invoiceAction(InvoiceModel $model)
|
||||
{
|
||||
return $this->render('invoice/print.html.twig', [
|
||||
'model' => $model,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(InvoiceQuery $query)
|
||||
{
|
||||
return $this->createForm(InvoiceToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('invoice', []),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(InvoiceTemplate $template)
|
||||
{
|
||||
if ($template->getId() === null) {
|
||||
$url = $this->generateUrl('admin_invoice_template_create');
|
||||
} else {
|
||||
$url = $this->generateUrl('admin_invoice_template_edit', ['id' => $template->getId()]);
|
||||
}
|
||||
|
||||
return $this->createForm(InvoiceTemplateForm::class, $template, [
|
||||
'action' => $url,
|
||||
'method' => 'POST'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
@@ -43,10 +44,18 @@ class TimesheetController extends AbstractController
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
$query = $this->getQueryForRequest($request);
|
||||
$query->setUser($this->getUser());
|
||||
$query = new TimesheetQuery();
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var TimesheetQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
@@ -54,7 +63,7 @@ class TimesheetController extends AbstractController
|
||||
'entries' => $entries,
|
||||
'page' => $page,
|
||||
'query' => $query,
|
||||
'toolbarForm' => $this->getToolbarForm($query)->createView(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -201,4 +210,18 @@ class TimesheetController extends AbstractController
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(TimesheetQuery $query)
|
||||
{
|
||||
return $this->createForm(TimesheetToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('timesheet', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,8 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TimesheetRepository;
|
||||
|
||||
/**
|
||||
@@ -35,77 +30,6 @@ trait TimesheetControllerTrait
|
||||
return $this->getDoctrine()->getRepository(Timesheet::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
protected function getQueryForRequest(Request $request)
|
||||
{
|
||||
$activity = $request->get('activity');
|
||||
$activity = !empty(trim($activity)) ? trim($activity) : null;
|
||||
$project = $request->get('project');
|
||||
$project = !empty(trim($project)) ? trim($project) : null;
|
||||
$customer = $request->get('customer');
|
||||
$customer = !empty(trim($customer)) ? trim($customer) : null;
|
||||
$state = $request->get('state');
|
||||
$state = !empty(trim($state)) ? trim($state) : null;
|
||||
$pageSize = (int) $request->get('pageSize');
|
||||
|
||||
if ($activity !== null) {
|
||||
$repo = $this->getDoctrine()->getRepository(Activity::class);
|
||||
$activity = $repo->getById($activity);
|
||||
if ($activity !== null) {
|
||||
$project = $activity->getProject();
|
||||
if ($project !== null) {
|
||||
$customer = $project->getCustomer();
|
||||
}
|
||||
} else {
|
||||
$customer = null;
|
||||
$project = null;
|
||||
}
|
||||
} elseif ($project !== null) {
|
||||
$repo = $this->getDoctrine()->getRepository(Project::class);
|
||||
$project = $repo->getById($project);
|
||||
if ($project !== null) {
|
||||
$customer = $project->getCustomer();
|
||||
} else {
|
||||
$customer = null;
|
||||
}
|
||||
} elseif ($customer !== null) {
|
||||
$repo = $this->getDoctrine()->getRepository(Customer::class);
|
||||
$customer = $repo->getById($customer);
|
||||
}
|
||||
|
||||
$query = new TimesheetQuery();
|
||||
$query
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setCustomer($customer)
|
||||
->setPageSize($pageSize)
|
||||
->setState($state);
|
||||
|
||||
return $query ;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetQuery $query
|
||||
* @param string $route
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getToolbarForm(TimesheetQuery $query, $route = 'timesheet')
|
||||
{
|
||||
return $this->createForm(
|
||||
TimesheetToolbarForm::class,
|
||||
$query,
|
||||
[
|
||||
'action' => $this->generateUrl($route, [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entry
|
||||
* @param string $route
|
||||
|
||||
@@ -12,17 +12,23 @@
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
|
||||
/**
|
||||
* Sample data to load in the database when running tests or for development
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests or while development.
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class AppFixtures extends Fixture
|
||||
{
|
||||
use FixturesTrait;
|
||||
|
||||
const DEFAULT_PASSWORD = 'kitten';
|
||||
|
||||
@@ -31,6 +37,10 @@ class AppFixtures extends Fixture
|
||||
*/
|
||||
private $encoder;
|
||||
|
||||
/**
|
||||
* AppFixtures constructor.
|
||||
* @param UserPasswordEncoderInterface $encoder
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder)
|
||||
{
|
||||
$this->encoder = $encoder;
|
||||
@@ -44,117 +54,53 @@ class AppFixtures extends Fixture
|
||||
$this->loadUsers($manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
private function loadUsers(ObjectManager $manager)
|
||||
{
|
||||
$passwordEncoder = $this->encoder;
|
||||
|
||||
$claraCustomer = new User();
|
||||
$claraCustomer
|
||||
->setAlias('Clara Haynes')
|
||||
->setTitle('CFO')
|
||||
->setUsername('clara_customer')
|
||||
->setEmail('clara_customer@example.com')
|
||||
->setRoles(['ROLE_CUSTOMER'])
|
||||
->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y')
|
||||
->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD))
|
||||
;
|
||||
$manager->persist($claraCustomer);
|
||||
foreach ($this->getUserDefinition() as $userData) {
|
||||
$user = new User();
|
||||
$user
|
||||
->setAlias($userData[0])
|
||||
->setTitle($userData[1])
|
||||
->setUsername($userData[2])
|
||||
->setEmail($userData[3])
|
||||
->setRoles([$userData[4]])
|
||||
->setAvatar($userData[5])
|
||||
->setActive($userData[6])
|
||||
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
|
||||
;
|
||||
|
||||
$johnUser = new User();
|
||||
$johnUser
|
||||
->setAlias('John Doe')
|
||||
->setTitle('Developer')
|
||||
->setUsername('john_user')
|
||||
->setEmail('john_user@example.com')
|
||||
->setRoles(['ROLE_USER'])
|
||||
->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y')
|
||||
->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD))
|
||||
;
|
||||
$manager->persist($johnUser);
|
||||
$preference = new UserPreference();
|
||||
$preference->setName(UserPreference::HOURLY_RATE);
|
||||
$preference->setValue(rand(0, 100));
|
||||
$preference->setUser($user);
|
||||
$user->setPreferences([$preference]);
|
||||
|
||||
$deactiveUser = new User();
|
||||
$deactiveUser
|
||||
->setAlias('Chris Deactive')
|
||||
->setTitle('Developer (left company)')
|
||||
->setUsername('chris_user')
|
||||
->setEmail('chris_user@example.com')
|
||||
->setRoles(['ROLE_USER'])
|
||||
->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y')
|
||||
->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD))
|
||||
// inactive for testing user login and UI
|
||||
->setActive(false)
|
||||
;
|
||||
$manager->persist($deactiveUser);
|
||||
|
||||
$tonyTeamlead = new User();
|
||||
$tonyTeamlead
|
||||
->setAlias('Tony Maier')
|
||||
->setTitle('Head of Development')
|
||||
->setUsername('tony_teamlead')
|
||||
->setEmail('tony_teamlead@example.com')
|
||||
->setRoles(['ROLE_TEAMLEAD'])
|
||||
->setAvatar('https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg')
|
||||
->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD))
|
||||
;
|
||||
$manager->persist($tonyTeamlead);
|
||||
|
||||
$annaAdmin = new User();
|
||||
$annaAdmin
|
||||
->setAlias('Anna Smith')
|
||||
->setTitle('Administrator')
|
||||
->setUsername('anna_admin')
|
||||
->setEmail('anna_admin@example.com')
|
||||
->setRoles(['ROLE_ADMIN'])
|
||||
// no avatar to test default image!
|
||||
->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD))
|
||||
;
|
||||
$manager->persist($annaAdmin);
|
||||
|
||||
$susanSuper = new User();
|
||||
$susanSuper
|
||||
// no alias to test the username macros
|
||||
->setTitle('Super Administrator')
|
||||
->setUsername('susan_super')
|
||||
->setEmail('susan_super@example.com')
|
||||
->setRoles(['ROLE_SUPER_ADMIN'])
|
||||
->setAvatar('/bundles/avanzuadmintheme/img/avatar.png')
|
||||
->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD))
|
||||
;
|
||||
$manager->persist($susanSuper);
|
||||
$manager->persist($user);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
protected function getPhrases()
|
||||
/**
|
||||
* @return []
|
||||
*/
|
||||
protected function getUserDefinition()
|
||||
{
|
||||
return [
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit',
|
||||
'Pellentesque vitae velit ex',
|
||||
'Mauris dapibus risus quis suscipit vulputate',
|
||||
'Eros diam egestas libero eu vulputate risus',
|
||||
'In hac habitasse platea dictumst',
|
||||
'Morbi tempus commodo mattis',
|
||||
'Ut suscipit posuere justo at vulputate',
|
||||
'Ut eleifend mauris et risus ultrices egestas',
|
||||
'Aliquam sodales odio id eleifend tristique',
|
||||
'Urna nisl sollicitudin id varius orci quam id turpis',
|
||||
'Nulla porta lobortis ligula vel egestas',
|
||||
'Curabitur aliquam euismod dolor non ornare',
|
||||
'Sed varius a risus eget aliquam',
|
||||
'Nunc viverra elit ac laoreet suscipit',
|
||||
'Pellentesque et sapien pulvinar consectetur',
|
||||
['Clara Haynes', 'CFO', 'clara_customer', 'clara_customer@example.com', 'ROLE_CUSTOMER', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y', true],
|
||||
['John Doe', 'Developer', 'john_user', 'john_user@example.com', 'ROLE_USER', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', true],
|
||||
// inactive user to test login
|
||||
['Chris Deactive', 'Developer (left company)', 'chris_user', 'chris_user@example.com', 'ROLE_USER', 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y', false],
|
||||
['Tony Maier', 'Head of Development', 'tony_teamlead', 'tony_teamlead@example.com', 'ROLE_TEAMLEAD', 'https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg', true],
|
||||
// no avatar to test default image macro
|
||||
['Anna Smith', 'Administrator', 'anna_admin', 'anna_admin@example.com', 'ROLE_ADMIN', null, true],
|
||||
// no alias to test twig username macro
|
||||
[null, 'Super Administrator', 'susan_super', 'susan_super@example.com', 'ROLE_SUPER_ADMIN', '/bundles/avanzuadmintheme/img/avatar.png', true]
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRandomPhrase()
|
||||
{
|
||||
return $this->getRandomPostTitle();
|
||||
}
|
||||
|
||||
private function getRandomPostTitle()
|
||||
{
|
||||
$titles = $this->getPhrases();
|
||||
|
||||
return $titles[array_rand($titles)];
|
||||
}
|
||||
}
|
||||
|
||||
53
src/DataFixtures/FixturesTrait.php
Normal file
53
src/DataFixtures/FixturesTrait.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
/**
|
||||
* Defines reusable sample data.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
trait FixturesTrait
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getPhrases()
|
||||
{
|
||||
return [
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit',
|
||||
'Pellentesque vitae velit ex',
|
||||
'Mauris dapibus risus quis suscipit vulputate',
|
||||
'Eros diam egestas libero eu vulputate risus',
|
||||
'In hac habitasse platea dictumst',
|
||||
'Morbi tempus commodo mattis',
|
||||
'Ut suscipit posuere justo at vulputate',
|
||||
'Ut eleifend mauris et risus ultrices egestas',
|
||||
'Aliquam sodales odio id eleifend tristique',
|
||||
'Urna nisl sollicitudin id varius orci quam id turpis',
|
||||
'Nulla porta lobortis ligula vel egestas',
|
||||
'Curabitur aliquam euismod dolor non ornare',
|
||||
'Sed varius a risus eget aliquam',
|
||||
'Nunc viverra elit ac laoreet suscipit',
|
||||
'Pellentesque et sapien pulvinar consectetur',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getRandomPhrase()
|
||||
{
|
||||
$phrases = $this->getPhrases();
|
||||
return $phrases[array_rand($phrases)];
|
||||
}
|
||||
}
|
||||
65
src/DataFixtures/InvoiceFixtures.php
Normal file
65
src/DataFixtures/InvoiceFixtures.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests or while development.
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceFixtures extends Fixture
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$this->loadInvoiceTemplates($manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
*/
|
||||
private function loadInvoiceTemplates(ObjectManager $manager)
|
||||
{
|
||||
$template = new InvoiceTemplate();
|
||||
$template
|
||||
->setName('Invoice')
|
||||
->setTitle('Your company name')
|
||||
->setCompany('Kimai, Inc.')
|
||||
->setVat(19)
|
||||
->setDueDays(14)
|
||||
->setPaymentTerms(
|
||||
'I would like to thank you for your confidence and will gladly be there for you in the future.
|
||||
Please transfer the total amount within 14 days to the given account and use the invoice number as reference.'
|
||||
)
|
||||
->setAddress(
|
||||
'795 Folsom Ave, Suite 600
|
||||
San Francisco, CA 94107
|
||||
Phone: (804) 123-5432
|
||||
Email: info@almasaeedstudio.com'
|
||||
)
|
||||
;
|
||||
|
||||
$manager->persist($template);
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,22 @@ use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests. Execute this command to load the data:
|
||||
* functional tests or while development.
|
||||
*
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
* Execute this command to load the data:
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class TimesheetFixtures extends AppFixtures
|
||||
class TimesheetFixtures extends Fixture
|
||||
{
|
||||
use FixturesTrait;
|
||||
|
||||
const AMOUNT_TIMESHEET = 5000; // timesheet entries total
|
||||
const RATE_MIN = 10; // minimum rate for one hour
|
||||
const RATE_MAX = 80; // maximum rate for one hour
|
||||
@@ -185,11 +189,11 @@ class TimesheetFixtures extends AppFixtures
|
||||
$entry = new Customer();
|
||||
$entry
|
||||
->setCurrency($this->getRandomCurrency())
|
||||
->setVat(rand(0, 30))
|
||||
->setName($customerName . ($visible ? '' : '.'))
|
||||
->setAddress($this->getRandomLocation())
|
||||
->setComment($this->getRandomPhrase())
|
||||
->setVisible($visible)
|
||||
->setNumber('C0815-42-' . $i)
|
||||
->setCountry('DE') // TODO randomize country ?
|
||||
->setTimezone($allTimezones[rand(1, $amountTimezone)]);
|
||||
|
||||
|
||||
68
src/DependencyInjection/AppExtension.php
Normal file
68
src/DependencyInjection/AppExtension.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
|
||||
/**
|
||||
* This is the class that loads and manages your bundle configuration
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class AppExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container)
|
||||
{
|
||||
$configuration = new Configuration();
|
||||
try {
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
} catch (InvalidConfigurationException $e) {
|
||||
trigger_error('Found invalid "kimai" configuration: ' . $e->getMessage());
|
||||
$config = [];
|
||||
}
|
||||
|
||||
$this->createInvoiceParameter($config, $container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param ContainerBuilder $container
|
||||
*/
|
||||
private function createInvoiceParameter(array $config, ContainerBuilder $container)
|
||||
{
|
||||
$keys = ['renderer', 'calculator', 'number_generator'];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (!isset($config['invoice'][$key]) || 0 === count($config['invoice'][$key])) {
|
||||
throw new InvalidDefinitionException('Missing invoice configuration: kimai.invoice.' . $key);
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.invoice.' . $key, $config['invoice'][$key]);
|
||||
}
|
||||
|
||||
$container->setParameter('kimai.invoice', $config['invoice']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAlias()
|
||||
{
|
||||
return 'kimai';
|
||||
}
|
||||
}
|
||||
@@ -29,16 +29,46 @@ class Configuration implements ConfigurationInterface
|
||||
$treeBuilder = new TreeBuilder();
|
||||
$rootNode = $treeBuilder->root('kimai');
|
||||
|
||||
// FIXME add kimai config
|
||||
$rootNode
|
||||
->children()
|
||||
->arrayNode('timesheet')
|
||||
->children()
|
||||
->integerNode('round_record')->end()
|
||||
->children()
|
||||
->integerNode('rounding')->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('invoice')
|
||||
->children()
|
||||
->arrayNode('renderer')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue(array(
|
||||
'default' => 'App\Controller\InvoiceController::invoiceAction',
|
||||
))
|
||||
->end()
|
||||
->arrayNode('calculator')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue(array(
|
||||
'default' => 'App\Invoice\DefaultCalculator',
|
||||
))
|
||||
->end()
|
||||
->arrayNode('number_generator')
|
||||
->requiresAtLeastOneElement()
|
||||
->useAttributeAsKey('key')
|
||||
->isRequired()
|
||||
->prototype('scalar')->end()
|
||||
->defaultValue(array(
|
||||
'default' => 'App\Invoice\DateNumberGenerator',
|
||||
))
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
->end();
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
|
||||
@@ -60,23 +60,25 @@ class TimesheetSubscriber implements EventSubscriber
|
||||
$entity = $args->getObject();
|
||||
|
||||
if ($entity instanceof Timesheet) {
|
||||
$duration = 0;
|
||||
if ($entity->getEnd() !== null) {
|
||||
$duration = $entity->getEnd()->getTimestamp() - $entity->getBegin()->getTimestamp();
|
||||
$entity->setDuration($duration);
|
||||
|
||||
// TODO allow to set hourly rate on activity, project and customer and prefer these
|
||||
|
||||
$hourlyRate = 0;
|
||||
foreach ($entity->getUser()->getPreferences() as $preference) {
|
||||
if ($preference->getName() == UserPreference::HOURLY_RATE) {
|
||||
$hourlyRate = (int) $preference->getValue();
|
||||
}
|
||||
}
|
||||
|
||||
$rate = $hourlyRate * ($duration / 3600);
|
||||
$rate = $this->calculateRate($entity);
|
||||
$entity->setRate($rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $entity
|
||||
* @return float
|
||||
*/
|
||||
protected function calculateRate(Timesheet $entity)
|
||||
{
|
||||
$hourlyRate = (float) $entity->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0);
|
||||
return (float) $hourlyRate * ($entity->getDuration() / 3600);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,14 @@ class Customer
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="number", type="string", length=50, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $number;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
@@ -73,15 +81,6 @@ class Customer
|
||||
*/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="vat", type="integer", length=2, nullable=true)
|
||||
* @Assert\Range(min = 0, max = 99)
|
||||
*
|
||||
*/
|
||||
private $vat;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
@@ -92,7 +91,7 @@ class Customer
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="street", type="text", length=65535, nullable=true)
|
||||
* @ORM\Column(name="address", type="text", length=65535, nullable=true)
|
||||
*/
|
||||
private $address;
|
||||
|
||||
@@ -167,13 +166,11 @@ class Customer
|
||||
* Set name
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -182,22 +179,38 @@ class Customer
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $number
|
||||
* @return Customer
|
||||
*/
|
||||
public function setNumber(string $number)
|
||||
{
|
||||
$this->number = $number;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNumber(): ?string
|
||||
{
|
||||
return $this->number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set comment
|
||||
*
|
||||
* @param string $comment
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setComment($comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -215,13 +228,11 @@ class Customer
|
||||
* Set visible
|
||||
*
|
||||
* @param boolean $visible
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setVisible($visible)
|
||||
{
|
||||
$this->visible = $visible;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -239,13 +250,11 @@ class Customer
|
||||
* Set company
|
||||
*
|
||||
* @param string $company
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setCompany($company)
|
||||
{
|
||||
$this->company = $company;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -259,41 +268,15 @@ class Customer
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set vat
|
||||
*
|
||||
* @param integer $vat
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setVat($vat)
|
||||
{
|
||||
$this->vat = $vat;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vat
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getVat()
|
||||
{
|
||||
return $this->vat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set contact
|
||||
*
|
||||
* @param string $contact
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setContact($contact)
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -307,6 +290,16 @@ class Customer
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $address
|
||||
* @return Customer
|
||||
*/
|
||||
public function setAddress($address)
|
||||
{
|
||||
$this->address = $address;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -315,29 +308,15 @@ class Customer
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $address
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setAddress($address)
|
||||
{
|
||||
$this->address = $address;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set country
|
||||
*
|
||||
* @param string $country
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setCountry($country)
|
||||
{
|
||||
$this->country = $country;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -351,6 +330,16 @@ class Customer
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $currency
|
||||
* @return Customer
|
||||
*/
|
||||
public function setCurrency($currency)
|
||||
{
|
||||
$this->currency = $currency;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -359,28 +348,15 @@ class Customer
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $currency
|
||||
* @return $this
|
||||
*/
|
||||
public function setCurrency($currency)
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set phone
|
||||
*
|
||||
* @param string $phone
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setPhone($phone)
|
||||
{
|
||||
$this->phone = $phone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -398,13 +374,11 @@ class Customer
|
||||
* Set fax
|
||||
*
|
||||
* @param string $fax
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setFax($fax)
|
||||
{
|
||||
$this->fax = $fax;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -422,13 +396,11 @@ class Customer
|
||||
* Set mobile
|
||||
*
|
||||
* @param string $mobile
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setMobile($mobile)
|
||||
{
|
||||
$this->mobile = $mobile;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -446,13 +418,11 @@ class Customer
|
||||
* Set mail
|
||||
*
|
||||
* @param string $mail
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setMail($mail)
|
||||
{
|
||||
$this->mail = $mail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -470,13 +440,11 @@ class Customer
|
||||
* Set homepage
|
||||
*
|
||||
* @param string $homepage
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setHomepage($homepage)
|
||||
{
|
||||
$this->homepage = $homepage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -494,13 +462,11 @@ class Customer
|
||||
* Set timezone
|
||||
*
|
||||
* @param string $timezone
|
||||
*
|
||||
* @return Customer
|
||||
*/
|
||||
public function setTimezone($timezone)
|
||||
{
|
||||
$this->timezone = $timezone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -516,12 +482,11 @@ class Customer
|
||||
|
||||
/**
|
||||
* @param Project[] $projects
|
||||
* @return $this
|
||||
* @return Customer
|
||||
*/
|
||||
public function setProjects($projects)
|
||||
{
|
||||
$this->projects = $projects;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
317
src/Entity/InvoiceTemplate.php
Normal file
317
src/Entity/InvoiceTemplate.php
Normal file
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* InvoiceTemplate
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\InvoiceTemplateRepository")
|
||||
* @ORM\Table(
|
||||
* name="invoice_templates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceTemplate
|
||||
{
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=255, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="title", type="string", length=255, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="company", type="string", length=255, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $company;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="address", type="text", length=65535, nullable=true)
|
||||
*/
|
||||
private $address;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="due_days", type="integer", length=3, nullable=false)
|
||||
* @Assert\Range(min = 0, max = 999)
|
||||
*/
|
||||
private $dueDays = 30;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="vat", type="integer", length=2, nullable=true)
|
||||
* @Assert\Range(min = 0, max = 99)
|
||||
*/
|
||||
private $vat = 0.00;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="calculator", type="string", length=20, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $calculator = 'default';
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="number_generator", type="string", length=20, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $numberGenerator = 'default';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="renderer", type="string", length=20, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $renderer = 'default';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="payment_terms", type="text", length=65535, nullable=true)
|
||||
*/
|
||||
private $paymentTerms;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name
|
||||
*
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setTitle(string $title)
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAddress(): ?string
|
||||
{
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $address
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setAddress(string $address)
|
||||
{
|
||||
$this->address = $address;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNumberGenerator(): ?string
|
||||
{
|
||||
return $this->numberGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $numberGenerator
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setNumberGenerator(string $numberGenerator)
|
||||
{
|
||||
$this->numberGenerator = $numberGenerator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDueDays(): ?int
|
||||
{
|
||||
return $this->dueDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dueDays
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setDueDays(int $dueDays)
|
||||
{
|
||||
$this->dueDays = $dueDays;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getVat(): ?float
|
||||
{
|
||||
return $this->vat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $vat
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setVat(float $vat)
|
||||
{
|
||||
$this->vat = $vat;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCompany(): ?string
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $company
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setCompany(string $company)
|
||||
{
|
||||
$this->company = $company;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRenderer(): string
|
||||
{
|
||||
return $this->renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $renderer
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setRenderer(string $renderer)
|
||||
{
|
||||
$this->renderer = $renderer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCalculator(): string
|
||||
{
|
||||
return $this->calculator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $calculator
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setCalculator(string $calculator)
|
||||
{
|
||||
$this->calculator = $calculator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPaymentTerms(): ?string
|
||||
{
|
||||
return $this->paymentTerms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $paymentTerms
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function setPaymentTerms(string $paymentTerms)
|
||||
{
|
||||
$this->paymentTerms = $paymentTerms;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class Timesheet
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="rate", type="decimal", precision=10, scale=2, nullable=false)
|
||||
*/
|
||||
@@ -150,7 +150,6 @@ class Timesheet
|
||||
* Set duration
|
||||
*
|
||||
* @param integer $duration
|
||||
*
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setDuration($duration)
|
||||
@@ -182,7 +181,6 @@ class Timesheet
|
||||
* Set user
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setUser(User $user)
|
||||
@@ -205,7 +203,6 @@ class Timesheet
|
||||
* Set activity
|
||||
*
|
||||
* @param Activity $activity
|
||||
*
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setActivity($activity)
|
||||
@@ -228,7 +225,6 @@ class Timesheet
|
||||
* Set description
|
||||
*
|
||||
* @param string $description
|
||||
*
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setDescription($description)
|
||||
@@ -250,8 +246,7 @@ class Timesheet
|
||||
/**
|
||||
* Set rate
|
||||
*
|
||||
* @param string $rate
|
||||
*
|
||||
* @param float $rate
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setRate($rate)
|
||||
@@ -263,7 +258,7 @@ class Timesheet
|
||||
/**
|
||||
* Get rate
|
||||
*
|
||||
* @return string
|
||||
* @return float
|
||||
*/
|
||||
public function getRate()
|
||||
{
|
||||
|
||||
@@ -369,6 +369,35 @@ class User implements UserInterface, AdvancedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return UserPreference|null
|
||||
*/
|
||||
public function getPreference(string $name)
|
||||
{
|
||||
foreach ($this->preferences as $preference) {
|
||||
if ($preference->getName() == $name) {
|
||||
return $preference;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @param null $default
|
||||
* @return bool|int|null|string
|
||||
*/
|
||||
public function getPreferenceValue($name, $default = null)
|
||||
{
|
||||
$preference = $this->getPreference($name);
|
||||
if ($preference === null) {
|
||||
return $default;
|
||||
}
|
||||
return $preference->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserPreference $preference
|
||||
* @return User
|
||||
|
||||
@@ -59,6 +59,7 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
$isLoggedIn = $auth->isGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||
$isUser = $isLoggedIn && $auth->isGranted('ROLE_USER');
|
||||
$isTeamlead = $isLoggedIn && $auth->isGranted('ROLE_USER');
|
||||
|
||||
if (!$isLoggedIn || !$isUser) {
|
||||
return;
|
||||
@@ -68,6 +69,14 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
$menu->addItem(
|
||||
new MenuItemModel('timesheet', 'menu.timesheet', 'timesheet', [], 'fa fa-clock-o')
|
||||
);
|
||||
|
||||
if (!$isTeamlead) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu->addItem(
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fa fa-print')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,6 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PercentType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -40,84 +39,57 @@ class CustomerEditForm extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
// string - length 255
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'label.name',
|
||||
])
|
||||
// text
|
||||
->add('number', TextType::class, [
|
||||
'label' => 'label.customer_number',
|
||||
])
|
||||
->add('comment', TextareaType::class, [
|
||||
'label' => 'label.comment',
|
||||
'required' => false,
|
||||
])
|
||||
// do not allow project selection:
|
||||
// 1. it is a bad UX
|
||||
// 2. what should happen if they are detached?
|
||||
/*
|
||||
->add('projects', EntityType::class, [
|
||||
'label' => 'label.project',
|
||||
'class' => 'Kimai:Project',
|
||||
'multiple' => true,
|
||||
'expanded' => true
|
||||
])
|
||||
*/
|
||||
// boolean
|
||||
->add('visible', VisibilityType::class, [
|
||||
'label' => 'label.visible',
|
||||
])
|
||||
// string - length 255
|
||||
->add('company', TextType::class, [
|
||||
'label' => 'label.company',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 255
|
||||
->add('vat', PercentType::class, [
|
||||
'label' => 'label.vat',
|
||||
'type' => 'integer',
|
||||
])
|
||||
// string - length 255
|
||||
->add('contact', TextType::class, [
|
||||
'label' => 'label.contact',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 255
|
||||
->add('address', TextareaType::class, [
|
||||
'label' => 'label.address',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 2
|
||||
->add('country', CountryType::class, [
|
||||
'label' => 'label.country',
|
||||
])
|
||||
// string - length 3
|
||||
->add('currency', CurrencyType::class, [
|
||||
'label' => 'label.currency',
|
||||
])
|
||||
// string - length 255
|
||||
->add('phone', TelType::class, [
|
||||
'label' => 'label.phone',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 255
|
||||
->add('fax', TelType::class, [
|
||||
'label' => 'label.fax',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 255
|
||||
->add('mobile', TelType::class, [
|
||||
'label' => 'label.mobile',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 255
|
||||
->add('mail', EmailType::class, [
|
||||
'label' => 'label.email',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 255
|
||||
->add('homepage', UrlType::class, [
|
||||
'label' => 'label.homepage',
|
||||
'required' => false,
|
||||
])
|
||||
// string - length 255
|
||||
->add('timezone', TimezoneType::class, [
|
||||
'label' => 'label.timezone',
|
||||
])
|
||||
|
||||
81
src/Form/InvoiceTemplateForm.php
Normal file
81
src/Form/InvoiceTemplateForm.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Form\Type\InvoiceCalculatorType;
|
||||
use App\Form\Type\InvoiceNumberGeneratorType;
|
||||
use App\Form\Type\InvoiceRendererType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PercentType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the form used to manipulate invoice templates.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceTemplateForm extends AbstractType
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'label.name',
|
||||
])
|
||||
->add('title', TextType::class, [
|
||||
'label' => 'label.title',
|
||||
])
|
||||
->add('company', TextType::class, [
|
||||
'label' => 'label.company',
|
||||
])
|
||||
->add('address', TextareaType::class, [
|
||||
'label' => 'label.address',
|
||||
'required' => false,
|
||||
])
|
||||
->add('paymentTerms', TextareaType::class, [
|
||||
'label' => 'label.payment_terms',
|
||||
'required' => false,
|
||||
])
|
||||
->add('dueDays', TextType::class, [
|
||||
'label' => 'label.due_days',
|
||||
])
|
||||
->add('vat', PercentType::class, [
|
||||
'label' => 'label.vat',
|
||||
'type' => 'integer',
|
||||
])
|
||||
->add('renderer', InvoiceRendererType::class, [])
|
||||
->add('calculator', InvoiceCalculatorType::class, [])
|
||||
->add('numberGenerator', InvoiceNumberGeneratorType::class, [])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => InvoiceTemplate::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'admin_invoice_template',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,33 @@
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\PageSizeType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\UserRoleType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\VisibilityType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
/**
|
||||
* Defines the base form used for all toolbars.
|
||||
*
|
||||
* Extend this class and stack the elements defined here, they are coupled to each other and with the toolbar.js.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
abstract class AbstractToolbarForm extends AbstractType
|
||||
{
|
||||
|
||||
/**
|
||||
* Dirty hack to enable easy handling of GET form in controller and javascript.
|
||||
*Cleans up the name of all form elents (and unfortunately of the form itself).
|
||||
@@ -30,4 +48,135 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addUserChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('user', UserType::class, [
|
||||
'label' => 'label.user',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addCustomerChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'query_builder' => function (CustomerRepository $repo) {
|
||||
$query = new CustomerQuery();
|
||||
$query->setVisibility(CustomerQuery::SHOW_BOTH); // this field is the reason for the query here
|
||||
$query->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addVisibilityChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('visibility', VisibilityType::class, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addPageSizeChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('pageSize', PageSizeType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addUserRoleChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('role', UserRoleType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addStartDateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('begin', DateType::class, [
|
||||
'label' => 'label.begin',
|
||||
'widget' => 'single_text',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addEndDateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('end', DateType::class, [
|
||||
'label' => 'label.end',
|
||||
'widget' => 'single_text',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addProjectChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['customer']) || empty($data['customer'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('project', ProjectType::class, array(
|
||||
'group_by' => null,
|
||||
'required' => false,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data) {
|
||||
$qb = $repo->builderForEntityType();
|
||||
$qb->where('p.customer = :customer')->setParameter('customer', $data['customer']);
|
||||
return $qb;
|
||||
},
|
||||
));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addActivityChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$data = $event->getData();
|
||||
if (!isset($data['project']) || empty($data['project'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getForm()->add('activity', ActivityType::class, array(
|
||||
'group_by' => null,
|
||||
'required' => false,
|
||||
'query_builder' => function (ActivityRepository $repo) use ($data) {
|
||||
$qb = $repo->builderForEntityType();
|
||||
$qb->where('a.project = :project')->setParameter('project', $data['project']);
|
||||
return $qb;
|
||||
},
|
||||
));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace App\Form\Toolbar;
|
||||
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
|
||||
/**
|
||||
@@ -21,7 +20,7 @@ use App\Repository\Query\ActivityQuery;
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class ActivityToolbarForm extends ProjectToolbarForm
|
||||
class ActivityToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -29,25 +28,10 @@ class ActivityToolbarForm extends ProjectToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
/** @var ActivityQuery $query */
|
||||
$query = $options['data'];
|
||||
|
||||
if ($query->getCustomer() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$choices = [];
|
||||
foreach ($query->getCustomer()->getProjects() as $project) {
|
||||
$choices[] = $project;
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('project', ProjectType::class, [
|
||||
'required' => false,
|
||||
'choices' => $choices,
|
||||
]);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Toolbar\VisibilityToolbarForm;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
|
||||
@@ -20,9 +20,18 @@ use App\Repository\Query\CustomerQuery;
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class CustomerToolbarForm extends VisibilityToolbarForm
|
||||
class CustomerToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
58
src/Form/Toolbar/InvoiceToolbarForm.php
Normal file
58
src/Form/Toolbar/InvoiceToolbarForm.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Type\InvoiceTemplateType;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the form used for filtering timesheet entries for invoices.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addTemplateChoice($builder);
|
||||
$this->addUserChoice($builder);
|
||||
$this->addStartDateChoice($builder);
|
||||
$this->addEndDateChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
}
|
||||
|
||||
protected function addTemplateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('template', InvoiceTemplateType::class, [
|
||||
'required' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => InvoiceQuery::class,
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Type\PageSizeType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
|
||||
/**
|
||||
* Defines the base form used for all toolbars with pageSizes.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class PagedToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
/** @var CustomerQuery $query */
|
||||
$query = $options['data'];
|
||||
|
||||
$builder
|
||||
->add('pageSize', PageSizeType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,8 @@
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Toolbar\VisibilityToolbarForm;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
|
||||
/**
|
||||
@@ -24,7 +20,7 @@ use App\Repository\Query\ProjectQuery;
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class ProjectToolbarForm extends VisibilityToolbarForm
|
||||
class ProjectToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -32,19 +28,9 @@ class ProjectToolbarForm extends VisibilityToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder
|
||||
->add('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'query_builder' => function (CustomerRepository $repo) {
|
||||
$query = new CustomerQuery();
|
||||
$query->setVisibility(CustomerQuery::SHOW_BOTH); // this field is the reason for the query here
|
||||
$query->setResultType(CustomerQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
])
|
||||
;
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,28 +11,26 @@
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Type\VisibilityType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* Defines the form used for filtering entities with a "visibility" field.
|
||||
* Defines the form used for filtering the admin timesheet.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class VisibilityToolbarForm extends PagedToolbarForm
|
||||
class TimesheetAdminToolbarForm extends TimesheetToolbarForm
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder
|
||||
->add('visibility', VisibilityType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addUserChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ namespace App\Form\Toolbar;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
|
||||
/**
|
||||
@@ -22,7 +21,7 @@ use App\Repository\Query\TimesheetQuery;
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class TimesheetToolbarForm extends ActivityToolbarForm
|
||||
class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -30,42 +29,26 @@ class TimesheetToolbarForm extends ActivityToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('state', ChoiceType::class, [
|
||||
'label' => 'label.entryState',
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
])
|
||||
;
|
||||
parent::buildForm($builder, $options);
|
||||
$this->addActivityChoice($builder, $options['data']);
|
||||
|
||||
$builder->remove('visibility');
|
||||
$this->addTimesheetStateChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param TimesheetQuery $query
|
||||
*/
|
||||
protected function addActivityChoice(FormBuilderInterface $builder, TimesheetQuery $query)
|
||||
protected function addTimesheetStateChoice(FormBuilderInterface $builder)
|
||||
{
|
||||
if ($query->getProject() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$choices = [];
|
||||
foreach ($query->getProject()->getActivities() as $activity) {
|
||||
$choices[] = $activity;
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('activity', ActivityType::class, [
|
||||
'required' => false,
|
||||
'choices' => $choices,
|
||||
]);
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
'label' => 'label.entryState',
|
||||
'choices' => [
|
||||
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Form\Type\UserRoleType;
|
||||
use App\Repository\Query\UserQuery;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
@@ -21,7 +20,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class UserToolbarForm extends VisibilityToolbarForm
|
||||
class UserToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -29,13 +28,9 @@ class UserToolbarForm extends VisibilityToolbarForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder
|
||||
->add('role', UserRoleType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addUserRoleChoice($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
66
src/Form/Type/InvoiceCalculatorType.php
Normal file
66
src/Form/Type/InvoiceCalculatorType.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to select an invoice calculator.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceCalculatorType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var ServiceInvoice
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* InvoiceCalculatorType constructor.
|
||||
* @param ServiceInvoice $service
|
||||
*/
|
||||
public function __construct(ServiceInvoice $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$renderer = [];
|
||||
foreach ($this->service->getCalculator() as $name => $class) {
|
||||
$renderer[$name] = $name;
|
||||
}
|
||||
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.invoice_calculator',
|
||||
'choices' => $renderer,
|
||||
'choice_label' => function($renderer) {
|
||||
return 'invoice_calculator.' . $renderer;
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
66
src/Form/Type/InvoiceNumberGeneratorType.php
Normal file
66
src/Form/Type/InvoiceNumberGeneratorType.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to select an invoice number generator.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceNumberGeneratorType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var ServiceInvoice
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* InvoiceNumberGeneratorType constructor.
|
||||
* @param ServiceInvoice $service
|
||||
*/
|
||||
public function __construct(ServiceInvoice $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$renderer = [];
|
||||
foreach ($this->service->getNumberGenerator() as $name => $class) {
|
||||
$renderer[$name] = $name;
|
||||
}
|
||||
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.invoice_number_generator',
|
||||
'choices' => $renderer,
|
||||
'choice_label' => function($renderer) {
|
||||
return 'invoice_number_generator.' . $renderer;
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
66
src/Form/Type/InvoiceRendererType.php
Normal file
66
src/Form/Type/InvoiceRendererType.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to select an invoice renderer.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceRendererType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var ServiceInvoice
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* InvoiceRendererType constructor.
|
||||
* @param ServiceInvoice $service
|
||||
*/
|
||||
public function __construct(ServiceInvoice $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$renderer = [];
|
||||
foreach ($this->service->getRenderer() as $name => $action) {
|
||||
$renderer[$name] = $name;
|
||||
}
|
||||
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.invoice_renderer',
|
||||
'choices' => $renderer,
|
||||
'choice_label' => function($renderer) {
|
||||
return 'invoice_renderer.' . $renderer;
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
45
src/Form/Type/InvoiceTemplateType.php
Normal file
45
src/Form/Type/InvoiceTemplateType.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Custom form field type to select an invoice template.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceTemplateType extends AbstractType
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.template',
|
||||
'class' => InvoiceTemplate::class,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return EntityType::class;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use App\Entity\Project;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
|
||||
/**
|
||||
* Custom form field type to select a project.
|
||||
@@ -33,7 +32,7 @@ class ProjectType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.project',
|
||||
'class' => 'Kimai:Project',
|
||||
'class' => Project::class,
|
||||
'choice_label' => 'name',
|
||||
'group_by' => function (Project $project, $key, $index) {
|
||||
return $project->getCustomer()->getName();
|
||||
|
||||
@@ -32,6 +32,7 @@ class VisibilityType extends AbstractType
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.visible',
|
||||
'choices' => [
|
||||
'both' => VisibilityQuery::SHOW_BOTH,
|
||||
'yes' => VisibilityQuery::SHOW_VISIBLE,
|
||||
'no' => VisibilityQuery::SHOW_HIDDEN,
|
||||
],
|
||||
|
||||
48
src/Invoice/CalculatorInterface.php
Normal file
48
src/Invoice/CalculatorInterface.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Model\InvoiceModel;
|
||||
|
||||
/**
|
||||
* CalculatorInterface defines all methods for any invoice price calculator.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
interface CalculatorInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
*/
|
||||
public function setModel(InvoiceModel $model);
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getSubtotal(): float;
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getTax(): float;
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getTotal(): float;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCurrency(): string;
|
||||
}
|
||||
44
src/Invoice/DateNumberGenerator.php
Normal file
44
src/Invoice/DateNumberGenerator.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Model\InvoiceModel;
|
||||
|
||||
/**
|
||||
* Class DateNumberGenerator generates the invoice number based on the current day.
|
||||
* It will create duplicate IDs if you create multiple invoices per day.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class DateNumberGenerator implements NumberGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @var InvoiceModel
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
*/
|
||||
public function setModel(InvoiceModel $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInvoiceNumber(): string
|
||||
{
|
||||
return date('ymd');
|
||||
}
|
||||
}
|
||||
94
src/Invoice/DefaultCalculator.php
Normal file
94
src/Invoice/DefaultCalculator.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Model\InvoiceModel;
|
||||
|
||||
/**
|
||||
* Class DefaultCalculator works on all given entries using:
|
||||
* - the customers currency
|
||||
* - the invoice template vat rate
|
||||
* - the entries rate
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class DefaultCalculator implements CalculatorInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $currency;
|
||||
|
||||
/**
|
||||
* @var InvoiceModel
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
*/
|
||||
public function setModel(InvoiceModel $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getSubtotal(): float
|
||||
{
|
||||
$amount = 0;
|
||||
foreach ($this->model->getEntries() as $entry) {
|
||||
$amount += $entry->getRate();
|
||||
}
|
||||
return round($amount, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getVat()
|
||||
{
|
||||
return $this->model->getTemplate()->getVat();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getTax(): float
|
||||
{
|
||||
$vat = $this->getVat();
|
||||
if ($vat == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$percent = $vat / 100.00;
|
||||
return round($this->getSubtotal() * $percent, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
*/
|
||||
public function getTotal(): float
|
||||
{
|
||||
return $this->getSubtotal() + $this->getTax();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCurrency(): string
|
||||
{
|
||||
return $this->model->getCustomer()->getCurrency();
|
||||
}
|
||||
}
|
||||
33
src/Invoice/NumberGeneratorInterface.php
Normal file
33
src/Invoice/NumberGeneratorInterface.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Model\InvoiceModel;
|
||||
|
||||
/**
|
||||
* Class NumberGeneratorInterface defines all methods that invoice number generator have to implement.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
interface NumberGeneratorInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
*/
|
||||
public function setModel(InvoiceModel $model);
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInvoiceNumber(): string;
|
||||
}
|
||||
43
src/Invoice/RandomNumberGenerator.php
Normal file
43
src/Invoice/RandomNumberGenerator.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Model\InvoiceModel;
|
||||
|
||||
/**
|
||||
* Class RandomNumberGenerator is meant for testing purpose only.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class RandomNumberGenerator implements NumberGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @var InvoiceModel
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @param InvoiceModel $model
|
||||
*/
|
||||
public function setModel(InvoiceModel $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInvoiceNumber(): string
|
||||
{
|
||||
return rand(1000000, 9999999);
|
||||
}
|
||||
}
|
||||
106
src/Invoice/ServiceInvoice.php
Normal file
106
src/Invoice/ServiceInvoice.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
/**
|
||||
* A service to manage the invoice configuration:
|
||||
* - invoice number generator
|
||||
* - invoice sum calculator
|
||||
* - template renderer
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class ServiceInvoice
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
/**
|
||||
* ServiceInvoice constructor.
|
||||
* @param array $invoiceConfig
|
||||
*/
|
||||
public function __construct(array $invoiceConfig)
|
||||
{
|
||||
$this->config = $invoiceConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getNumberGenerator()
|
||||
{
|
||||
return $this->config['number_generator'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return NumberGeneratorInterface|null
|
||||
*/
|
||||
public function getNumberGeneratorByName(string $name)
|
||||
{
|
||||
foreach ($this->getNumberGenerator() as $key => $class) {
|
||||
if ($key === $name) {
|
||||
return new $class();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getCalculator()
|
||||
{
|
||||
return $this->config['calculator'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return CalculatorInterface|null
|
||||
*/
|
||||
public function getCalculatorByName(string $name)
|
||||
{
|
||||
foreach ($this->getCalculator() as $key => $class) {
|
||||
if ($key === $name) {
|
||||
return new $class();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of invoice renderer, which will consist of a unique name and a controller action.
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getRenderer()
|
||||
{
|
||||
return $this->config['renderer'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $renderer
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRendererActionByName($renderer)
|
||||
{
|
||||
foreach ($this->config['renderer'] as $name => $action) {
|
||||
if ($name == $renderer) {
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\DependencyInjection\AppExtension;
|
||||
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
@@ -38,6 +39,8 @@ class Kernel extends BaseKernel
|
||||
|
||||
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
|
||||
{
|
||||
$container->registerExtension(new AppExtension());
|
||||
|
||||
$container->setParameter('container.autowiring.strict_mode', true);
|
||||
$container->setParameter('container.dumper.inline_class_loader', true);
|
||||
$confDir = $this->getProjectDir().'/config';
|
||||
|
||||
186
src/Model/InvoiceModel.php
Normal file
186
src/Model/InvoiceModel.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Invoice\CalculatorInterface;
|
||||
use App\Invoice\DateNumberGenerator;
|
||||
use App\Invoice\DefaultCalculator;
|
||||
use App\Invoice\NumberGeneratorInterface;
|
||||
use App\Repository\Query\InvoiceQuery;
|
||||
|
||||
/**
|
||||
* Class InvoiceModel is the ONLY value that a renderer template receives for generating the invoice.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceModel
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*/
|
||||
protected $customer;
|
||||
|
||||
/**
|
||||
* @var InvoiceQuery
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]
|
||||
*/
|
||||
protected $entries;
|
||||
|
||||
/**
|
||||
* @var InvoiceTemplate
|
||||
*/
|
||||
protected $template;
|
||||
|
||||
/**
|
||||
* @var CalculatorInterface
|
||||
*/
|
||||
protected $calculator;
|
||||
|
||||
/**
|
||||
* @var NumberGeneratorInterface
|
||||
*/
|
||||
protected $generator;
|
||||
|
||||
/**
|
||||
* @return InvoiceQuery
|
||||
*/
|
||||
public function getQuery(): InvoiceQuery
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceQuery $query
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setQuery(InvoiceQuery $query)
|
||||
{
|
||||
$this->query = $query;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $entries
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setEntries(array $entries)
|
||||
{
|
||||
$this->entries = $entries;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function getTemplate(): ?InvoiceTemplate
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setTemplate($template)
|
||||
{
|
||||
$this->template = $template;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer
|
||||
*/
|
||||
public function getCustomer()
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setCustomer($customer)
|
||||
{
|
||||
$this->customer = $customer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDueDate(): \DateTime
|
||||
{
|
||||
return new \DateTime('+'.$this->getTemplate()->getDueDays().' days');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getInvoiceDate(): \DateTime
|
||||
{
|
||||
return new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CalculatorInterface $calculator
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setCalculator(CalculatorInterface $calculator)
|
||||
{
|
||||
$this->calculator = $calculator;
|
||||
$this->calculator->setModel($this);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NumberGeneratorInterface $generator
|
||||
* @return InvoiceModel
|
||||
*/
|
||||
public function setNumberGenerator(NumberGeneratorInterface $generator)
|
||||
{
|
||||
$this->generator = $generator;
|
||||
$this->generator->setModel($this);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NumberGeneratorInterface
|
||||
*/
|
||||
public function getNumberGenerator(): NumberGeneratorInterface
|
||||
{
|
||||
return $this->generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CalculatorInterface
|
||||
*/
|
||||
public function getCalculator(): CalculatorInterface
|
||||
{
|
||||
return $this->calculator;
|
||||
}
|
||||
}
|
||||
60
src/Repository/InvoiceTemplateRepository.php
Normal file
60
src/Repository/InvoiceTemplateRepository.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
/**
|
||||
* Class InvoiceTemplateRepository
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceTemplateRepository extends AbstractRepository
|
||||
{
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTemplate()
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('COUNT(t.id) as totalRecords')
|
||||
->from(InvoiceTemplate::class, 't')
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->execute([], Query::HYDRATE_ARRAY);
|
||||
|
||||
if (!isset($result[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $result[0]['totalRecords'] > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BaseQuery $query
|
||||
* @return \Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
|
||||
*/
|
||||
public function findByQuery(BaseQuery $query)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('t')
|
||||
->from(InvoiceTemplate::class, 't')
|
||||
->orderBy('t.id');
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ use App\Entity\Project;
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class ActivityQuery extends VisibilityQuery
|
||||
class ActivityQuery extends ProjectQuery
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -27,29 +27,6 @@ class ActivityQuery extends VisibilityQuery
|
||||
*/
|
||||
protected $project;
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*/
|
||||
protected $customer;
|
||||
|
||||
/**
|
||||
* @return Customer
|
||||
*/
|
||||
public function getCustomer()
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomer(Customer $customer = null)
|
||||
{
|
||||
$this->customer = $customer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
*/
|
||||
|
||||
69
src/Repository/Query/InvoiceQuery.php
Normal file
69
src/Repository/Query/InvoiceQuery.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Repository\Query;
|
||||
|
||||
use App\Entity\InvoiceTemplate;
|
||||
|
||||
/**
|
||||
* Can be used for invoice queries.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InvoiceQuery extends TimesheetQuery
|
||||
{
|
||||
|
||||
/**
|
||||
* @var InvoiceTemplate
|
||||
*/
|
||||
protected $template;
|
||||
|
||||
/**
|
||||
* @var InvoiceTemplate[]
|
||||
*/
|
||||
protected $templates = [];
|
||||
|
||||
/**
|
||||
* @return InvoiceTemplate
|
||||
*/
|
||||
public function getTemplate()
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate $template
|
||||
* @return InvoiceQuery
|
||||
*/
|
||||
public function setTemplate($template)
|
||||
{
|
||||
$this->template = $template;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InvoiceTemplate[]
|
||||
*/
|
||||
public function getTemplates(): array
|
||||
{
|
||||
return $this->templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InvoiceTemplate[] $templates
|
||||
* @return InvoiceQuery
|
||||
*/
|
||||
public function setTemplates(array $templates)
|
||||
{
|
||||
$this->templates = $templates;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ use App\Entity\Project;
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class TimesheetQuery extends BaseQuery
|
||||
class TimesheetQuery extends ActivityQuery
|
||||
{
|
||||
|
||||
const STATE_ALL = 1;
|
||||
@@ -47,18 +47,18 @@ class TimesheetQuery extends BaseQuery
|
||||
* @var Activity
|
||||
*/
|
||||
protected $activity;
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
protected $project;
|
||||
/**
|
||||
* @var Customer
|
||||
*/
|
||||
protected $customer;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $state = self::STATE_ALL;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
protected $begin;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
protected $end;
|
||||
|
||||
/**
|
||||
* @return User
|
||||
@@ -98,48 +98,6 @@ class TimesheetQuery extends BaseQuery
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
*/
|
||||
public function getProject()
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project overwrites: setCustomer()
|
||||
* Is overwritten by: setActivity()
|
||||
*
|
||||
* @param Project $project
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function setProject(Project $project = null)
|
||||
{
|
||||
$this->project = $project;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Customer
|
||||
*/
|
||||
public function getCustomer()
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project overwrites: none
|
||||
* Is overwritten by: setActivity() and setProject()
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function setCustomer(Customer $customer = null)
|
||||
{
|
||||
$this->customer = $customer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
@@ -165,4 +123,40 @@ class TimesheetQuery extends BaseQuery
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getBegin()
|
||||
{
|
||||
return $this->begin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $begin
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function setBegin($begin)
|
||||
{
|
||||
$this->begin = $begin;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getEnd()
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $end
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function setEnd($end)
|
||||
{
|
||||
$this->end = $end;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Entity\User;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Timesheet;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use App\Model\Statistic\Month;
|
||||
use App\Model\Statistic\Year;
|
||||
@@ -266,7 +267,7 @@ class TimesheetRepository extends AbstractRepository
|
||||
|
||||
/**
|
||||
* @param TimesheetQuery $query
|
||||
* @return Pagerfanta
|
||||
* @return QueryBuilder|Pagerfanta
|
||||
*/
|
||||
public function findByQuery(TimesheetQuery $query)
|
||||
{
|
||||
@@ -291,6 +292,15 @@ class TimesheetRepository extends AbstractRepository
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
}
|
||||
|
||||
if ($query->getBegin() !== null) {
|
||||
$qb->andWhere('t.begin >= :begin')
|
||||
->setParameter('begin', $query->getBegin());
|
||||
}
|
||||
if ($query->getEnd() !== null) {
|
||||
$qb->andWhere('t.end <= :end')
|
||||
->setParameter('end', $query->getEnd());
|
||||
}
|
||||
|
||||
if ($query->getActivity() !== null) {
|
||||
$qb->andWhere('t.activity = :activity')
|
||||
->setParameter('activity', $query->getActivity());
|
||||
|
||||
@@ -23,6 +23,14 @@ use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
||||
*/
|
||||
class UserRepository extends AbstractRepository implements UserLoaderInterface
|
||||
{
|
||||
/**
|
||||
* @param $id
|
||||
* @return null|User
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return statistic data for all user.
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Utils\Markdown;
|
||||
use Symfony\Component\Intl\Intl;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
/**
|
||||
@@ -123,8 +121,11 @@ class Extensions extends \Twig_Extension
|
||||
*/
|
||||
public function money($amount, $currency = null)
|
||||
{
|
||||
$currency = $currency ?: Customer::DEFAULT_CURRENCY;
|
||||
return round($amount) . ' ' . Intl::getCurrencyBundle()->getCurrencySymbol($currency);
|
||||
$result = round($amount, 2);
|
||||
if ($currency !== null) {
|
||||
$result .= ' ' . Intl::getCurrencyBundle()->getCurrencySymbol($currency);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,11 +11,9 @@
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% endif %}
|
||||
|
||||
{# Available fields: vat; contact; address; country; phone; fax; mobile; mail; homepage; timezone; #}
|
||||
|
||||
{{ tables.data_table_header({
|
||||
'label.id': 'hidden-xs',
|
||||
'label.name': '',
|
||||
'label.customer_number': 'hidden-xs',
|
||||
'label.project': '',
|
||||
'label.comment': 'hidden-xs',
|
||||
'label.country': 'hidden-xs',
|
||||
@@ -26,8 +24,8 @@
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td class="hidden-xs">{{ entry.id }}</td>
|
||||
<td>{{ entry.name }} {% if entry.company is not empty %}({{ entry.company }}){% endif %}</td>
|
||||
<td class="hidden-xs">{{ entry.number }}</td>
|
||||
<td>
|
||||
{% for project in entry.projects %}
|
||||
<a href="{{ path('admin_project_edit', {'id' : project.id}) }}">{{ widgets.label_project(project) }}</a>
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{% extends '@AvanzuAdminTheme/layout/default-layout.html.twig' %}
|
||||
|
||||
{% block avanzu_page_content_before %}
|
||||
<div class="pad margin no-print">
|
||||
{% block main_before %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block avanzu_page_content_after %}
|
||||
{% block main_after %}{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block avanzu_page_content %}
|
||||
{{ include('default/_flash_messages.html.twig') }}
|
||||
{% block main %}{% endblock %}
|
||||
@@ -31,16 +41,17 @@
|
||||
|
||||
{% block avanzu_head %}
|
||||
<link rel="stylesheet" href="{{ asset('css/kimai.css') }}">
|
||||
{#<link rel="stylesheet" href="{{ asset('theme/plugins/datepicker/datepicker3.css') }}">#}
|
||||
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block avanzu_footer %}
|
||||
{% block footer %}
|
||||
<footer class="main-footer">
|
||||
<footer class="main-footer no-print">
|
||||
<div class="pull-right hidden-xs">
|
||||
<b>Version</b> 2.0
|
||||
<b>Version</b> 2.0 dev
|
||||
</div>
|
||||
<strong>Copyright © {{ 'now'|date('Y') }} - <a href="http://www.kevinpapst.de">Kevin Papst</a></strong>.
|
||||
Made with ♥ by <a href="http://www.kevinpapst.de">Kevin Papst</a>.
|
||||
{{ 'footer.license'|trans }}
|
||||
</footer>
|
||||
<!-- Page rendered on {{ 'now'|localizeddate('long', 'long', null, 'UTC') }} -->
|
||||
@@ -74,6 +85,8 @@
|
||||
{% block avanzu_javascripts %}
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
|
||||
<script src="{{ asset('theme/bootstrap/js/bootstrap.min.js') }}"></script>
|
||||
{#<script src="{{ asset('theme/plugins/daterangepicker/moment.min.js') }}"></script>
|
||||
<script src="{{ asset('theme/plugins/datepicker/bootstrap-datepicker.js') }}"></script>#}
|
||||
<script src="{{ asset('js/kimai.js') }}"></script>
|
||||
<script src="{{ asset('theme/plugins/slimScroll/jquery.slimscroll.min.js') }}"></script>
|
||||
<script src="{{ asset('js/Chart.min.js') }}"></script>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
{#
|
||||
Each field type is rendered by a template fragment, which is determined
|
||||
by the name of your form type class (DateTimePickerType -> date_time_picker)
|
||||
and the suffix "_widget". This can be controlled by overriding getBlockPrefix()
|
||||
in DateTimePickerType.
|
||||
|
||||
See http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#creating-a-template-for-the-field
|
||||
#}
|
||||
|
||||
{% block date_time_picker_widget %}
|
||||
{% spaceless %}
|
||||
<div class="input-group date" data-toggle="datetimepicker">
|
||||
{{ block('datetime_widget') }}
|
||||
<span class="input-group-addon">
|
||||
<span class="fa fa-calendar"></span>
|
||||
</span>
|
||||
</div>
|
||||
{% endspaceless %}
|
||||
{% endblock %}
|
||||
26
templates/form/toolbar-theme.html.twig
Normal file
26
templates/form/toolbar-theme.html.twig
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "@AvanzuAdminTheme/layout/form-theme.html.twig" %}
|
||||
|
||||
{% block date_widget %}
|
||||
{% if widget == 'single_text' %}
|
||||
{% if attr.class is defined %}
|
||||
{% set class = attr.class ~ ' timepicker' %}
|
||||
{% else %}
|
||||
{% set class = ' timepicker' %}
|
||||
{% endif %}
|
||||
{% set class = class ~ ' datepicker' %}
|
||||
{% set attr = attr|merge({'class' : class, 'data-datepickerenable':'on'}) %}
|
||||
{{ block('form_widget_simple') }}
|
||||
{% else %}
|
||||
{% set date_pattern = '<div class="row">' ~ date_pattern ~ '</div>'|raw %}
|
||||
{{ date_pattern|replace({
|
||||
'{{ year }}' : '<div class="col-xs-4">{{ year }}</div>',
|
||||
'{{ month }}' : '<div class="col-xs-4">{{ month }}</div>',
|
||||
'{{ day }}' : '<div class="col-xs-4">{{ day }}</div>',
|
||||
})|raw|replace({
|
||||
'{{ year }}': form_widget(form.year),
|
||||
'{{ month }}': form_widget(form.month),
|
||||
'{{ day }}': form_widget(form.day),
|
||||
})|raw }}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
27
templates/invoice/index.html.twig
Normal file
27
templates/invoice/index.html.twig
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/toolbar.html.twig" as macro %}
|
||||
|
||||
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'invoice.subtitle'|trans }}{% endblock %}
|
||||
{% block javascript_imports %}<script src="{{ asset('js/toolbar.js') }}"></script>{% endblock %}
|
||||
|
||||
{% block avanzu_page_content_class %}invoice{% endblock %}
|
||||
|
||||
{% block main_before %}
|
||||
{{ macro.toolbar(form, {'plus-square': path('admin_invoice_template_create'), 'list': path('admin_invoice_template')}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_after %}
|
||||
<div class="clearfix"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% if model.entries is empty or action is empty %}
|
||||
{{ widgets.callout('warning', 'invoice.select_filter') }}
|
||||
{% else %}
|
||||
{{ render(controller(action, {'model': model})) }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
111
templates/invoice/print.html.twig
Normal file
111
templates/invoice/print.html.twig
Normal file
@@ -0,0 +1,111 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h2 class="page-header">
|
||||
<i class="fa fa-globe"></i> <span contenteditable="true">{{ model.template.title }}</span>
|
||||
<small class="pull-right">{{ 'label.date'|trans }}: {{ model.invoiceDate|date(kimai_context.date_1) }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row invoice-info">
|
||||
<div class="col-sm-4 invoice-col">
|
||||
{{ 'invoice.from'|trans }}
|
||||
<address contenteditable="true">
|
||||
<strong>{{ model.template.company }}</strong><br>
|
||||
{{ model.template.address|trim|nl2br }}
|
||||
</address>
|
||||
</div>
|
||||
<div class="col-sm-4 invoice-col">
|
||||
{% set customerPhone = model.customer.phone|default(model.customer.mobile) %}
|
||||
{{ 'invoice.to'|trans }}
|
||||
<address contenteditable="true">
|
||||
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
|
||||
{{ model.customer.address|nl2br }}
|
||||
{% if model.customer.phone is not empty %}
|
||||
<br>
|
||||
{{ 'label.phone'|trans }}: {{ model.customer.phone }}
|
||||
{% endif %}
|
||||
{% if model.customer.mail is not empty %}
|
||||
<br>
|
||||
{{ 'label.email'|trans }}: {{ model.customer.mail }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</div>
|
||||
<div class="col-sm-4 invoice-col">
|
||||
<p contenteditable="true">
|
||||
<b>{{ 'invoice.number'|trans }} {{ model.numberGenerator.invoiceNumber }}</b>
|
||||
</p>
|
||||
<p contenteditable="true">
|
||||
<b>{{ 'invoice.due_days'|trans }}:</b> {{ model.dueDate|date(kimai_context.date_1) }}<br>
|
||||
<b>{{ 'label.customer_number'|trans }}:</b> {{ model.customer.number }}<br>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans }}</th>
|
||||
<th>{{ 'label.activity'|trans }}</th>
|
||||
<th>{{ 'label.hours'|trans }}</th>
|
||||
<th>{{ 'label.rate'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in model.entries %}
|
||||
<tr>
|
||||
<td>{{ entry.begin|date(kimai_context.date_1) }}</td>
|
||||
<td>{{ entry.activity.name }} / {{ entry.activity.project.name }}</td>
|
||||
<td>{{ entry.duration|duration }}</td>
|
||||
<td>{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
{% if model.template.paymentTerms is not empty %}
|
||||
<p class="lead">{{ 'label.payment_terms'|trans }}</p>
|
||||
|
||||
<p class="text-muted well well-sm no-shadow" contenteditable="true">
|
||||
{{ model.template.paymentTerms|trim|nl2br }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-xs-6">
|
||||
<p class="lead" contenteditable="true">{{ 'invoice.due_days'|trans }} {{ model.dueDate|date(kimai_context.date_1) }}</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:50%">{{ 'invoice.subtotal'|trans }}</th>
|
||||
<td>{{ model.calculator.subtotal|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)</th>
|
||||
<td>{{ model.calculator.tax|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'invoice.total'|trans }}</th>
|
||||
<td>{{ model.calculator.total|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-print">
|
||||
<div class="col-xs-12">
|
||||
<button type="button" class="btn btn-success pull-right" onclick="window.print();return false;" style="margin-right: 5px;">
|
||||
<i class="fa fa-print"></i> {{ 'button.print'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
12
templates/invoice/template_edit.html.twig
Normal file
12
templates/invoice/template_edit.html.twig
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_invoice_template.subtitle'|trans }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{{ include('default/_form.html.twig', {
|
||||
'title': template.name|default('create'|trans),
|
||||
'form': form,
|
||||
'back': path('admin_invoice_template')
|
||||
}) }}
|
||||
{% endblock %}
|
||||
37
templates/invoice/templates.html.twig
Normal file
37
templates/invoice/templates.html.twig
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/datatables.html.twig" as tables %}
|
||||
|
||||
{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'admin_invoice_template.subtitle'|trans }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% if entries.count == 0 %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% endif %}
|
||||
|
||||
{{ tables.data_table_header({
|
||||
'label.name': '',
|
||||
'label.title': '',
|
||||
'label.due_days': 'hidden-xs',
|
||||
'label.vat': 'hidden-xs',
|
||||
'label.invoice_renderer': 'hidden-xs',
|
||||
'label.actions': '',
|
||||
}) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td>{{ entry.name }}</td>
|
||||
<td>{{ entry.title }}</td>
|
||||
<td class="hidden-xs hidden-sm">{{ entry.dueDays }}</td>
|
||||
<td class="hidden-xs hidden-sm">{{ entry.vat }}</td>
|
||||
<td class="hidden-xs hidden-sm">{{ "invoice_renderer.#{entry.renderer}"|trans }}</td>
|
||||
<td>
|
||||
{% set actionButtons = {'edit': path('admin_invoice_template_edit', {'id' : entry.id, 'page': page})} %}
|
||||
{{ widgets.button_group(actionButtons) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{{ tables.data_table_footer(entries, 'admin_invoice_template_paginated') }}
|
||||
{% endblock %}
|
||||
@@ -2,33 +2,8 @@
|
||||
{% macro data_table_header(entries, form, tools, toolsRight) %}
|
||||
<div class="box data_table">
|
||||
{% if form or tools or toolsRight %}
|
||||
<div class="box-header with-border">
|
||||
{% if tools %}
|
||||
<div class="btn-group tools-left">
|
||||
{% for icon,url in tools %}
|
||||
<a class="btn btn-default" href="{{ url }}"><i class="fa fa-{{ icon }}"></i></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if form %}
|
||||
<div class="box-title toolbar">
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if toolsRight %}
|
||||
<div class="box-tools">
|
||||
<div class="btn-group">
|
||||
{% for icon,url in tools %}
|
||||
<a class="btn btn-default" href="{{ url }}"><i class="fa fa-{{ icon }}"></i></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% import "macros/toolbar.html.twig" as macro %}
|
||||
{{ macro.toolbar(form, tools, toolsRight) }}
|
||||
{% endif %}
|
||||
<div class="box-body no-padding">
|
||||
<div class="dataTables_wrapper form-inline dt-bootstrap">
|
||||
@@ -72,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navigation text-center">
|
||||
<div class="navigation text-center no-print">
|
||||
{{ pagerfanta(entries, 'twitter_bootstrap3_translated', { routeName: route }) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
31
templates/macros/toolbar.html.twig
Normal file
31
templates/macros/toolbar.html.twig
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
{% macro toolbar(form, tools, toolsRight) %}
|
||||
<div class="box-header">
|
||||
{% if tools %}
|
||||
<div class="btn-group tools-left no-print">
|
||||
{% for icon,url in tools %}
|
||||
<a class="btn btn-default" href="{{ url }}"><i class="fa fa-{{ icon }}"></i></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if form %}
|
||||
<div class="box-title toolbar no-print">
|
||||
{% form_theme form 'form/toolbar-theme.html.twig' %}
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if toolsRight %}
|
||||
<div class="box-tools">
|
||||
<div class="btn-group">
|
||||
{% for icon,url in tools %}
|
||||
<a class="btn btn-default" href="{{ url }}"><i class="fa fa-{{ icon }}"></i></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -83,9 +83,9 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro callout(type, description, title) %}
|
||||
{% macro callout(type, description, title, icon) %}
|
||||
<div class="callout callout-{{ type|default('danger') }} lead">
|
||||
{% if title %}<h4>{{ title|trans }}</h4>{% endif %}
|
||||
{% if title %}<h4>{% if icon %}<i class="fa fa-{{ icon }}">{% endif %}</i> {{ title|trans }}</h4>{% endif %}
|
||||
<p>{{ description|trans }}</p>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<source>action.deleted_successfully</source>
|
||||
<target>Eintrag wurde erfolgreich gelöscht</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.first_template">
|
||||
<source>invoice.first_template</source>
|
||||
<target>Sie müssen zunächst eine Rechnungsvorlage erstellen, bevor Sie fortfahren können</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<source>action.deleted_successfully</source>
|
||||
<target>Entry was deleted successful</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.first_template">
|
||||
<source>invoice.first_template</source>
|
||||
<target>You have to create your first template invoice before you can proceed</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
<source>no</source>
|
||||
<target>Nein</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="both">
|
||||
<source>both</source>
|
||||
<target></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="subtitle.amount">
|
||||
<source>subtitle.amount</source>
|
||||
<target>(insgesamt %count%)</target>
|
||||
@@ -35,7 +39,9 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="confirm.delete_message">
|
||||
<source>confirm.delete_message</source>
|
||||
<target>Sie müssen das Löschen erneut bestätigen, da verknüpfte Objekte existieren welche ebenfalls mit gelöscht werden.</target>
|
||||
<target>
|
||||
Sie müssen das Löschen erneut bestätigen, da verknüpfte Objekte existieren welche ebenfalls mit gelöscht werden.
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
@@ -85,6 +91,10 @@
|
||||
<source>menu.timesheet</source>
|
||||
<target>Zeiterfassung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.invoice">
|
||||
<source>menu.invoice</source>
|
||||
<target>Rechnungen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.admin_timesheet">
|
||||
<source>menu.admin_timesheet</source>
|
||||
<target>Mitarbeiter Zeiten</target>
|
||||
@@ -99,7 +109,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.admin_activity">
|
||||
<source>menu.admin_activity</source>
|
||||
<target>Aktivitäten</target>
|
||||
<target>Tätigkeiten</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.admin_user">
|
||||
<source>menu.admin_user</source>
|
||||
@@ -117,42 +127,6 @@
|
||||
<!--
|
||||
Error templates
|
||||
-->
|
||||
<trans-unit id="http_error.name">
|
||||
<source>http_error.name</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error.description">
|
||||
<source>http_error.description</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error.suggestion">
|
||||
<source>http_error.suggestion</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_403.description">
|
||||
<source>http_error_403.description</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_403.suggestion">
|
||||
<source>http_error_403.suggestion</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_404.description">
|
||||
<source>http_error_404.description</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_404.suggestion">
|
||||
<source>http_error_404.suggestion</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_405.description">
|
||||
<source>http_error_405.description</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_405.suggestion">
|
||||
<source>http_error_405.suggestion</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="error.no_entries_found">
|
||||
<source>error.no_entries_found</source>
|
||||
<target>Keine Einträge vorhanden</target>
|
||||
@@ -211,7 +185,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="label.activity">
|
||||
<source>label.activity</source>
|
||||
<target>Aktivität</target>
|
||||
<target>Tätigkeit</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.project">
|
||||
<source>label.project</source>
|
||||
@@ -421,8 +395,8 @@
|
||||
<source>admin_project.delete_confirm</source>
|
||||
<target>
|
||||
Momentan existieren für das Projekt %project% des Kunden %customer% insgesamt %activities%
|
||||
Aktivitäten und %records% Zeiteinträge, welche sich auf eine Gesamtdauer von %duration% belaufen.
|
||||
Diese Aktivitäten und Zeiteinträge werden ebenfalls mit gelöscht!
|
||||
Tätigkeiten und %records% Zeiteinträge, welche sich auf eine Gesamtdauer von %duration% belaufen.
|
||||
Diese Tätigkeiten und Zeiteinträge werden ebenfalls mit gelöscht!
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
@@ -431,7 +405,7 @@
|
||||
-->
|
||||
<trans-unit id="admin_activity.title">
|
||||
<source>admin_activity.title</source>
|
||||
<target>Aktivitäten</target>
|
||||
<target>Tätigkeiten</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="admin_activity.subtitle">
|
||||
<source>admin_activity.subtitle</source>
|
||||
@@ -440,7 +414,7 @@
|
||||
<trans-unit id="admin_activity.delete_confirm">
|
||||
<source>admin_activity.delete_confirm</source>
|
||||
<target>
|
||||
Momentan existieren für die Aktivität %activity% im Projekt %project% für den Kunden %customer%
|
||||
Momentan existieren für die Tätigkeit %activity% im Projekt %project% für den Kunden %customer%
|
||||
insgesamt %records% Zeiteinträge, welche sich auf eine Gesamtdauer von %duration% belaufen. Diese
|
||||
Zeiteinträge werden ebenfalls mit gelöscht!
|
||||
</target>
|
||||
@@ -457,6 +431,10 @@
|
||||
<source>admin_customer.subtitle</source>
|
||||
<target>Ihre Kunden stellen die Basis dar, auf der Sie Projekte und Tätigkeiten anlegen können</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.customer_number">
|
||||
<source>label.customer_number</source>
|
||||
<target>Kundennummer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company">
|
||||
<source>label.company</source>
|
||||
<target>Unternehmensbezeichnung</target>
|
||||
@@ -505,8 +483,8 @@
|
||||
<source>admin_customer.delete_confirm</source>
|
||||
<target>
|
||||
Momentan existieren für den Kunden %customer% insgesamt %project% Projekte mit %activity%
|
||||
Aktivitäten, die sich in %records% Zeiteinträgen auf eine Gesamtdauer von %duration% belaufen. Diese
|
||||
Projekte, Aktivitäten und Zeiteinträge werden ebenfalls mit gelöscht!
|
||||
Tätigkeiten, die sich in %records% Zeiteinträgen auf eine Gesamtdauer von %duration% belaufen. Diese
|
||||
Projekte, Tätigkeiten und Zeiteinträge werden ebenfalls mit gelöscht!
|
||||
</target>
|
||||
</trans-unit>
|
||||
|
||||
@@ -607,7 +585,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.activitiesTotal">
|
||||
<source>stats.activitiesTotal</source>
|
||||
<target>Anzahl Aktivitäten</target>
|
||||
<target>Anzahl Tätigkeiten</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.projectsTotal">
|
||||
<source>stats.projectsTotal</source>
|
||||
@@ -618,6 +596,106 @@
|
||||
<target>Anzahl Kunden</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Invoice
|
||||
-->
|
||||
<trans-unit id="admin_invoice_template.title">
|
||||
<source>admin_invoice_template.title</source>
|
||||
<target>Rechnungsvorlagen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="admin_invoice_template.subtitle">
|
||||
<source>admin_invoice_template.subtitle</source>
|
||||
<target>Verwalten Sie Ihre Rechnungsvorlagen, mit deren Hilfe Sie ihre Daten exportieren können.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.title">
|
||||
<source>invoice.title</source>
|
||||
<target>Rechnungen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.subtitle">
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Hier erstellen Sie Rechnungen für ihre aufgezeichneten Zeiten.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.select_filter">
|
||||
<source>invoice.select_filter</source>
|
||||
<target>Bevor Sie eine Rechnung erstellen können, müssen Sie mindestens einen Kunden-Filter auswählen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>Drucken</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_print">
|
||||
<source>invoice_print</source>
|
||||
<target>Rechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.template">
|
||||
<source>label.template</source>
|
||||
<target>Vorlage</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.due_days">
|
||||
<source>label.due_days</source>
|
||||
<target>Zahlungsziel in Tagen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.due_days">
|
||||
<source>invoice.due_days</source>
|
||||
<target>Zahlungsziel</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.from">
|
||||
<source>invoice.from</source>
|
||||
<target>Von</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.to">
|
||||
<source>invoice.to</source>
|
||||
<target>An</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.number">
|
||||
<source>invoice.number</source>
|
||||
<target>Rechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.subtotal">
|
||||
<source>invoice.subtotal</source>
|
||||
<target>Netto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.tax">
|
||||
<source>invoice.tax</source>
|
||||
<target>Umsatzsteuer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.total">
|
||||
<source>invoice.total</source>
|
||||
<target>Rechnungsbetrag</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.payment_terms">
|
||||
<source>label.payment_terms</source>
|
||||
<target>Zahlungsinformationen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_renderer">
|
||||
<source>label.invoice_renderer</source>
|
||||
<target>Rechnungsvorlage</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_renderer.default">
|
||||
<source>invoice_renderer.default</source>
|
||||
<target>HTML Rechnung (Standard)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_number_generator">
|
||||
<source>label.invoice_number_generator</source>
|
||||
<target>Rechnungsnummern-Generator</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_number_generator.default">
|
||||
<source>invoice_number_generator.default</source>
|
||||
<target>Datum (Standard)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_number_generator.random">
|
||||
<source>invoice_number_generator.random</source>
|
||||
<target>Zufallszahl (nur zum Testen)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_calculator">
|
||||
<source>label.invoice_calculator</source>
|
||||
<target>Zahlungsberechnung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_calculator.default">
|
||||
<source>invoice_calculator.default</source>
|
||||
<target>Stundenbasiert (Standard)</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Month names
|
||||
-->
|
||||
@@ -683,7 +761,9 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities">
|
||||
<source>recent.activities</source>
|
||||
<target>Ihr %count% zuletzt bearbeitete Aktivität|Eine ihrer %count% letzten Aktivitäten neustarten</target>
|
||||
<target>
|
||||
Ihr %count% zuletzt bearbeitete Tätigkeit|Eine ihrer %count% letzten Tätigkeiten neustarten
|
||||
</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="recent.activities.format">
|
||||
<source>recent.activities.format</source>
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
<source>no</source>
|
||||
<target>No</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="both">
|
||||
<source>both</source>
|
||||
<target></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="subtitle.amount">
|
||||
<source>subtitle.amount</source>
|
||||
<target>(total %count%)</target>
|
||||
@@ -87,6 +91,10 @@
|
||||
<source>menu.timesheet</source>
|
||||
<target>Timesheet</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.invoice">
|
||||
<source>menu.invoice</source>
|
||||
<target>Invoices</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.admin_timesheet">
|
||||
<source>menu.admin_timesheet</source>
|
||||
<target>User timesheets</target>
|
||||
@@ -119,42 +127,6 @@
|
||||
<!--
|
||||
Error templates
|
||||
-->
|
||||
<trans-unit id="http_error.name">
|
||||
<source>http_error.name</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error.description">
|
||||
<source>http_error.description</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error.suggestion">
|
||||
<source>http_error.suggestion</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_403.description">
|
||||
<source>http_error_403.description</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_403.suggestion">
|
||||
<source>http_error_403.suggestion</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_404.description">
|
||||
<source>http_error_404.description</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_404.suggestion">
|
||||
<source>http_error_404.suggestion</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_405.description">
|
||||
<source>http_error_405.description</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="http_error_405.suggestion">
|
||||
<source>http_error_405.suggestion</source>
|
||||
<target>Error</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="error.no_entries_found">
|
||||
<source>error.no_entries_found</source>
|
||||
<target>No entries existing</target>
|
||||
@@ -179,7 +151,7 @@
|
||||
<source>label.duration</source>
|
||||
<target>Duration</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.usern">
|
||||
<trans-unit id="label.user">
|
||||
<source>label.user</source>
|
||||
<target>User</target>
|
||||
</trans-unit>
|
||||
@@ -459,6 +431,10 @@
|
||||
<source>admin_customer.subtitle</source>
|
||||
<target>Your customer are the base for creating projects and activities</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.customer_number">
|
||||
<source>label.customer_number</source>
|
||||
<target>Account</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.company">
|
||||
<source>label.company</source>
|
||||
<target>Company name</target>
|
||||
@@ -620,6 +596,106 @@
|
||||
<target>Amount customers</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Invoice
|
||||
-->
|
||||
<trans-unit id="admin_invoice_template.title">
|
||||
<source>admin_invoice_template.title</source>
|
||||
<target>Invoice template</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="admin_invoice_template.subtitle">
|
||||
<source>admin_invoice_template.subtitle</source>
|
||||
<target>Administrate your invoice templates which can be used to data export.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.title">
|
||||
<source>invoice.title</source>
|
||||
<target>Invoices</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.subtitle">
|
||||
<source>invoice.subtitle</source>
|
||||
<target>Create invoices from your recorded timesheet entries.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.select_filter">
|
||||
<source>invoice.select_filter</source>
|
||||
<target>Before you can create an invoice, you have to select at least a customer filter.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="button.print">
|
||||
<source>button.print</source>
|
||||
<target>Print</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_print">
|
||||
<source>invoice_print</source>
|
||||
<target>Invoice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.template">
|
||||
<source>label.template</source>
|
||||
<target>Template</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.due_days">
|
||||
<source>label.due_days</source>
|
||||
<target>Payment term in days</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.due_days">
|
||||
<source>invoice.due_days</source>
|
||||
<target>Payment target</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.from">
|
||||
<source>invoice.from</source>
|
||||
<target>From</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.to">
|
||||
<source>invoice.to</source>
|
||||
<target>To</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.number">
|
||||
<source>invoice.number</source>
|
||||
<target>Invoice</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.subtotal">
|
||||
<source>invoice.subtotal</source>
|
||||
<target>Subtotal</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.tax">
|
||||
<source>invoice.tax</source>
|
||||
<target>Tax</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice.total">
|
||||
<source>invoice.total</source>
|
||||
<target>Total</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.payment_terms">
|
||||
<source>label.payment_terms</source>
|
||||
<target>Terms of payment</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_renderer">
|
||||
<source>label.invoice_renderer</source>
|
||||
<target>Invoice template</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_renderer.default">
|
||||
<source>invoice_renderer.default</source>
|
||||
<target>HTML Invoice (default)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_number_generator">
|
||||
<source>label.invoice_number_generator</source>
|
||||
<target>Invoicenumber-Generator</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_number_generator.default">
|
||||
<source>invoice_number_generator.default</source>
|
||||
<target>Date (default)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_number_generator.random">
|
||||
<source>invoice_number_generator.random</source>
|
||||
<target>Random number (only for testing)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.invoice_calculator">
|
||||
<source>label.invoice_calculator</source>
|
||||
<target>Amount calculator</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invoice_calculator.default">
|
||||
<source>invoice_calculator.default</source>
|
||||
<target>Hourly rates (default)</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Month names
|
||||
-->
|
||||
|
||||
Reference in New Issue
Block a user