Release 1.6.2 (#1289)
* include user teams in user entity * prevent unauthorized access via API * improve teamlead permission handling in team timesheets * add team data to user entity * add security tests * highlight menu for invoice template copy * unified handling of invoice data across all templates * access to the current users data in invoice templates * permission improvement in invoice form * allow to skip record rows * allow to add new invoice locations without overwriting the global ones * allow to order user preferences * change permission for normal users with access to view_other_timesheets * properly validate invoice template field length * allow to replace multiple variables in cell values text * upgraded office invoice template * doctrine deprecation fix * upgrade phpoffice/phpword * fix future begin check for default rounding rules * dashboard widget counter: respect visibility and teams - fixes #1161 * fix future begin check for default rounding rules * added new events for pre and post invoice rendering * fix permission issue for users without team seeing all records * prevent error in spreadsheet renderer for empty invoices
This commit is contained in:
@@ -28,7 +28,7 @@ It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Do
|
|||||||
|
|
||||||
### About
|
### About
|
||||||
|
|
||||||
This is new version of the open source timetracker Kimai. It is stable and production ready, ships
|
This is the new version of the open source timetracker Kimai. It is stable and production ready, ships
|
||||||
with most advanced features from Kimai 1 and many new ones, including but not limited to:
|
with most advanced features from Kimai 1 and many new ones, including but not limited to:
|
||||||
|
|
||||||
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
|
||||||
@@ -59,9 +59,9 @@ You can see a rough development roadmap in the [Milestones](https://github.com/k
|
|||||||
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome.
|
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome.
|
||||||
|
|
||||||
> Kimai 2 uses a rolling release concept for delivering updates.
|
> Kimai 2 uses a rolling release concept for delivering updates.
|
||||||
> You can upgrade Kimai at any time, you don't need to wait for the next official release.
|
> You can upgrade Kimai at any time , you don't need to wait for the next official release.
|
||||||
|
|
||||||
Release versions will be created on a regular base (approx. one release per month) and you can use these tags if you are familiar with git.
|
Release versions will be created on a regular base (approx. one release per month) and you can should use these tags if you are not familiar with git.
|
||||||
Every code change, whether it's a new feature or a bug fix, will be done on the master branch.
|
Every code change, whether it's a new feature or a bug fix, will be done on the master branch.
|
||||||
I have to do it this way, as I develop Kimai in my free time and want to put my effort into the software instead of backporting changes for old versions.
|
I have to do it this way, as I develop Kimai in my free time and want to put my effort into the software instead of backporting changes for old versions.
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ you can upgrade your Kimai installation to the latest stable release.
|
|||||||
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
Check below if there are more version specific steps required, which need to be executed after the normal update process.
|
||||||
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
|
||||||
|
|
||||||
## [1.6](https://github.com/kevinpapst/kimai2/releases/tag/1.6) and [1.6.1](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1)
|
## [1.6](https://github.com/kevinpapst/kimai2/releases/tag/1.6), [1.6.1](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1), [1.6.2](https://github.com/kevinpapst/kimai2/releases/tag/1.6.2)
|
||||||
|
|
||||||
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
|
||||||
|
|
||||||
- Invoice improvements:
|
- Invoice changes:
|
||||||
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates). Using them? Install them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html)).
|
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates). Using them? Install them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html)).
|
||||||
- Added new invoice fields (VAT, contact, payment details) and customer field (VAT). Used the twig settings before? Move them to the respective invoice template settings.
|
- Added new invoice fields (VAT, contact, payment details) and customer field (VAT). Used the twig settings before? Move them to the respective invoice template settings.
|
||||||
- Permissions can be managed via Admin UI. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html).
|
- Permissions can be managed via Admin UI. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html).
|
||||||
|
- Important permission change: regular users with the `view_other_timesheet` permission could see all timesheets. This was a legacy from the time before team permissions were introduced. If you rely on this behavior, you need to create a team with all users and the teamlead being the user who needs access to all timesheets.
|
||||||
|
|
||||||
### Developer
|
### Developer
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
"nelmio/api-doc-bundle": "^3.2",
|
"nelmio/api-doc-bundle": "^3.2",
|
||||||
"nelmio/cors-bundle": "^1.5",
|
"nelmio/cors-bundle": "^1.5",
|
||||||
"ocramius/proxy-manager": "^2.1.1",
|
"ocramius/proxy-manager": "^2.1.1",
|
||||||
"phpoffice/phpspreadsheet": "^1.4",
|
"phpoffice/phpspreadsheet": "^1.10",
|
||||||
"phpoffice/phpword": "^0.15.0",
|
"phpoffice/phpword": "^0.17",
|
||||||
"psr/log": "^1.1",
|
"psr/log": "^1.1",
|
||||||
"sensio/framework-extra-bundle": "^5.2",
|
"sensio/framework-extra-bundle": "^5.2",
|
||||||
"symfony/asset": "^4.0",
|
"symfony/asset": "^4.0",
|
||||||
|
|||||||
22
composer.lock
generated
22
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "4c71764ce6f4c135070a2f7e7a0c62b6",
|
"content-hash": "c08938efb681d04751323b133c8d8199",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "beberlei/doctrineextensions",
|
"name": "beberlei/doctrineextensions",
|
||||||
@@ -4525,16 +4525,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoffice/phpword",
|
"name": "phpoffice/phpword",
|
||||||
"version": "0.15.0",
|
"version": "0.17.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPOffice/PHPWord.git",
|
"url": "https://github.com/PHPOffice/PHPWord.git",
|
||||||
"reference": "dfa2f36cad2b632b7ab1c56473e4f5db9a7caf7f"
|
"reference": "b8346af548d399acd9e30fc76ab0c55c2fec03a5"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/dfa2f36cad2b632b7ab1c56473e4f5db9a7caf7f",
|
"url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/b8346af548d399acd9e30fc76ab0c55c2fec03a5",
|
||||||
"reference": "dfa2f36cad2b632b7ab1c56473e4f5db9a7caf7f",
|
"reference": "b8346af548d399acd9e30fc76ab0c55c2fec03a5",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4566,7 +4566,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-develop": "0.16-dev"
|
"dev-develop": "0.18-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -4587,6 +4587,10 @@
|
|||||||
"email": "me@gabrielbull.com",
|
"email": "me@gabrielbull.com",
|
||||||
"homepage": "http://gabrielbull.com/"
|
"homepage": "http://gabrielbull.com/"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Franck Lefevre",
|
||||||
|
"homepage": "https://rootslabs.net/blog/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Ivan Lanin",
|
"name": "Ivan Lanin",
|
||||||
"homepage": "http://ivan.lanin.org"
|
"homepage": "http://ivan.lanin.org"
|
||||||
@@ -4595,10 +4599,6 @@
|
|||||||
"name": "Roman Syroeshko",
|
"name": "Roman Syroeshko",
|
||||||
"homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/"
|
"homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Franck Lefevre",
|
|
||||||
"homepage": "https://rootslabs.net/blog/"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Antoine de Troostembergh"
|
"name": "Antoine de Troostembergh"
|
||||||
}
|
}
|
||||||
@@ -4630,7 +4630,7 @@
|
|||||||
"word",
|
"word",
|
||||||
"writer"
|
"writer"
|
||||||
],
|
],
|
||||||
"time": "2018-07-14T16:59:43+00:00"
|
"time": "2019-10-01T20:43:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/cache",
|
"name": "psr/cache",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ doctrine:
|
|||||||
entity_managers:
|
entity_managers:
|
||||||
default:
|
default:
|
||||||
connection: default
|
connection: default
|
||||||
naming_strategy: doctrine.orm.naming_strategy.underscore
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
auto_mapping: true
|
auto_mapping: true
|
||||||
mappings:
|
mappings:
|
||||||
App:
|
App:
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ nelmio_api_doc:
|
|||||||
- { alias: TimesheetMeta, type: App\Entity\TimesheetMeta, groups: [Default, Timesheet] }
|
- { alias: TimesheetMeta, type: App\Entity\TimesheetMeta, groups: [Default, Timesheet] }
|
||||||
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
|
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
|
||||||
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
|
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
|
||||||
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User, User_Entity] }
|
||||||
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
|
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
|
||||||
- { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team] }
|
- { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team, Team_Entity] }
|
||||||
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
|
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
|
||||||
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
|
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
|
||||||
areas:
|
areas:
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
App\Entity\Team:
|
App\Entity\Team:
|
||||||
exclusion_policy: All
|
exclusion_policy: All
|
||||||
custom_accessor_order: [id, name, users]
|
custom_accessor_order: [id, name, teamlead, users]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
include: true
|
include: true
|
||||||
name:
|
name:
|
||||||
include: true
|
include: true
|
||||||
|
teamlead:
|
||||||
|
include: true
|
||||||
|
groups: [Team_Entity]
|
||||||
users:
|
users:
|
||||||
exclude: true
|
include: true
|
||||||
|
groups: [Team_Entity]
|
||||||
customers:
|
customers:
|
||||||
exclude: true
|
exclude: true
|
||||||
projects:
|
projects:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
App\Entity\User:
|
App\Entity\User:
|
||||||
exclusion_policy: All
|
exclusion_policy: All
|
||||||
custom_accessor_order: [id, alias, title, avatar, language, timezone]
|
custom_accessor_order: [id, alias, title, avatar, language, timezone, teams]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
include: true
|
include: true
|
||||||
@@ -10,18 +10,21 @@ App\Entity\User:
|
|||||||
groups: [Default]
|
groups: [Default]
|
||||||
title:
|
title:
|
||||||
include: true
|
include: true
|
||||||
groups: [Entity]
|
groups: [User_Entity]
|
||||||
avatar:
|
avatar:
|
||||||
include: true
|
include: true
|
||||||
groups: [Entity]
|
groups: [User_Entity]
|
||||||
|
teams:
|
||||||
|
include: true
|
||||||
|
groups: [User_Entity]
|
||||||
virtual_properties:
|
virtual_properties:
|
||||||
getLanguage:
|
getLanguage:
|
||||||
serialized_name: language
|
serialized_name: language
|
||||||
exp: "object.getPreferenceValue('language') === null ? null : object.getPreferenceValue('language')"
|
exp: "object.getPreferenceValue('language') === null ? null : object.getPreferenceValue('language')"
|
||||||
type: string
|
type: string
|
||||||
groups: [Entity]
|
groups: [User_Entity]
|
||||||
getTimezone:
|
getTimezone:
|
||||||
serialized_name: timezone
|
serialized_name: timezone
|
||||||
exp: "object.getPreferenceValue('timezone') === null ? null : object.getPreferenceValue('timezone')"
|
exp: "object.getPreferenceValue('timezone') === null ? null : object.getPreferenceValue('timezone')"
|
||||||
type: string
|
type: string
|
||||||
groups: [Entity]
|
groups: [User_Entity]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ FOS\UserBundle\Model\User:
|
|||||||
roles:
|
roles:
|
||||||
type: array<string>
|
type: array<string>
|
||||||
include: true
|
include: true
|
||||||
groups: [Entity]
|
groups: [User_Entity]
|
||||||
groups:
|
groups:
|
||||||
exclude: true
|
exclude: true
|
||||||
email:
|
email:
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @RouteResource("Tag")
|
* @RouteResource("Tag")
|
||||||
|
*
|
||||||
|
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||||
*/
|
*/
|
||||||
class TagController extends BaseApiController
|
class TagController extends BaseApiController
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @RouteResource("Team")
|
* @RouteResource("Team")
|
||||||
|
*
|
||||||
|
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
|
||||||
*/
|
*/
|
||||||
class TeamController extends BaseApiController
|
class TeamController extends BaseApiController
|
||||||
{
|
{
|
||||||
@@ -78,6 +80,8 @@ class TeamController extends BaseApiController
|
|||||||
* @SWG\Schema(ref="#/definitions/TeamEntity"),
|
* @SWG\Schema(ref="#/definitions/TeamEntity"),
|
||||||
* )
|
* )
|
||||||
*
|
*
|
||||||
|
* @Security("is_granted('view_team')")
|
||||||
|
*
|
||||||
* @ApiSecurity(name="apiUser")
|
* @ApiSecurity(name="apiUser")
|
||||||
* @ApiSecurity(name="apiToken")
|
* @ApiSecurity(name="apiToken")
|
||||||
*/
|
*/
|
||||||
@@ -90,7 +94,7 @@ class TeamController extends BaseApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$view = new View($data, 200);
|
$view = new View($data, 200);
|
||||||
$view->getContext()->setGroups(['Default', 'Entity', 'Team']);
|
$view->getContext()->setGroups(['Default', 'Entity', 'Team', 'Team_Entity']);
|
||||||
|
|
||||||
return $this->viewHandler->handle($view);
|
return $this->viewHandler->handle($view);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class UserController extends BaseApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$view = new View($user, 200);
|
$view = new View($user, 200);
|
||||||
$view->getContext()->setGroups(['Default', 'Entity', 'User']);
|
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
|
||||||
|
|
||||||
return $this->viewHandler->handle($view);
|
return $this->viewHandler->handle($view);
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ class UserController extends BaseApiController
|
|||||||
public function meAction(): Response
|
public function meAction(): Response
|
||||||
{
|
{
|
||||||
$view = new View($this->getUser(), 200);
|
$view = new View($this->getUser(), 200);
|
||||||
$view->getContext()->setGroups(['Default', 'Entity', 'User']);
|
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
|
||||||
|
|
||||||
return $this->viewHandler->handle($view);
|
return $this->viewHandler->handle($view);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '1.6.1';
|
public const VERSION = '1.6.2';
|
||||||
/**
|
/**
|
||||||
* The current release status, either "stable" or "dev"
|
* The current release status, either "stable" or "dev"
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
|
use App\Event\InvoicePostRenderEvent;
|
||||||
|
use App\Event\InvoicePreRenderEvent;
|
||||||
use App\Form\InvoiceTemplateForm;
|
use App\Form\InvoiceTemplateForm;
|
||||||
use App\Form\Toolbar\InvoiceToolbarForm;
|
use App\Form\Toolbar\InvoiceToolbarForm;
|
||||||
|
use App\Invoice\InvoiceFormatter;
|
||||||
use App\Invoice\InvoiceItemInterface;
|
use App\Invoice\InvoiceItemInterface;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Invoice\ServiceInvoice;
|
use App\Invoice\ServiceInvoice;
|
||||||
@@ -25,6 +28,7 @@ use Symfony\Component\Form\SubmitButton;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller used to create invoices and manage invoice templates.
|
* Controller used to create invoices and manage invoice templates.
|
||||||
@@ -32,26 +36,36 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
* @Route(path="/invoice")
|
* @Route(path="/invoice")
|
||||||
* @Security("is_granted('view_invoice')")
|
* @Security("is_granted('view_invoice')")
|
||||||
*/
|
*/
|
||||||
class InvoiceController extends AbstractController
|
final class InvoiceController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var ServiceInvoice
|
* @var ServiceInvoice
|
||||||
*/
|
*/
|
||||||
protected $service;
|
private $service;
|
||||||
/**
|
/**
|
||||||
* @var InvoiceTemplateRepository
|
* @var InvoiceTemplateRepository
|
||||||
*/
|
*/
|
||||||
protected $invoiceRepository;
|
private $invoiceRepository;
|
||||||
/**
|
/**
|
||||||
* @var UserDateTimeFactory
|
* @var UserDateTimeFactory
|
||||||
*/
|
*/
|
||||||
protected $dateTimeFactory;
|
private $dateTimeFactory;
|
||||||
|
/**
|
||||||
|
* @var InvoiceFormatter
|
||||||
|
*/
|
||||||
|
private $formatter;
|
||||||
|
/**
|
||||||
|
* @var EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
private $dispatcher;
|
||||||
|
|
||||||
public function __construct(ServiceInvoice $service, InvoiceTemplateRepository $invoice, UserDateTimeFactory $dateTimeFactory)
|
public function __construct(ServiceInvoice $service, InvoiceTemplateRepository $invoice, UserDateTimeFactory $dateTimeFactory, InvoiceFormatter $formatter, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->service = $service;
|
$this->service = $service;
|
||||||
$this->invoiceRepository = $invoice;
|
$this->invoiceRepository = $invoice;
|
||||||
$this->dateTimeFactory = $dateTimeFactory;
|
$this->dateTimeFactory = $dateTimeFactory;
|
||||||
|
$this->formatter = $formatter;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,8 +129,14 @@ class InvoiceController extends AbstractController
|
|||||||
$query->setEnd($end);
|
$query->setEnd($end);
|
||||||
$query->setExported(InvoiceQuery::STATE_NOT_EXPORTED);
|
$query->setExported(InvoiceQuery::STATE_NOT_EXPORTED);
|
||||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||||
|
// limit access to data from teams
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|
||||||
|
if (!$this->isGranted('view_other_timesheet')) {
|
||||||
|
// limit access to own data
|
||||||
|
$query->setUser($this->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +155,15 @@ class InvoiceController extends AbstractController
|
|||||||
|
|
||||||
foreach ($this->service->getRenderer() as $renderer) {
|
foreach ($this->service->getRenderer() as $renderer) {
|
||||||
if ($renderer->supports($document)) {
|
if ($renderer->supports($document)) {
|
||||||
|
$this->dispatcher->dispatch(new InvoicePreRenderEvent($model, $document, $renderer));
|
||||||
|
|
||||||
$response = $renderer->render($document, $model);
|
$response = $renderer->render($document, $model);
|
||||||
if ($query->isMarkAsExported()) {
|
if ($query->isMarkAsExported()) {
|
||||||
$this->markEntriesAsExported($entries);
|
$this->markEntriesAsExported($entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->dispatcher->dispatch(new InvoicePostRenderEvent($model, $document, $renderer, $response));
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,9 +240,10 @@ class InvoiceController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
protected function prepareModel(InvoiceQuery $query): InvoiceModel
|
protected function prepareModel(InvoiceQuery $query): InvoiceModel
|
||||||
{
|
{
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel($this->formatter);
|
||||||
$model
|
$model
|
||||||
->setQuery($query)
|
->setQuery($query)
|
||||||
|
->setUser($this->getUser())
|
||||||
->setCustomer($query->getCustomer())
|
->setCustomer($query->getCustomer())
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -340,6 +365,7 @@ class InvoiceController extends AbstractController
|
|||||||
return $this->createForm(InvoiceToolbarForm::class, $query, [
|
return $this->createForm(InvoiceToolbarForm::class, $query, [
|
||||||
'action' => $this->generateUrl('invoice', []),
|
'action' => $this->generateUrl('invoice', []),
|
||||||
'method' => $method,
|
'method' => $method,
|
||||||
|
'include_user' => $this->isGranted('view_other_timesheet'),
|
||||||
'attr' => [
|
'attr' => [
|
||||||
'id' => 'invoice-print-form'
|
'id' => 'invoice-print-form'
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserPreference;
|
||||||
use App\Event\PrepareUserEvent;
|
use App\Event\PrepareUserEvent;
|
||||||
use App\Form\UserApiTokenType;
|
use App\Form\UserApiTokenType;
|
||||||
use App\Form\UserEditType;
|
use App\Form\UserEditType;
|
||||||
@@ -20,6 +21,7 @@ use App\Form\UserTeamsType;
|
|||||||
use App\Repository\TeamRepository;
|
use App\Repository\TeamRepository;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
use App\Voter\UserVoter;
|
use App\Voter\UserVoter;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
@@ -210,6 +212,13 @@ class ProfileController extends AbstractController
|
|||||||
$event = new PrepareUserEvent($profile);
|
$event = new PrepareUserEvent($profile);
|
||||||
$this->dispatcher->dispatch($event);
|
$this->dispatcher->dispatch($event);
|
||||||
|
|
||||||
|
/** @var \ArrayIterator $iterator */
|
||||||
|
$iterator = $profile->getPreferences()->getIterator();
|
||||||
|
$iterator->uasort(function (UserPreference $a, UserPreference $b) {
|
||||||
|
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
|
||||||
|
});
|
||||||
|
$profile->setPreferences(new ArrayCollection(iterator_to_array($iterator)));
|
||||||
|
|
||||||
$original = [];
|
$original = [];
|
||||||
foreach ($profile->getPreferences() as $preference) {
|
foreach ($profile->getPreferences() as $preference) {
|
||||||
$original[$preference->getName()] = $preference;
|
$original[$preference->getName()] = $preference;
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
'action' => $this->generateUrl($this->getMultiUpdateRoute(), []),
|
'action' => $this->generateUrl($this->getMultiUpdateRoute(), []),
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'include_exported' => $this->isGranted($this->getPermissionEditExport()),
|
'include_exported' => $this->isGranted($this->getPermissionEditExport()),
|
||||||
'include_user' => $this->includeUserInForms(),
|
'include_user' => $this->includeUserInForms('multi'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
'action' => $this->generateUrl($this->getCreateRoute()),
|
'action' => $this->generateUrl($this->getCreateRoute()),
|
||||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
'include_exported' => $this->isGranted('edit_export', $entry),
|
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||||
'include_user' => $this->includeUserInForms(),
|
'include_user' => $this->includeUserInForms('create'),
|
||||||
'allow_begin_datetime' => $mode->canEditBegin(),
|
'allow_begin_datetime' => $mode->canEditBegin(),
|
||||||
'allow_end_datetime' => $mode->canEditEnd(),
|
'allow_end_datetime' => $mode->canEditEnd(),
|
||||||
'allow_duration' => $mode->canEditDuration(),
|
'allow_duration' => $mode->canEditDuration(),
|
||||||
@@ -450,7 +450,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
]),
|
]),
|
||||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
'include_exported' => $this->isGranted('edit_export', $entry),
|
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||||
'include_user' => $this->includeUserInForms(),
|
'include_user' => $this->includeUserInForms('edit'),
|
||||||
'allow_begin_datetime' => $mode->canEditBegin(),
|
'allow_begin_datetime' => $mode->canEditBegin(),
|
||||||
'allow_end_datetime' => $mode->canEditEnd(),
|
'allow_end_datetime' => $mode->canEditEnd(),
|
||||||
'allow_duration' => $mode->canEditDuration(),
|
'allow_duration' => $mode->canEditDuration(),
|
||||||
@@ -469,7 +469,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
'page' => $query->getPage(),
|
'page' => $query->getPage(),
|
||||||
]),
|
]),
|
||||||
'method' => 'GET',
|
'method' => 'GET',
|
||||||
'include_user' => $this->includeUserInForms(),
|
'include_user' => $this->includeUserInForms('toolbar'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
return (bool) $this->getUser()->getPreferenceValue('timesheet.daily_stats', false);
|
return (bool) $this->getUser()->getPreferenceValue('timesheet.daily_stats', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function includeUserInForms(): bool
|
protected function includeUserInForms(string $formName): bool
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,9 +118,13 @@ class TimesheetTeamController extends TimesheetAbstractController
|
|||||||
return TimesheetAdminEditForm::class;
|
return TimesheetAdminEditForm::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function includeUserInForms(): bool
|
protected function includeUserInForms(string $formName): bool
|
||||||
{
|
{
|
||||||
return true;
|
if ($formName === 'toolbar') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->isGranted('edit_other_timesheet');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getTimesheetRoute(): string
|
protected function getTimesheetRoute(): string
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class InvoiceFixtures extends Fixture
|
|||||||
|
|
||||||
$paymentTerms_alt =
|
$paymentTerms_alt =
|
||||||
$faker->firstName . ', thank you very much. We really appreciate your business.' . PHP_EOL .
|
$faker->firstName . ', thank you very much. We really appreciate your business.' . PHP_EOL .
|
||||||
'Please send payments before the due date.I would like to thank you for your confidence and will gladly be there for you in the future.'
|
'Please send payments before the due date. I would like to thank you for your confidence and will gladly be there for you in the future.'
|
||||||
;
|
;
|
||||||
|
|
||||||
$paymentTerms_de =
|
$paymentTerms_de =
|
||||||
@@ -114,7 +114,6 @@ class InvoiceFixtures extends Fixture
|
|||||||
protected function generateAddress(Generator $faker)
|
protected function generateAddress(Generator $faker)
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
'Kimai Inc.' . PHP_EOL .
|
|
||||||
$faker->streetAddress . PHP_EOL .
|
$faker->streetAddress . PHP_EOL .
|
||||||
$faker->city . ', ' . $faker->stateAbbr . ' ' . $faker->postcode
|
$faker->city . ', ' . $faker->stateAbbr . ' ' . $faker->postcode
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ class AppExtension extends Extension
|
|||||||
$config['timesheet']['rounding'][$name]['days'] = implode(',', $settings['days']);
|
$config['timesheet']['rounding'][$name]['days'] = implode(',', $settings['days']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$config['invoice']['documents'] = array_merge($config['invoice']['documents'], $config['invoice']['defaults']);
|
||||||
|
unset($config['invoice']['defaults']);
|
||||||
|
|
||||||
// safe alternatives to %kernel.project_dir%
|
// safe alternatives to %kernel.project_dir%
|
||||||
$container->setParameter('kimai.data_dir', $config['data_dir']);
|
$container->setParameter('kimai.data_dir', $config['data_dir']);
|
||||||
$container->setParameter('kimai.plugin_dir', $config['plugin_dir']);
|
$container->setParameter('kimai.plugin_dir', $config['plugin_dir']);
|
||||||
|
|||||||
@@ -211,14 +211,18 @@ class Configuration implements ConfigurationInterface
|
|||||||
$node
|
$node
|
||||||
->addDefaultsIfNotSet()
|
->addDefaultsIfNotSet()
|
||||||
->children()
|
->children()
|
||||||
->arrayNode('documents')
|
->arrayNode('defaults')
|
||||||
->requiresAtLeastOneElement()
|
|
||||||
->scalarPrototype()->end()
|
->scalarPrototype()->end()
|
||||||
->defaultValue([
|
->defaultValue([
|
||||||
'var/invoices/',
|
'var/invoices/',
|
||||||
'templates/invoice/renderer/'
|
'templates/invoice/renderer/'
|
||||||
])
|
])
|
||||||
->end()
|
->end()
|
||||||
|
->arrayNode('documents')
|
||||||
|
->requiresAtLeastOneElement()
|
||||||
|
->scalarPrototype()->end()
|
||||||
|
->defaultValue([])
|
||||||
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class InvoiceTemplate
|
|||||||
*
|
*
|
||||||
* @ORM\Column(name="calculator", type="string", length=20, nullable=false)
|
* @ORM\Column(name="calculator", type="string", length=20, nullable=false)
|
||||||
* @Assert\NotBlank()
|
* @Assert\NotBlank()
|
||||||
|
* @Assert\Length(max=20)
|
||||||
*/
|
*/
|
||||||
private $calculator = 'default';
|
private $calculator = 'default';
|
||||||
/**
|
/**
|
||||||
@@ -106,6 +107,7 @@ class InvoiceTemplate
|
|||||||
*
|
*
|
||||||
* @ORM\Column(name="number_generator", type="string", length=20, nullable=false)
|
* @ORM\Column(name="number_generator", type="string", length=20, nullable=false)
|
||||||
* @Assert\NotBlank()
|
* @Assert\NotBlank()
|
||||||
|
* @Assert\Length(max=20)
|
||||||
*/
|
*/
|
||||||
private $numberGenerator = 'default';
|
private $numberGenerator = 'default';
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ class InvoiceTemplate
|
|||||||
*
|
*
|
||||||
* @ORM\Column(name="renderer", type="string", length=20, nullable=false)
|
* @ORM\Column(name="renderer", type="string", length=20, nullable=false)
|
||||||
* @Assert\NotBlank()
|
* @Assert\NotBlank()
|
||||||
|
* @Assert\Length(max=20)
|
||||||
*/
|
*/
|
||||||
private $renderer = 'default';
|
private $renderer = 'default';
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ class UserPreference
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private $options = [];
|
private $options = [];
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $order = 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
@@ -246,4 +250,16 @@ class UserPreference
|
|||||||
|
|
||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getOrder(): int
|
||||||
|
{
|
||||||
|
return $this->order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOrder(int $order): UserPreference
|
||||||
|
{
|
||||||
|
$this->order = $order;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/Event/InvoicePostRenderEvent.php
Normal file
64
src/Event/InvoicePostRenderEvent.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
use App\Entity\InvoiceDocument;
|
||||||
|
use App\Invoice\InvoiceModel;
|
||||||
|
use App\Invoice\RendererInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
|
final class InvoicePostRenderEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var InvoiceModel
|
||||||
|
*/
|
||||||
|
private $model;
|
||||||
|
/**
|
||||||
|
* @var InvoiceDocument
|
||||||
|
*/
|
||||||
|
private $document;
|
||||||
|
/**
|
||||||
|
* @var RendererInterface
|
||||||
|
*/
|
||||||
|
private $renderer;
|
||||||
|
/**
|
||||||
|
* @var Response
|
||||||
|
*/
|
||||||
|
private $response;
|
||||||
|
|
||||||
|
public function __construct(InvoiceModel $model, InvoiceDocument $document, RendererInterface $renderer, Response $response)
|
||||||
|
{
|
||||||
|
$this->model = $model;
|
||||||
|
$this->document = $document;
|
||||||
|
$this->renderer = $renderer;
|
||||||
|
$this->response = $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModel(): InvoiceModel
|
||||||
|
{
|
||||||
|
return $this->model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDocument(): InvoiceDocument
|
||||||
|
{
|
||||||
|
return $this->document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRenderer(): RendererInterface
|
||||||
|
{
|
||||||
|
return $this->renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResponse(): Response
|
||||||
|
{
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Event/InvoicePreRenderEvent.php
Normal file
53
src/Event/InvoicePreRenderEvent.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
use App\Entity\InvoiceDocument;
|
||||||
|
use App\Invoice\InvoiceModel;
|
||||||
|
use App\Invoice\RendererInterface;
|
||||||
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
|
final class InvoicePreRenderEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var InvoiceModel
|
||||||
|
*/
|
||||||
|
private $model;
|
||||||
|
/**
|
||||||
|
* @var InvoiceDocument
|
||||||
|
*/
|
||||||
|
private $document;
|
||||||
|
/**
|
||||||
|
* @var RendererInterface
|
||||||
|
*/
|
||||||
|
private $renderer;
|
||||||
|
|
||||||
|
public function __construct(InvoiceModel $model, InvoiceDocument $document, RendererInterface $renderer)
|
||||||
|
{
|
||||||
|
$this->model = $model;
|
||||||
|
$this->document = $document;
|
||||||
|
$this->renderer = $renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModel(): InvoiceModel
|
||||||
|
{
|
||||||
|
return $this->model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDocument(): InvoiceDocument
|
||||||
|
{
|
||||||
|
return $this->document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRenderer(): RendererInterface
|
||||||
|
{
|
||||||
|
return $this->renderer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,10 @@ use App\Event\DashboardEvent;
|
|||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
|
use App\Repository\Query\ActivityQuery;
|
||||||
|
use App\Repository\Query\CustomerQuery;
|
||||||
|
use App\Repository\Query\ProjectQuery;
|
||||||
|
use App\Repository\Query\UserQuery;
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use App\Widget\Type\CompoundRow;
|
use App\Widget\Type\CompoundRow;
|
||||||
use App\Widget\Type\More;
|
use App\Widget\Type\More;
|
||||||
@@ -82,8 +86,9 @@ class DashboardSubscriber implements EventSubscriberInterface
|
|||||||
*/
|
*/
|
||||||
public function onDashboardEvent(DashboardEvent $event)
|
public function onDashboardEvent(DashboardEvent $event)
|
||||||
{
|
{
|
||||||
|
$user = $event->getUser();
|
||||||
$section = new CompoundRow();
|
$section = new CompoundRow();
|
||||||
$section->setTitle('ROLE_ADMIN');
|
$section->setTitle('');
|
||||||
$section->setOrder(100);
|
$section->setOrder(100);
|
||||||
|
|
||||||
if ($this->security->isGranted('view_user')) {
|
if ($this->security->isGranted('view_user')) {
|
||||||
@@ -91,7 +96,7 @@ class DashboardSubscriber implements EventSubscriberInterface
|
|||||||
(new More())
|
(new More())
|
||||||
->setId('userTotal')
|
->setId('userTotal')
|
||||||
->setTitle('stats.userTotal')
|
->setTitle('stats.userTotal')
|
||||||
->setData($this->user->countUser())
|
->setData($this->user->countUsersForQuery((new UserQuery())->setCurrentUser($user)))
|
||||||
->setOptions([
|
->setOptions([
|
||||||
'route' => 'admin_user',
|
'route' => 'admin_user',
|
||||||
'icon' => 'user',
|
'icon' => 'user',
|
||||||
@@ -105,7 +110,7 @@ class DashboardSubscriber implements EventSubscriberInterface
|
|||||||
(new More())
|
(new More())
|
||||||
->setId('customerTotal')
|
->setId('customerTotal')
|
||||||
->setTitle('stats.customerTotal')
|
->setTitle('stats.customerTotal')
|
||||||
->setData($this->customer->countCustomer())
|
->setData($this->customer->countCustomersForQuery((new CustomerQuery())->setCurrentUser($user)))
|
||||||
->setOptions([
|
->setOptions([
|
||||||
'route' => 'admin_customer',
|
'route' => 'admin_customer',
|
||||||
'icon' => 'customer',
|
'icon' => 'customer',
|
||||||
@@ -119,7 +124,7 @@ class DashboardSubscriber implements EventSubscriberInterface
|
|||||||
(new More())
|
(new More())
|
||||||
->setId('projectTotal')
|
->setId('projectTotal')
|
||||||
->setTitle('stats.projectTotal')
|
->setTitle('stats.projectTotal')
|
||||||
->setData($this->project->countProject())
|
->setData($this->project->countProjectsForQuery((new ProjectQuery())->setCurrentUser($user)))
|
||||||
->setOptions([
|
->setOptions([
|
||||||
'route' => 'admin_project',
|
'route' => 'admin_project',
|
||||||
'icon' => 'project',
|
'icon' => 'project',
|
||||||
@@ -133,7 +138,7 @@ class DashboardSubscriber implements EventSubscriberInterface
|
|||||||
(new More())
|
(new More())
|
||||||
->setId('activityTotal')
|
->setId('activityTotal')
|
||||||
->setTitle('stats.activityTotal')
|
->setTitle('stats.activityTotal')
|
||||||
->setData($this->activity->countActivity())
|
->setData($this->activity->countActivitiesForQuery((new ActivityQuery())->setCurrentUser($user)))
|
||||||
->setOptions([
|
->setOptions([
|
||||||
'route' => 'admin_activity',
|
'route' => 'admin_activity',
|
||||||
'icon' => 'activity',
|
'icon' => 'activity',
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
|||||||
|
|
||||||
if ($auth->isGranted('view_invoice')) {
|
if ($auth->isGranted('view_invoice')) {
|
||||||
$invoice = new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], $this->getIcon('invoice'));
|
$invoice = new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], $this->getIcon('invoice'));
|
||||||
$invoice->setChildRoutes(['admin_invoice_template', 'admin_invoice_template_edit', 'admin_invoice_template_create']);
|
$invoice->setChildRoutes(['admin_invoice_template', 'admin_invoice_template_edit', 'admin_invoice_template_create', 'admin_invoice_template_copy']);
|
||||||
$menu->addItem($invoice);
|
$menu->addItem($invoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
->setName(UserPreference::HOURLY_RATE)
|
->setName(UserPreference::HOURLY_RATE)
|
||||||
->setValue(0)
|
->setValue(0)
|
||||||
|
->setOrder(100)
|
||||||
->setType(MoneyType::class)
|
->setType(MoneyType::class)
|
||||||
->setEnabled($enableHourlyRate)
|
->setEnabled($enableHourlyRate)
|
||||||
->setOptions($hourlyRateOptions)
|
->setOptions($hourlyRateOptions)
|
||||||
@@ -112,36 +113,43 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
->setName(UserPreference::TIMEZONE)
|
->setName(UserPreference::TIMEZONE)
|
||||||
->setValue($this->getDefaultTimezone())
|
->setValue($this->getDefaultTimezone())
|
||||||
|
->setOrder(200)
|
||||||
->setType(TimezoneType::class),
|
->setType(TimezoneType::class),
|
||||||
|
|
||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
->setName(UserPreference::LOCALE)
|
->setName(UserPreference::LOCALE)
|
||||||
->setValue($this->getDefaultLanguage())
|
->setValue($this->getDefaultLanguage())
|
||||||
|
->setOrder(300)
|
||||||
->setType(LanguageType::class),
|
->setType(LanguageType::class),
|
||||||
|
|
||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
->setName(UserPreference::SKIN)
|
->setName(UserPreference::SKIN)
|
||||||
->setValue($this->getDefaultTheme())
|
->setValue($this->getDefaultTheme())
|
||||||
|
->setOrder(400)
|
||||||
->setType(SkinType::class),
|
->setType(SkinType::class),
|
||||||
|
|
||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
->setName('theme.collapsed_sidebar')
|
->setName('theme.collapsed_sidebar')
|
||||||
->setValue(false)
|
->setValue(false)
|
||||||
|
->setOrder(500)
|
||||||
->setType(CheckboxType::class),
|
->setType(CheckboxType::class),
|
||||||
|
|
||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
->setName('calendar.initial_view')
|
->setName('calendar.initial_view')
|
||||||
->setValue(CalendarViewType::DEFAULT_VIEW)
|
->setValue(CalendarViewType::DEFAULT_VIEW)
|
||||||
|
->setOrder(600)
|
||||||
->setType(CalendarViewType::class),
|
->setType(CalendarViewType::class),
|
||||||
|
|
||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
->setName('login.initial_view')
|
->setName('login.initial_view')
|
||||||
->setValue(InitialViewType::DEFAULT_VIEW)
|
->setValue(InitialViewType::DEFAULT_VIEW)
|
||||||
|
->setOrder(700)
|
||||||
->setType(InitialViewType::class),
|
->setType(InitialViewType::class),
|
||||||
|
|
||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
->setName('timesheet.daily_stats')
|
->setName('timesheet.daily_stats')
|
||||||
->setValue(false)
|
->setValue(false)
|
||||||
|
->setOrder(800)
|
||||||
->setType(CheckboxType::class),
|
->setType(CheckboxType::class),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -153,29 +161,22 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
{
|
{
|
||||||
$user = $event->getUser();
|
$user = $event->getUser();
|
||||||
|
|
||||||
$prefs = [];
|
|
||||||
foreach ($user->getPreferences() as $preference) {
|
|
||||||
$prefs[$preference->getName()] = $preference;
|
|
||||||
}
|
|
||||||
|
|
||||||
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user));
|
$event = new UserPreferenceEvent($user, $this->getDefaultPreferences($user));
|
||||||
$this->eventDispatcher->dispatch($event);
|
$this->eventDispatcher->dispatch($event);
|
||||||
|
|
||||||
foreach ($event->getPreferences() as $preference) {
|
foreach ($event->getPreferences() as $preference) {
|
||||||
/* @var UserPreference[] $prefs */
|
$userPref = $user->getPreference($preference->getName());
|
||||||
if (isset($prefs[$preference->getName()])) {
|
if (null !== $userPref) {
|
||||||
/* @var UserPreference $pref */
|
$userPref
|
||||||
$prefs[$preference->getName()]
|
|
||||||
->setType($preference->getType())
|
->setType($preference->getType())
|
||||||
->setConstraints($preference->getConstraints())
|
->setConstraints($preference->getConstraints())
|
||||||
->setEnabled($preference->isEnabled())
|
->setEnabled($preference->isEnabled())
|
||||||
->setOptions($preference->getOptions())
|
->setOptions($preference->getOptions())
|
||||||
|
->setOrder($preference->getOrder())
|
||||||
;
|
;
|
||||||
} else {
|
} else {
|
||||||
$prefs[$preference->getName()] = $preference;
|
$user->addPreference($preference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->setPreferences(array_values($prefs));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ class InvoiceToolbarForm extends AbstractToolbarForm
|
|||||||
{
|
{
|
||||||
$this->addSearchTermInputField($builder);
|
$this->addSearchTermInputField($builder);
|
||||||
$this->addTemplateChoice($builder);
|
$this->addTemplateChoice($builder);
|
||||||
$this->addUsersChoice($builder);
|
if ($options['include_user']) {
|
||||||
|
$this->addUsersChoice($builder);
|
||||||
|
}
|
||||||
$this->addDateRangeChoice($builder);
|
$this->addDateRangeChoice($builder);
|
||||||
$this->addCustomerChoice($builder, true);
|
$this->addCustomerChoice($builder, true);
|
||||||
$this->addProjectChoice($builder);
|
$this->addProjectChoice($builder);
|
||||||
@@ -64,6 +66,7 @@ class InvoiceToolbarForm extends AbstractToolbarForm
|
|||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => InvoiceQuery::class,
|
'data_class' => InvoiceQuery::class,
|
||||||
'csrf_protection' => false,
|
'csrf_protection' => false,
|
||||||
|
'include_user' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Entity\Timesheet;
|
|||||||
use App\Invoice\InvoiceItem;
|
use App\Invoice\InvoiceItem;
|
||||||
use App\Invoice\InvoiceItemInterface;
|
use App\Invoice\InvoiceItemInterface;
|
||||||
use App\Invoice\InvoiceItemWithAmountInterface;
|
use App\Invoice\InvoiceItemWithAmountInterface;
|
||||||
|
use App\Invoice\InvoiceItemWithTypeInterface;
|
||||||
|
|
||||||
abstract class AbstractMergedCalculator extends AbstractCalculator
|
abstract class AbstractMergedCalculator extends AbstractCalculator
|
||||||
{
|
{
|
||||||
@@ -37,6 +38,25 @@ abstract class AbstractMergedCalculator extends AbstractCalculator
|
|||||||
if ($entry instanceof InvoiceItemWithAmountInterface) {
|
if ($entry instanceof InvoiceItemWithAmountInterface) {
|
||||||
$amount = $entry->getAmount();
|
$amount = $entry->getAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($entry instanceof InvoiceItemWithTypeInterface) {
|
||||||
|
$type = $entry->getInvoiceType();
|
||||||
|
$category = $entry->getInvoiceCategory();
|
||||||
|
} else {
|
||||||
|
$type = InvoiceItem::TYPE_TIMESHEET;
|
||||||
|
$category = InvoiceItem::CATEGORY_TIMESHEET_WORK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $invoiceItem->getType() && $type !== $invoiceItem->getType()) {
|
||||||
|
$type = InvoiceItem::TYPE_MIXED;
|
||||||
|
}
|
||||||
|
if (null !== $invoiceItem->getCategory() && $category !== $invoiceItem->getCategory()) {
|
||||||
|
$category = InvoiceItem::CATEGORY_MIXED;
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoiceItem->setType($type);
|
||||||
|
$invoiceItem->setCategory($category);
|
||||||
|
|
||||||
$invoiceItem->setAmount($invoiceItem->getAmount() + $amount);
|
$invoiceItem->setAmount($invoiceItem->getAmount() + $amount);
|
||||||
$invoiceItem->setUser($entry->getUser());
|
$invoiceItem->setUser($entry->getUser());
|
||||||
$invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate());
|
$invoiceItem->setRate($invoiceItem->getRate() + $entry->getRate());
|
||||||
|
|||||||
104
src/Invoice/DefaultInvoiceFormatter.php
Normal file
104
src/Invoice/DefaultInvoiceFormatter.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Invoice;
|
||||||
|
|
||||||
|
use App\Twig\DateExtensions;
|
||||||
|
use App\Twig\Extensions;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final class DefaultInvoiceFormatter implements InvoiceFormatter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var DateExtensions
|
||||||
|
*/
|
||||||
|
private $dateExtension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Extensions
|
||||||
|
*/
|
||||||
|
private $extension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
* @param DateExtensions $dateExtension
|
||||||
|
* @param Extensions $extensions
|
||||||
|
*/
|
||||||
|
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
|
||||||
|
{
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->dateExtension = $dateExtension;
|
||||||
|
$this->extension = $extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDateTime(\DateTime $date)
|
||||||
|
{
|
||||||
|
return $this->dateExtension->dateShort($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedTime(\DateTime $date)
|
||||||
|
{
|
||||||
|
return $this->dateExtension->time($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $amount
|
||||||
|
* @param string $currency
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFormattedMoney($amount, $currency)
|
||||||
|
{
|
||||||
|
return $this->extension->money($amount, $currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedMonthName(\DateTime $date)
|
||||||
|
{
|
||||||
|
return $this->translator->trans($this->dateExtension->monthName($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $seconds
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDuration($seconds)
|
||||||
|
{
|
||||||
|
return $this->extension->duration($seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $seconds
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDecimalDuration($seconds)
|
||||||
|
{
|
||||||
|
return $this->extension->durationDecimal($seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrencySymbol(string $currency): string
|
||||||
|
{
|
||||||
|
return $this->extension->currency($currency);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/Invoice/InvoiceFormatter.php
Normal file
55
src/Invoice/InvoiceFormatter.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Invoice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal this is subject to change
|
||||||
|
*/
|
||||||
|
interface InvoiceFormatter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDateTime(\DateTime $date);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedTime(\DateTime $date);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $amount
|
||||||
|
* @param string|null $currency
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedMoney($amount, $currency);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedMonthName(\DateTime $date);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $seconds
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDuration($seconds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $seconds
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDecimalDuration($seconds);
|
||||||
|
|
||||||
|
public function getCurrencySymbol(string $currency): string;
|
||||||
|
}
|
||||||
@@ -18,6 +18,11 @@ use App\Entity\User;
|
|||||||
*/
|
*/
|
||||||
final class InvoiceItem
|
final class InvoiceItem
|
||||||
{
|
{
|
||||||
|
public const TYPE_TIMESHEET = 'timesheet';
|
||||||
|
public const CATEGORY_TIMESHEET_WORK = 'work';
|
||||||
|
public const TYPE_MIXED = 'mixed';
|
||||||
|
public const CATEGORY_MIXED = 'mixed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var float
|
* @var float
|
||||||
*/
|
*/
|
||||||
@@ -66,6 +71,14 @@ final class InvoiceItem
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private $additionalFields = [];
|
private $additionalFields = [];
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $type;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $category;
|
||||||
|
|
||||||
public function addAdditionalField(string $name, ?string $value): InvoiceItem
|
public function addAdditionalField(string $name, ?string $value): InvoiceItem
|
||||||
{
|
{
|
||||||
@@ -215,4 +228,28 @@ final class InvoiceItem
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getType(): ?string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(string $type): InvoiceItem
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCategory(): ?string
|
||||||
|
{
|
||||||
|
return $this->category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCategory(string $category): InvoiceItem
|
||||||
|
{
|
||||||
|
$this->category = $category;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/Invoice/InvoiceItemWithTypeInterface.php
Normal file
17
src/Invoice/InvoiceItemWithTypeInterface.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Invoice;
|
||||||
|
|
||||||
|
interface InvoiceItemWithTypeInterface
|
||||||
|
{
|
||||||
|
public function getInvoiceType(): string;
|
||||||
|
|
||||||
|
public function getInvoiceCategory(): string;
|
||||||
|
}
|
||||||
@@ -11,52 +11,57 @@ namespace App\Invoice;
|
|||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserPreference;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* InvoiceModel is the ONLY value that a RendererInterface receives for generating the invoice,
|
* InvoiceModel is the ONLY value that a RendererInterface receives for generating the invoice,
|
||||||
* besides the InvoiceDocument which is used as a "template".
|
* besides the InvoiceDocument which is used as a "template".
|
||||||
*/
|
*/
|
||||||
class InvoiceModel
|
final class InvoiceModel
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var Customer|null
|
* @var Customer|null
|
||||||
*/
|
*/
|
||||||
protected $customer;
|
private $customer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var InvoiceQuery
|
* @var InvoiceQuery
|
||||||
*/
|
*/
|
||||||
protected $query;
|
private $query;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var InvoiceItemInterface[]
|
* @var InvoiceItemInterface[]
|
||||||
*/
|
*/
|
||||||
protected $entries = [];
|
private $entries = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var InvoiceTemplate
|
* @var InvoiceTemplate
|
||||||
*/
|
*/
|
||||||
protected $template;
|
private $template;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var CalculatorInterface
|
* @var CalculatorInterface
|
||||||
*/
|
*/
|
||||||
protected $calculator;
|
private $calculator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var NumberGeneratorInterface
|
* @var NumberGeneratorInterface
|
||||||
*/
|
*/
|
||||||
protected $generator;
|
private $generator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \DateTime
|
* @var \DateTime
|
||||||
*/
|
*/
|
||||||
protected $invoiceDate;
|
private $invoiceDate;
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
/**
|
||||||
|
* @var InvoiceFormatter
|
||||||
|
*/
|
||||||
|
private $formatter;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct(InvoiceFormatter $formatter)
|
||||||
{
|
{
|
||||||
$this->invoiceDate = new \DateTime();
|
$this->invoiceDate = new \DateTime();
|
||||||
|
$this->formatter = $formatter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,4 +188,242 @@ class InvoiceModel
|
|||||||
{
|
{
|
||||||
return $this->calculator;
|
return $this->calculator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the user who is currently creating the invoice.
|
||||||
|
*
|
||||||
|
* @return User|null
|
||||||
|
*/
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(User $user): InvoiceModel
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormatter(): ?InvoiceFormatter
|
||||||
|
{
|
||||||
|
return $this->formatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$model = $this;
|
||||||
|
$customer = $model->getCustomer();
|
||||||
|
$project = $model->getQuery()->getProject();
|
||||||
|
$activity = $model->getQuery()->getActivity();
|
||||||
|
$currency = $model->getCalculator()->getCurrency();
|
||||||
|
$tax = $model->getCalculator()->getTax();
|
||||||
|
$total = $model->getCalculator()->getTotal();
|
||||||
|
$subtotal = $model->getCalculator()->getSubtotal();
|
||||||
|
$formatter = $model->getFormatter();
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'invoice.due_date' => $formatter->getFormattedDateTime($model->getDueDate()),
|
||||||
|
'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()),
|
||||||
|
'invoice.number' => $model->getNumberGenerator()->getInvoiceNumber(),
|
||||||
|
'invoice.currency' => $currency,
|
||||||
|
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
|
||||||
|
'invoice.vat' => $model->getCalculator()->getVat(),
|
||||||
|
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency),
|
||||||
|
'invoice.tax_nc' => $formatter->getFormattedMoney($tax, null),
|
||||||
|
'invoice.tax_plain' => $tax,
|
||||||
|
'invoice.total_time' => $formatter->getFormattedDuration($model->getCalculator()->getTimeWorked()),
|
||||||
|
'invoice.duration_decimal' => $formatter->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()),
|
||||||
|
'invoice.total' => $formatter->getFormattedMoney($total, $currency),
|
||||||
|
'invoice.total_nc' => $formatter->getFormattedMoney($total, null),
|
||||||
|
'invoice.total_plain' => $total,
|
||||||
|
'invoice.subtotal' => $formatter->getFormattedMoney($subtotal, $currency),
|
||||||
|
'invoice.subtotal_nc' => $formatter->getFormattedMoney($subtotal, null),
|
||||||
|
'invoice.subtotal_plain' => $subtotal,
|
||||||
|
|
||||||
|
'template.name' => $model->getTemplate()->getName(),
|
||||||
|
'template.company' => $model->getTemplate()->getCompany(),
|
||||||
|
'template.address' => $model->getTemplate()->getAddress(),
|
||||||
|
'template.title' => $model->getTemplate()->getTitle(),
|
||||||
|
'template.payment_terms' => $model->getTemplate()->getPaymentTerms(),
|
||||||
|
'template.due_days' => $model->getTemplate()->getDueDays(),
|
||||||
|
'template.vat_id' => $model->getTemplate()->getVatId(),
|
||||||
|
'template.contact' => $model->getTemplate()->getContact(),
|
||||||
|
'template.payment_details' => $model->getTemplate()->getPaymentDetails(),
|
||||||
|
|
||||||
|
'query.begin' => $formatter->getFormattedDateTime($model->getQuery()->getBegin()),
|
||||||
|
'query.day' => $model->getQuery()->getBegin()->format('d'),
|
||||||
|
'query.end' => $formatter->getFormattedDateTime($model->getQuery()->getEnd()),
|
||||||
|
'query.month' => $formatter->getFormattedMonthName($model->getQuery()->getBegin()),
|
||||||
|
'query.month_number' => $model->getQuery()->getBegin()->format('m'),
|
||||||
|
'query.year' => $model->getQuery()->getBegin()->format('Y'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (null !== $model->getUser()) {
|
||||||
|
$user = $model->getUser();
|
||||||
|
|
||||||
|
/** @var UserPreference $metaField */
|
||||||
|
foreach ($user->getPreferences() as $metaField) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'user.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'user.name' => $user->getUsername(),
|
||||||
|
'user.email' => $user->getEmail(),
|
||||||
|
'user.title' => $user->getTitle(),
|
||||||
|
'user.alias' => $user->getAlias(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $activity) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'activity.id' => $activity->getId(),
|
||||||
|
'activity.name' => $activity->getName(),
|
||||||
|
'activity.comment' => $activity->getComment(),
|
||||||
|
'activity.fixed_rate' => $formatter->getFormattedMoney($activity->getFixedRate(), $currency),
|
||||||
|
'activity.fixed_rate_nc' => $formatter->getFormattedMoney($activity->getFixedRate(), null),
|
||||||
|
'activity.fixed_rate_plain' => $activity->getFixedRate(),
|
||||||
|
'activity.hourly_rate' => $formatter->getFormattedMoney($activity->getHourlyRate(), $currency),
|
||||||
|
'activity.hourly_rate_nc' => $formatter->getFormattedMoney($activity->getHourlyRate(), null),
|
||||||
|
'activity.hourly_rate_plain' => $activity->getHourlyRate(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'activity.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $project) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'project.id' => $project->getId(),
|
||||||
|
'project.name' => $project->getName(),
|
||||||
|
'project.comment' => $project->getComment(),
|
||||||
|
'project.order_number' => $project->getOrderNumber(),
|
||||||
|
'project.order_date' => null !== $project->getOrderDate() ? $formatter->getFormattedDateTime($project->getOrderDate()) : '',
|
||||||
|
'project.fixed_rate' => $formatter->getFormattedMoney($project->getFixedRate(), $currency),
|
||||||
|
'project.fixed_rate_nc' => $formatter->getFormattedMoney($project->getFixedRate(), null),
|
||||||
|
'project.fixed_rate_plain' => $project->getFixedRate(),
|
||||||
|
'project.hourly_rate' => $formatter->getFormattedMoney($project->getHourlyRate(), $currency),
|
||||||
|
'project.hourly_rate_nc' => $formatter->getFormattedMoney($project->getHourlyRate(), null),
|
||||||
|
'project.hourly_rate_plain' => $project->getHourlyRate(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($project->getVisibleMetaFields() as $metaField) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'project.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $customer) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'customer.id' => $customer->getId(),
|
||||||
|
'customer.address' => $customer->getAddress(),
|
||||||
|
'customer.name' => $customer->getName(),
|
||||||
|
'customer.contact' => $customer->getContact(),
|
||||||
|
'customer.company' => $customer->getCompany(),
|
||||||
|
'customer.vat' => $customer->getVatId(),
|
||||||
|
'customer.number' => $customer->getNumber(),
|
||||||
|
'customer.country' => $customer->getCountry(),
|
||||||
|
'customer.homepage' => $customer->getHomepage(),
|
||||||
|
'customer.comment' => $customer->getComment(),
|
||||||
|
'customer.fixed_rate' => $formatter->getFormattedMoney($customer->getFixedRate(), $currency),
|
||||||
|
'customer.fixed_rate_nc' => $formatter->getFormattedMoney($customer->getFixedRate(), null),
|
||||||
|
'customer.fixed_rate_plain' => $customer->getFixedRate(),
|
||||||
|
'customer.hourly_rate' => $formatter->getFormattedMoney($customer->getHourlyRate(), $currency),
|
||||||
|
'customer.hourly_rate_nc' => $formatter->getFormattedMoney($customer->getHourlyRate(), null),
|
||||||
|
'customer.hourly_rate_plain' => $customer->getHourlyRate(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function itemToArray(InvoiceItem $invoiceItem): array
|
||||||
|
{
|
||||||
|
$formatter = $this->getFormatter();
|
||||||
|
|
||||||
|
$rate = $invoiceItem->getRate();
|
||||||
|
$appliedRate = $invoiceItem->getHourlyRate();
|
||||||
|
$amount = $formatter->getFormattedDuration($invoiceItem->getDuration());
|
||||||
|
$description = $invoiceItem->getDescription();
|
||||||
|
|
||||||
|
if ($invoiceItem->isFixedRate()) {
|
||||||
|
$appliedRate = $invoiceItem->getFixedRate();
|
||||||
|
$amount = $invoiceItem->getAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($description)) {
|
||||||
|
$description = $invoiceItem->getActivity()->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $invoiceItem->getUser();
|
||||||
|
|
||||||
|
// this should never happen!
|
||||||
|
if (empty($appliedRate)) {
|
||||||
|
$appliedRate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$activity = $invoiceItem->getActivity();
|
||||||
|
$project = $invoiceItem->getProject();
|
||||||
|
$customer = $project->getCustomer();
|
||||||
|
$currency = $customer->getCurrency();
|
||||||
|
|
||||||
|
$begin = $invoiceItem->getBegin();
|
||||||
|
$end = $invoiceItem->getEnd();
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'entry.row' => '',
|
||||||
|
'entry.description' => $description,
|
||||||
|
'entry.amount' => $amount,
|
||||||
|
'entry.type' => $invoiceItem->getType(),
|
||||||
|
'entry.category' => $invoiceItem->getCategory(),
|
||||||
|
'entry.rate' => $formatter->getFormattedMoney($appliedRate, $currency),
|
||||||
|
'entry.rate_nc' => $formatter->getFormattedMoney($appliedRate, null),
|
||||||
|
'entry.rate_plain' => $appliedRate,
|
||||||
|
'entry.total' => $formatter->getFormattedMoney($rate, $currency),
|
||||||
|
'entry.total_nc' => $formatter->getFormattedMoney($rate, null),
|
||||||
|
'entry.total_plain' => $rate,
|
||||||
|
'entry.currency' => $currency,
|
||||||
|
'entry.duration' => $invoiceItem->getDuration(),
|
||||||
|
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($invoiceItem->getDuration()),
|
||||||
|
'entry.duration_minutes' => number_format($invoiceItem->getDuration() / 60),
|
||||||
|
'entry.begin' => $formatter->getFormattedDateTime($begin),
|
||||||
|
'entry.begin_time' => $formatter->getFormattedTime($begin),
|
||||||
|
'entry.begin_timestamp' => $begin->getTimestamp(),
|
||||||
|
'entry.end' => $formatter->getFormattedDateTime($end),
|
||||||
|
'entry.end_time' => $formatter->getFormattedTime($end),
|
||||||
|
'entry.end_timestamp' => $end->getTimestamp(),
|
||||||
|
'entry.date' => $formatter->getFormattedDateTime($begin),
|
||||||
|
'entry.user_id' => $user->getId(),
|
||||||
|
'entry.user_name' => $user->getUsername(),
|
||||||
|
'entry.user_title' => $user->getTitle(),
|
||||||
|
'entry.user_alias' => $user->getAlias(),
|
||||||
|
'entry.activity' => $activity->getName(),
|
||||||
|
'entry.activity_id' => $activity->getId(),
|
||||||
|
'entry.project' => $project->getName(),
|
||||||
|
'entry.project_id' => $project->getId(),
|
||||||
|
'entry.customer' => $customer->getName(),
|
||||||
|
'entry.customer_id' => $customer->getId(),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($invoiceItem->getAdditionalFields() as $name => $value) {
|
||||||
|
$values = array_merge($values, [
|
||||||
|
'entry.meta.' . $name => $value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,93 +9,54 @@
|
|||||||
|
|
||||||
namespace App\Invoice\Renderer;
|
namespace App\Invoice\Renderer;
|
||||||
|
|
||||||
use App\Twig\DateExtensions;
|
use App\Entity\InvoiceDocument;
|
||||||
use App\Twig\Extensions;
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
abstract class AbstractRenderer
|
abstract class AbstractRenderer
|
||||||
{
|
{
|
||||||
use RendererTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DateExtensions
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
protected $dateExtension;
|
abstract protected function getFileExtensions();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Extensions
|
|
||||||
*/
|
|
||||||
protected $extension;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var TranslatorInterface
|
|
||||||
*/
|
|
||||||
protected $translator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param TranslatorInterface $translator
|
|
||||||
* @param DateExtensions $dateExtension
|
|
||||||
* @param Extensions $extensions
|
|
||||||
*/
|
|
||||||
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
|
|
||||||
{
|
|
||||||
$this->translator = $translator;
|
|
||||||
$this->dateExtension = $dateExtension;
|
|
||||||
$this->extension = $extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $date
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedDateTime(\DateTime $date)
|
|
||||||
{
|
|
||||||
return $this->dateExtension->dateShort($date);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $date
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedTime(\DateTime $date)
|
|
||||||
{
|
|
||||||
return $this->dateExtension->time($date);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $amount
|
|
||||||
* @param string $currency
|
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function getFormattedMoney($amount, $currency)
|
abstract protected function getContentType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param InvoiceDocument $document
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function supports(InvoiceDocument $document): bool
|
||||||
{
|
{
|
||||||
return $this->extension->money($amount, $currency);
|
foreach ($this->getFileExtensions() as $extension) {
|
||||||
|
if (stripos($document->getFilename(), $extension) !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \DateTime $date
|
* @param mixed $file
|
||||||
* @return mixed
|
* @param string $filename
|
||||||
|
* @return BinaryFileResponse
|
||||||
*/
|
*/
|
||||||
protected function getFormattedMonthName(\DateTime $date)
|
protected function getFileResponse($file, $filename)
|
||||||
{
|
{
|
||||||
return $this->translator->trans($this->dateExtension->monthName($date));
|
$response = new BinaryFileResponse($file);
|
||||||
}
|
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||||
|
|
||||||
/**
|
$response->headers->set('Content-Type', $this->getContentType());
|
||||||
* @param int $seconds
|
$response->headers->set('Content-Disposition', $disposition);
|
||||||
* @return mixed
|
$response->deleteFileAfterSend(true);
|
||||||
*/
|
|
||||||
protected function getFormattedDuration($seconds)
|
|
||||||
{
|
|
||||||
return $this->extension->duration($seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return $response;
|
||||||
* @param int $seconds
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedDecimalDuration($seconds)
|
|
||||||
{
|
|
||||||
return $this->extension->durationDecimal($seconds);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
|
abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +43,7 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
|
|||||||
$spreadsheet = IOFactory::load($document->getFilename());
|
$spreadsheet = IOFactory::load($document->getFilename());
|
||||||
$worksheet = $spreadsheet->getActiveSheet();
|
$worksheet = $spreadsheet->getActiveSheet();
|
||||||
$entries = $model->getCalculator()->getEntries();
|
$entries = $model->getCalculator()->getEntries();
|
||||||
$replacer = $this->modelToReplacer($model);
|
$sheetReplacer = $model->toArray();
|
||||||
$invoiceItemCount = count($entries);
|
$invoiceItemCount = count($entries);
|
||||||
if ($invoiceItemCount > 1) {
|
if ($invoiceItemCount > 1) {
|
||||||
$this->addTemplateRows($worksheet, $invoiceItemCount);
|
$this->addTemplateRows($worksheet, $invoiceItemCount);
|
||||||
@@ -57,26 +60,37 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
|
|||||||
$entryRow = 0;
|
$entryRow = 0;
|
||||||
|
|
||||||
foreach ($worksheet->getRowIterator() as $row) {
|
foreach ($worksheet->getRowIterator() as $row) {
|
||||||
$invoiceItem = $entries[$entryRow];
|
|
||||||
$sheetValues = false;
|
$sheetValues = false;
|
||||||
foreach ($row->getCellIterator() as $cell) {
|
foreach ($row->getCellIterator() as $cell) {
|
||||||
$value = $cell->getValue();
|
$value = $cell->getValue();
|
||||||
if (stripos($value, '${entry.') !== false) {
|
$replacer = null;
|
||||||
if ($sheetValues === false) {
|
if (stripos($value, '${') === false) {
|
||||||
$sheetValues = $this->invoiceItemToArray($invoiceItem);
|
continue;
|
||||||
}
|
|
||||||
$searcher = str_replace('${', '', $value);
|
|
||||||
$searcher = str_replace('}', '', $searcher);
|
|
||||||
if (isset($sheetValues[$searcher])) {
|
|
||||||
$cell->setValue($sheetValues[$searcher]);
|
|
||||||
}
|
|
||||||
} elseif (stripos($value, '${') !== false) {
|
|
||||||
$searcher = str_replace('${', '', $value);
|
|
||||||
$searcher = str_replace('}', '', $searcher);
|
|
||||||
if (isset($replacer[$searcher])) {
|
|
||||||
$cell->setValue($replacer[$searcher]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stripos($value, '${entry.') !== false) {
|
||||||
|
if ($sheetValues === false && isset($entries[$entryRow])) {
|
||||||
|
$sheetValues = $model->itemToArray($entries[$entryRow]);
|
||||||
|
}
|
||||||
|
$replacer = $sheetValues;
|
||||||
|
} elseif (stripos($value, '${') !== false) {
|
||||||
|
$replacer = $sheetReplacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($replacer)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can have mixed cell content, which makes it much more complicated
|
||||||
|
foreach ($replacer as $key => $content) {
|
||||||
|
$searchKey = '${' . $key . '}';
|
||||||
|
if (stripos($value, $searchKey) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$value = str_replace($searchKey, $content, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cell->setValue($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sheetValues !== false && $entryRow < $invoiceItemCount - 1) {
|
if ($sheetValues !== false && $entryRow < $invoiceItemCount - 1) {
|
||||||
@@ -105,7 +119,7 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
|
|||||||
$value = $cell->getValue();
|
$value = $cell->getValue();
|
||||||
if (stripos($value, '${entry.') !== false) {
|
if (stripos($value, '${entry.') !== false) {
|
||||||
$startRow = $row->getRowIndex();
|
$startRow = $row->getRowIndex();
|
||||||
$worksheet->insertNewRowBefore($row->getRowIndex(), $invoiceItemCount - 1);
|
$worksheet->insertNewRowBefore($startRow + 1, $invoiceItemCount - 1);
|
||||||
break 2;
|
break 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,15 +137,18 @@ abstract class AbstractSpreadsheetRenderer extends AbstractRenderer
|
|||||||
throw new \Exception('Invalid invoice document, no template row found.');
|
throw new \Exception('Invalid invoice document, no template row found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill up all new rows with template values
|
// fill up all new rows with template replacer
|
||||||
$templateRow = $invoiceItemCount + $startRow;
|
$templateRow = $startRow;
|
||||||
$iterator = $worksheet->getRowIterator($templateRow - 1, $templateRow);
|
$iterator = $worksheet->getRowIterator($templateRow, $templateRow + 1);
|
||||||
|
|
||||||
$templateColumns = [];
|
$templateColumns = [];
|
||||||
foreach ($iterator->current()->getCellIterator() as $cell) {
|
|
||||||
|
$tmpRow = $iterator->current();
|
||||||
|
foreach ($tmpRow->getCellIterator() as $cell) {
|
||||||
$templateColumns[$cell->getColumn()] = $cell->getValue();
|
$templateColumns[$cell->getColumn()] = $cell->getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
$iterator = $worksheet->getRowIterator($startRow, $templateRow - 2);
|
$iterator = $worksheet->getRowIterator($startRow, $startRow + $invoiceItemCount - 1);
|
||||||
foreach ($iterator as $row) {
|
foreach ($iterator as $row) {
|
||||||
foreach ($row->getCellIterator() as $cell) {
|
foreach ($row->getCellIterator() as $cell) {
|
||||||
$cell->setValue($templateColumns[$cell->getColumn()]);
|
$cell->setValue($templateColumns[$cell->getColumn()]);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Invoice\RendererInterface;
|
|||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
final class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use PhpOffice\PhpWord\TemplateProcessor;
|
|||||||
use Symfony\Component\HttpFoundation\File\Stream;
|
use Symfony\Component\HttpFoundation\File\Stream;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class DocxRenderer extends AbstractRenderer implements RendererInterface
|
final class DocxRenderer extends AbstractRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param InvoiceDocument $document
|
* @param InvoiceDocument $document
|
||||||
@@ -35,7 +35,7 @@ class DocxRenderer extends AbstractRenderer implements RendererInterface
|
|||||||
$xmlEscaper = new Xml();
|
$xmlEscaper = new Xml();
|
||||||
$template = new TemplateProcessor($document->getFilename());
|
$template = new TemplateProcessor($document->getFilename());
|
||||||
|
|
||||||
foreach ($this->modelToReplacer($model) as $search => $replace) {
|
foreach ($model->toArray() as $search => $replace) {
|
||||||
$replace = $xmlEscaper->escape($replace);
|
$replace = $xmlEscaper->escape($replace);
|
||||||
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
||||||
|
|
||||||
@@ -45,12 +45,18 @@ class DocxRenderer extends AbstractRenderer implements RendererInterface
|
|||||||
try {
|
try {
|
||||||
$template->cloneRow('entry.description', count($model->getCalculator()->getEntries()));
|
$template->cloneRow('entry.description', count($model->getCalculator()->getEntries()));
|
||||||
} catch (OfficeException $ex) {
|
} catch (OfficeException $ex) {
|
||||||
$template->cloneRow('entry.row', count($model->getCalculator()->getEntries()));
|
try {
|
||||||
|
$template->cloneRow('entry.row', count($model->getCalculator()->getEntries()));
|
||||||
|
} catch (OfficeException $ex) {
|
||||||
|
@trigger_error(
|
||||||
|
sprintf('Invoice document (%s) did not contain a clone row, was that on purpose?', $document->getFilename())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$i = 1;
|
$i = 1;
|
||||||
foreach ($model->getCalculator()->getEntries() as $entry) {
|
foreach ($model->getCalculator()->getEntries() as $entry) {
|
||||||
$values = $this->invoiceItemToArray($entry);
|
$values = $model->itemToArray($entry);
|
||||||
foreach ($values as $search => $replace) {
|
foreach ($values as $search => $replace) {
|
||||||
$replace = $xmlEscaper->escape($replace);
|
$replace = $xmlEscaper->escape($replace);
|
||||||
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
$replace = str_replace(PHP_EOL, '</w:t><w:br /><w:t xml:space="preserve">', $replace);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Invoice\RendererInterface;
|
|||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
final class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
|
|||||||
@@ -9,181 +9,28 @@
|
|||||||
|
|
||||||
namespace App\Invoice\Renderer;
|
namespace App\Invoice\Renderer;
|
||||||
|
|
||||||
use App\Entity\InvoiceDocument;
|
|
||||||
use App\Invoice\InvoiceItem;
|
use App\Invoice\InvoiceItem;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
|
||||||
|
|
||||||
trait RendererTrait
|
trait RendererTrait
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @var InvoiceModel
|
||||||
*/
|
*/
|
||||||
abstract protected function getFileExtensions();
|
private $model;
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
abstract protected function getContentType();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param InvoiceDocument $document
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function supports(InvoiceDocument $document): bool
|
|
||||||
{
|
|
||||||
foreach ($this->getFileExtensions() as $extension) {
|
|
||||||
if (stripos($document->getFilename(), $extension) !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $date
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
abstract protected function getFormattedDateTime(\DateTime $date);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $date
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
abstract protected function getFormattedTime(\DateTime $date);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $amount
|
|
||||||
* @param string|null $currency
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
abstract protected function getFormattedMoney($amount, $currency);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $date
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
abstract protected function getFormattedMonthName(\DateTime $date);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $seconds
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
abstract protected function getFormattedDuration($seconds);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $seconds
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
abstract protected function getFormattedDecimalDuration($seconds);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated since 1.6.2 - will be removed with 2.0
|
||||||
* @param InvoiceModel $model
|
* @param InvoiceModel $model
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function modelToReplacer(InvoiceModel $model)
|
protected function modelToReplacer(InvoiceModel $model)
|
||||||
{
|
{
|
||||||
$customer = $model->getCustomer();
|
@trigger_error('modelToReplacer() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||||
$project = $model->getQuery()->getProject();
|
|
||||||
$activity = $model->getQuery()->getActivity();
|
|
||||||
$currency = $model->getCalculator()->getCurrency();
|
|
||||||
$tax = $model->getCalculator()->getTax();
|
|
||||||
$total = $model->getCalculator()->getTotal();
|
|
||||||
$subtotal = $model->getCalculator()->getSubtotal();
|
|
||||||
|
|
||||||
$values = [
|
$this->model = $model;
|
||||||
'invoice.due_date' => $this->getFormattedDateTime($model->getDueDate()),
|
|
||||||
'invoice.date' => $this->getFormattedDateTime($model->getInvoiceDate()),
|
|
||||||
'invoice.number' => $model->getNumberGenerator()->getInvoiceNumber(),
|
|
||||||
'invoice.currency' => $currency,
|
|
||||||
'invoice.vat' => $model->getCalculator()->getVat(),
|
|
||||||
'invoice.tax' => $this->getFormattedMoney($tax, $currency),
|
|
||||||
'invoice.tax_nc' => $this->getFormattedMoney($tax, null),
|
|
||||||
'invoice.total_time' => $this->getFormattedDuration($model->getCalculator()->getTimeWorked()),
|
|
||||||
'invoice.duration_decimal' => $this->getFormattedDecimalDuration($model->getCalculator()->getTimeWorked()),
|
|
||||||
'invoice.total' => $this->getFormattedMoney($total, $currency),
|
|
||||||
'invoice.total_nc' => $this->getFormattedMoney($total, null),
|
|
||||||
'invoice.subtotal' => $this->getFormattedMoney($subtotal, $currency),
|
|
||||||
'invoice.subtotal_nc' => $this->getFormattedMoney($subtotal, null),
|
|
||||||
|
|
||||||
'template.name' => $model->getTemplate()->getName(),
|
return $model->toArray();
|
||||||
'template.company' => $model->getTemplate()->getCompany(),
|
|
||||||
'template.address' => $model->getTemplate()->getAddress(),
|
|
||||||
'template.title' => $model->getTemplate()->getTitle(),
|
|
||||||
'template.payment_terms' => $model->getTemplate()->getPaymentTerms(),
|
|
||||||
'template.due_days' => $model->getTemplate()->getDueDays(),
|
|
||||||
'template.vat_id' => $model->getTemplate()->getVatId(),
|
|
||||||
'template.contact' => $model->getTemplate()->getContact(),
|
|
||||||
'template.payment_details' => $model->getTemplate()->getPaymentDetails(),
|
|
||||||
|
|
||||||
'query.begin' => $this->getFormattedDateTime($model->getQuery()->getBegin()),
|
|
||||||
'query.day' => $model->getQuery()->getBegin()->format('d'),
|
|
||||||
'query.end' => $this->getFormattedDateTime($model->getQuery()->getEnd()),
|
|
||||||
'query.month' => $this->getFormattedMonthName($model->getQuery()->getBegin()),
|
|
||||||
'query.month_number' => $model->getQuery()->getBegin()->format('m'),
|
|
||||||
'query.year' => $model->getQuery()->getBegin()->format('Y'),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (null !== $activity) {
|
|
||||||
$values = array_merge($values, [
|
|
||||||
'activity.id' => $activity->getId(),
|
|
||||||
'activity.name' => $activity->getName(),
|
|
||||||
'activity.comment' => $activity->getComment(),
|
|
||||||
'activity.fixed_rate' => $activity->getFixedRate(),
|
|
||||||
'activity.hourly_rate' => $activity->getHourlyRate(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($activity->getVisibleMetaFields() as $metaField) {
|
|
||||||
$values = array_merge($values, [
|
|
||||||
'activity.meta.' . $metaField->getName() => $metaField->getValue(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $project) {
|
|
||||||
$values = array_merge($values, [
|
|
||||||
'project.id' => $project->getId(),
|
|
||||||
'project.name' => $project->getName(),
|
|
||||||
'project.comment' => $project->getComment(),
|
|
||||||
'project.order_number' => $project->getOrderNumber(),
|
|
||||||
'project.order_date' => null !== $project->getOrderDate() ? $this->getFormattedDateTime($project->getOrderDate()) : '',
|
|
||||||
'project.fixed_rate' => $project->getFixedRate(),
|
|
||||||
'project.hourly_rate' => $project->getHourlyRate(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($project->getVisibleMetaFields() as $metaField) {
|
|
||||||
$values = array_merge($values, [
|
|
||||||
'project.meta.' . $metaField->getName() => $metaField->getValue(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $customer) {
|
|
||||||
$values = array_merge($values, [
|
|
||||||
'customer.id' => $customer->getId(),
|
|
||||||
'customer.address' => $customer->getAddress(),
|
|
||||||
'customer.name' => $customer->getName(),
|
|
||||||
'customer.contact' => $customer->getContact(),
|
|
||||||
'customer.company' => $customer->getCompany(),
|
|
||||||
'customer.vat' => $customer->getVatId(),
|
|
||||||
'customer.number' => $customer->getNumber(),
|
|
||||||
'customer.country' => $customer->getCountry(),
|
|
||||||
'customer.homepage' => $customer->getHomepage(),
|
|
||||||
'customer.comment' => $customer->getComment(),
|
|
||||||
'customer.fixed_rate' => $customer->getFixedRate(),
|
|
||||||
'customer.hourly_rate' => $customer->getHourlyRate(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($customer->getVisibleMetaFields() as $metaField) {
|
|
||||||
$values = array_merge($values, [
|
|
||||||
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -193,94 +40,18 @@ trait RendererTrait
|
|||||||
{
|
{
|
||||||
@trigger_error('timesheetToArray() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
@trigger_error('timesheetToArray() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||||
|
|
||||||
return $this->invoiceItemToArray($invoiceItem);
|
return $this->model->itemToArray($invoiceItem);
|
||||||
}
|
|
||||||
|
|
||||||
protected function invoiceItemToArray(InvoiceItem $invoiceItem): array
|
|
||||||
{
|
|
||||||
$rate = $invoiceItem->getRate();
|
|
||||||
$hourlyRate = $invoiceItem->getHourlyRate();
|
|
||||||
$amount = $this->getFormattedDuration($invoiceItem->getDuration());
|
|
||||||
$description = $invoiceItem->getDescription();
|
|
||||||
|
|
||||||
if ($invoiceItem->isFixedRate()) {
|
|
||||||
$hourlyRate = $invoiceItem->getFixedRate();
|
|
||||||
$amount = $invoiceItem->getAmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($description)) {
|
|
||||||
$description = $invoiceItem->getActivity()->getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $invoiceItem->getUser();
|
|
||||||
|
|
||||||
// this should never happen!
|
|
||||||
if (empty($hourlyRate)) {
|
|
||||||
$hourlyRate = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$activity = $invoiceItem->getActivity();
|
|
||||||
$project = $invoiceItem->getProject();
|
|
||||||
$customer = $project->getCustomer();
|
|
||||||
$currency = $customer->getCurrency();
|
|
||||||
|
|
||||||
$begin = $invoiceItem->getBegin();
|
|
||||||
$end = $invoiceItem->getEnd();
|
|
||||||
|
|
||||||
$values = [
|
|
||||||
'entry.row' => '',
|
|
||||||
'entry.description' => $description,
|
|
||||||
'entry.amount' => $amount,
|
|
||||||
'entry.rate' => $this->getFormattedMoney($hourlyRate, $currency),
|
|
||||||
'entry.rate_nc' => $this->getFormattedMoney($hourlyRate, null),
|
|
||||||
'entry.total' => $this->getFormattedMoney($rate, $currency),
|
|
||||||
'entry.total_nc' => $this->getFormattedMoney($rate, null),
|
|
||||||
'entry.currency' => $currency,
|
|
||||||
'entry.duration' => $invoiceItem->getDuration(),
|
|
||||||
'entry.duration_decimal' => $this->getFormattedDecimalDuration($invoiceItem->getDuration()),
|
|
||||||
'entry.duration_minutes' => number_format($invoiceItem->getDuration() / 60),
|
|
||||||
'entry.begin' => $this->getFormattedDateTime($begin),
|
|
||||||
'entry.begin_time' => $this->getFormattedTime($begin),
|
|
||||||
'entry.begin_timestamp' => $begin->getTimestamp(),
|
|
||||||
'entry.end' => $this->getFormattedDateTime($end),
|
|
||||||
'entry.end_time' => $this->getFormattedTime($end),
|
|
||||||
'entry.end_timestamp' => $end->getTimestamp(),
|
|
||||||
'entry.date' => $this->getFormattedDateTime($begin),
|
|
||||||
'entry.user_id' => $user->getId(),
|
|
||||||
'entry.user_name' => $user->getUsername(),
|
|
||||||
'entry.user_title' => $user->getTitle(),
|
|
||||||
'entry.user_alias' => $user->getAlias(),
|
|
||||||
'entry.activity' => $activity->getName(),
|
|
||||||
'entry.activity_id' => $activity->getId(),
|
|
||||||
'entry.project' => $project->getName(),
|
|
||||||
'entry.project_id' => $project->getId(),
|
|
||||||
'entry.customer' => $customer->getName(),
|
|
||||||
'entry.customer_id' => $customer->getId(),
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($invoiceItem->getAdditionalFields() as $name => $value) {
|
|
||||||
$values = array_merge($values, [
|
|
||||||
'entry.meta.' . $name => $value,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $file
|
* @deprecated since 1.6.2 - will be removed with 2.0
|
||||||
* @param string $filename
|
* @param InvoiceItem $invoiceItem
|
||||||
* @return BinaryFileResponse
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function getFileResponse($file, $filename)
|
protected function invoiceItemToArray(InvoiceItem $invoiceItem): array
|
||||||
{
|
{
|
||||||
$response = new BinaryFileResponse($file);
|
@trigger_error('invoiceItemToArray() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
|
||||||
|
|
||||||
$response->headers->set('Content-Type', $this->getContentType());
|
return $this->model->itemToArray($invoiceItem);
|
||||||
$response->headers->set('Content-Disposition', $disposition);
|
|
||||||
$response->deleteFileAfterSend(true);
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use App\Invoice\RendererInterface;
|
|||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
final class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
|
|||||||
@@ -346,6 +346,18 @@ class ActivityRepository extends EntityRepository
|
|||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countActivitiesForQuery(ActivityQuery $query): int
|
||||||
|
{
|
||||||
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
$qb
|
||||||
|
->resetDQLPart('select')
|
||||||
|
->resetDQLPart('orderBy')
|
||||||
|
->select($qb->expr()->countDistinct('a.id'))
|
||||||
|
;
|
||||||
|
|
||||||
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
public function getPagerfantaForQuery(ActivityQuery $query): Pagerfanta
|
public function getPagerfantaForQuery(ActivityQuery $query): Pagerfanta
|
||||||
{
|
{
|
||||||
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||||
@@ -357,14 +369,7 @@ class ActivityRepository extends EntityRepository
|
|||||||
|
|
||||||
protected function getPaginatorForQuery(ActivityQuery $query): PaginatorInterface
|
protected function getPaginatorForQuery(ActivityQuery $query): PaginatorInterface
|
||||||
{
|
{
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$counter = $this->countActivitiesForQuery($query);
|
||||||
$qb
|
|
||||||
->resetDQLPart('select')
|
|
||||||
->resetDQLPart('orderBy')
|
|
||||||
->select($qb->expr()->countDistinct('a.id'))
|
|
||||||
;
|
|
||||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
|
||||||
|
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
|
||||||
return new LoaderPaginator(new ActivityLoader($qb->getEntityManager()), $qb, $counter);
|
return new LoaderPaginator(new ActivityLoader($qb->getEntityManager()), $qb, $counter);
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ class CustomerRepository extends EntityRepository
|
|||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getPaginatorForQuery(CustomerQuery $query): PaginatorInterface
|
public function countCustomersForQuery(CustomerQuery $query): int
|
||||||
{
|
{
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
$qb
|
$qb
|
||||||
@@ -280,8 +280,13 @@ class CustomerRepository extends EntityRepository
|
|||||||
->resetDQLPart('orderBy')
|
->resetDQLPart('orderBy')
|
||||||
->select($qb->expr()->countDistinct('c.id'))
|
->select($qb->expr()->countDistinct('c.id'))
|
||||||
;
|
;
|
||||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
|
||||||
|
|
||||||
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPaginatorForQuery(CustomerQuery $query): PaginatorInterface
|
||||||
|
{
|
||||||
|
$counter = $this->countCustomersForQuery($query);
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
|
||||||
return new LoaderPaginator(new CustomerLoader($qb->getEntityManager()), $qb, $counter);
|
return new LoaderPaginator(new CustomerLoader($qb->getEntityManager()), $qb, $counter);
|
||||||
|
|||||||
@@ -283,6 +283,18 @@ class ProjectRepository extends EntityRepository
|
|||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countProjectsForQuery(ProjectQuery $query): int
|
||||||
|
{
|
||||||
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
$qb
|
||||||
|
->resetDQLPart('select')
|
||||||
|
->resetDQLPart('orderBy')
|
||||||
|
->select($qb->expr()->countDistinct('p.id'))
|
||||||
|
;
|
||||||
|
|
||||||
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
public function getPagerfantaForQuery(ProjectQuery $query): Pagerfanta
|
public function getPagerfantaForQuery(ProjectQuery $query): Pagerfanta
|
||||||
{
|
{
|
||||||
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||||
@@ -294,14 +306,7 @@ class ProjectRepository extends EntityRepository
|
|||||||
|
|
||||||
private function getPaginatorForQuery(ProjectQuery $query): PaginatorInterface
|
private function getPaginatorForQuery(ProjectQuery $query): PaginatorInterface
|
||||||
{
|
{
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$counter = $this->countProjectsForQuery($query);
|
||||||
$qb
|
|
||||||
->resetDQLPart('select')
|
|
||||||
->resetDQLPart('orderBy')
|
|
||||||
->select($qb->expr()->countDistinct('p.id'))
|
|
||||||
;
|
|
||||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
|
||||||
|
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
|
||||||
return new LoaderPaginator(new ProjectLoader($qb->getEntityManager()), $qb, $counter);
|
return new LoaderPaginator(new ProjectLoader($qb->getEntityManager()), $qb, $counter);
|
||||||
|
|||||||
@@ -640,6 +640,10 @@ class TimesheetRepository extends EntityRepository
|
|||||||
$currentUser = $query->getCurrentUser();
|
$currentUser = $query->getCurrentUser();
|
||||||
|
|
||||||
if (!$currentUser->isSuperAdmin() && !$currentUser->isAdmin()) {
|
if (!$currentUser->isSuperAdmin() && !$currentUser->isAdmin()) {
|
||||||
|
// make sure that the user himself is in the list of users, if he is part of a team
|
||||||
|
// if teams are used and the user is not a teamlead, the list of users would be empty and then leading to NOT limit the select by user IDs
|
||||||
|
$user[] = $currentUser;
|
||||||
|
|
||||||
foreach ($currentUser->getTeams() as $team) {
|
foreach ($currentUser->getTeams() as $team) {
|
||||||
if ($currentUser->isTeamleadOf($team)) {
|
if ($currentUser->isTeamleadOf($team)) {
|
||||||
$query->addTeam($team);
|
$query->addTeam($team);
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
|||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getPaginatorForQuery(UserQuery $query): PaginatorInterface
|
public function countUsersForQuery(UserQuery $query): int
|
||||||
{
|
{
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
$qb
|
$qb
|
||||||
@@ -244,8 +244,13 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
|||||||
->resetDQLPart('orderBy')
|
->resetDQLPart('orderBy')
|
||||||
->select($qb->expr()->countDistinct('u.id'))
|
->select($qb->expr()->countDistinct('u.id'))
|
||||||
;
|
;
|
||||||
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
|
||||||
|
|
||||||
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPaginatorForQuery(UserQuery $query): PaginatorInterface
|
||||||
|
{
|
||||||
|
$counter = $this->countUsersForQuery($query);
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
|
||||||
return new LoaderPaginator(new UserLoader($qb->getEntityManager()), $qb, $counter);
|
return new LoaderPaginator(new UserLoader($qb->getEntityManager()), $qb, $counter);
|
||||||
|
|||||||
@@ -119,12 +119,16 @@ class TimesheetValidator extends ConstraintValidator
|
|||||||
->addViolation();
|
->addViolation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $this->configuration->isAllowFutureTimes() && time() < $timesheet->getBegin()->getTimestamp()) {
|
if (false === $this->configuration->isAllowFutureTimes()) {
|
||||||
$context->buildViolation('The begin date cannot be in the future.')
|
// allow configured default rounding time + 1 minute - see #1295
|
||||||
->atPath('begin')
|
$allowedDiff = ($this->configuration->getDefaultRoundingBegin() * 60) + 60;
|
||||||
->setTranslationDomain('validators')
|
if ((time() + $allowedDiff) < $timesheet->getBegin()->getTimestamp()) {
|
||||||
->setCode(TimesheetConstraint::BEGIN_IN_FUTURE_ERROR)
|
$context->buildViolation('The begin date cannot be in the future.')
|
||||||
->addViolation();
|
->atPath('begin')
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode(TimesheetConstraint::BEGIN_IN_FUTURE_ERROR)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,9 @@
|
|||||||
{{ form_row(form.customer) }}
|
{{ form_row(form.customer) }}
|
||||||
{{ form_row(form.project) }}
|
{{ form_row(form.project) }}
|
||||||
{{ form_row(form.activity) }}
|
{{ form_row(form.activity) }}
|
||||||
{{ form_row(form.users) }}
|
{% if form.users is defined %}
|
||||||
|
{{ form_row(form.users) }}
|
||||||
|
{% endif %}
|
||||||
{{ form_row(form.tags) }}
|
{{ form_row(form.tags) }}
|
||||||
{{ form_row(form.exported) }}
|
{{ form_row(form.exported) }}
|
||||||
{{ form_row(form.template) }}
|
{{ form_row(form.template) }}
|
||||||
|
|||||||
Binary file not shown.
@@ -110,7 +110,7 @@
|
|||||||
<td class="text-right">{{ model.calculator.tax|money(model.calculator.currency) }}</td>
|
<td class="text-right">{{ model.calculator.tax|money(model.calculator.currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-right">
|
<td colspan="4" class="text-right text-nowrap">
|
||||||
<strong>{{ 'invoice.total'|trans }}</strong>
|
<strong>{{ 'invoice.total'|trans }}</strong>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ 'label.address'|trans }}</strong>: {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
<strong>{{ 'label.address'|trans }}</strong>: {{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||||
<br>
|
<br>
|
||||||
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
@@ -4,18 +4,11 @@
|
|||||||
|
|
||||||
{% block invoice %}
|
{% block invoice %}
|
||||||
|
|
||||||
{% set signature = null %}
|
|
||||||
{# You can overwrite these settings in your config/packages/local.yaml under the key "twig.globals.company" - previously '/build/images/signature.png' #}
|
|
||||||
{% set company = company|default({signature: null}) %}
|
|
||||||
{% if company.signature is defined and company.signature is not empty %}
|
|
||||||
{% set signature = company.signature %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row" id="freelancer-invoice">
|
<div class="row" id="freelancer-invoice">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<header>
|
<header>
|
||||||
<address>
|
<address>
|
||||||
<p contenteditable="true">{{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}</p>
|
<p contenteditable="true">{{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}</p>
|
||||||
</address>
|
</address>
|
||||||
</header>
|
</header>
|
||||||
<article class="address">
|
<article class="address">
|
||||||
@@ -93,9 +86,9 @@
|
|||||||
{{ entry.activity.name }} / {{ entry.project.name }}
|
{{ entry.activity.name }} / {{ entry.project.name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">{{ rate|money(model.calculator.currency) }}</td>
|
<td class="text-right text-nowrap">{{ rate|money(model.calculator.currency) }}</td>
|
||||||
<td class="text-right">{{ duration }}</td>
|
<td class="text-right text-nowrap">{{ duration }}</td>
|
||||||
<td class="text-right">{{ entry.rate|money(model.calculator.currency) }}</td>
|
<td class="text-right text-nowrap">{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -120,9 +113,6 @@
|
|||||||
<div contenteditable="true">
|
<div contenteditable="true">
|
||||||
{{ model.template.paymentTerms|nl2br|md2html }}
|
{{ model.template.paymentTerms|nl2br|md2html }}
|
||||||
</div>
|
</div>
|
||||||
{% if signature is not empty %}
|
|
||||||
<img src="{{ signature }}">
|
|
||||||
{% endif %}
|
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
@@ -132,6 +122,7 @@
|
|||||||
<strong>{{ 'label.address'|trans }}</strong>
|
<strong>{{ 'label.address'|trans }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
{{ model.template.company }}<br>
|
||||||
{{ model.template.address|nl2br }}
|
{{ model.template.address|nl2br }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use App\Entity\User;
|
|||||||
*/
|
*/
|
||||||
class ConfigurationControllerTest extends APIControllerBaseTest
|
class ConfigurationControllerTest extends APIControllerBaseTest
|
||||||
{
|
{
|
||||||
public function testI18nIsSecure()
|
public function testIsSecure()
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/api/config/i18n');
|
$this->assertUrlIsSecured('/api/config/i18n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class StatusControllerTest extends APIControllerBaseTest
|
|||||||
public function testIsSecure()
|
public function testIsSecure()
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/api/ping');
|
$this->assertUrlIsSecured('/api/ping');
|
||||||
|
$this->assertUrlIsSecured('/api/version');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPing()
|
public function testPing()
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ class TagControllerTest extends APIControllerBaseTest
|
|||||||
$this->importFixture($em, $fixture);
|
$this->importFixture($em, $fixture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->assertUrlIsSecured('/api/tags');
|
||||||
|
}
|
||||||
|
|
||||||
public function testGetCollection()
|
public function testGetCollection()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class TeamControllerTest extends APIControllerBaseTest
|
|||||||
public function testIsSecure()
|
public function testIsSecure()
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/api/teams');
|
$this->assertUrlIsSecured('/api/teams');
|
||||||
|
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/teams');
|
||||||
|
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/api/teams');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetCollection()
|
public function testGetCollection()
|
||||||
@@ -56,7 +58,7 @@ class TeamControllerTest extends APIControllerBaseTest
|
|||||||
|
|
||||||
public function testNotFound()
|
public function testNotFound()
|
||||||
{
|
{
|
||||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/teams/3');
|
$this->assertEntityNotFound(User::ROLE_ADMIN, '/api/teams/3');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDeleteActionWithUnknownTeam()
|
public function testDeleteActionWithUnknownTeam()
|
||||||
@@ -80,7 +82,7 @@ class TeamControllerTest extends APIControllerBaseTest
|
|||||||
$this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
|
$this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
|
||||||
$this->assertEmpty($client->getResponse()->getContent());
|
$this->assertEmpty($client->getResponse()->getContent());
|
||||||
|
|
||||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/teams/' . $id);
|
$this->assertEntityNotFound(User::ROLE_ADMIN, '/api/teams/' . $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function assertStructure(array $result, $full = true)
|
protected function assertStructure(array $result, $full = true)
|
||||||
@@ -90,7 +92,9 @@ class TeamControllerTest extends APIControllerBaseTest
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($full) {
|
if ($full) {
|
||||||
$expectedKeys = array_merge($expectedKeys, []);
|
$expectedKeys = array_merge($expectedKeys, [
|
||||||
|
'teamlead', 'users'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$actual = array_keys($result);
|
$actual = array_keys($result);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class UserControllerTest extends APIControllerBaseTest
|
|||||||
public function testIsSecure()
|
public function testIsSecure()
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/api/users');
|
$this->assertUrlIsSecured('/api/users');
|
||||||
|
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/users');
|
||||||
|
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/api/users');
|
||||||
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/api/users');
|
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/api/users');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +120,7 @@ class UserControllerTest extends APIControllerBaseTest
|
|||||||
if ($full) {
|
if ($full) {
|
||||||
$expectedKeys = array_merge(
|
$expectedKeys = array_merge(
|
||||||
$expectedKeys,
|
$expectedKeys,
|
||||||
['title', 'avatar', 'roles', 'language', 'timezone']
|
['title', 'avatar', 'teams', 'roles', 'language', 'timezone']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Tests\Controller;
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
@@ -35,15 +36,79 @@ class ExportControllerTest extends ControllerBaseTest
|
|||||||
$this->assertHasNoEntriesWithFilter($client);
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexActionWithEntries()
|
public function testIndexActionWithEntriesAndTeams()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
$teamlead = $this->getUserByRole($em, User::ROLE_TEAMLEAD);
|
||||||
|
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||||
|
/** @var Team $team */
|
||||||
|
$team = new Team();
|
||||||
|
$team->setName('fooo');
|
||||||
|
$team->setTeamLead($teamlead);
|
||||||
|
$team->addUser($user);
|
||||||
|
$em->persist($team);
|
||||||
|
$em->persist($user);
|
||||||
|
$em->persist($teamlead);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||||
|
|
||||||
|
$begin = new \DateTime('first day of this month');
|
||||||
|
$fixture = new TimesheetFixtures();
|
||||||
|
$fixture
|
||||||
|
->setUser($user)
|
||||||
|
->setAmount(20)
|
||||||
|
->setStartDate($begin)
|
||||||
|
->setCallback(function (Timesheet $timesheet) use ($team, $em) {
|
||||||
|
$team->addProject($timesheet->getProject());
|
||||||
|
$em->persist($team);
|
||||||
|
})
|
||||||
|
;
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$teamlead = $this->getUserByRole($em, User::ROLE_TEAMLEAD);
|
||||||
|
|
||||||
|
$fixture = new TimesheetFixtures();
|
||||||
|
$fixture
|
||||||
|
->setUser($teamlead)
|
||||||
|
->setAmount(2)
|
||||||
|
->setStartDate($begin)
|
||||||
|
;
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$this->request($client, '/export/?preview=');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
// make sure all existing records are displayed
|
||||||
|
$this->assertHasDataTable($client);
|
||||||
|
$this->assertDataTableRowCount($client, 'datatable_export', 22);
|
||||||
|
|
||||||
|
// assert export type buttons are available
|
||||||
|
$expected = ['csv', 'html', 'pdf', 'xlsx'];
|
||||||
|
$node = $client->getCrawler()->filter('#export-buttons button');
|
||||||
|
$this->assertEquals(count($expected), $node->count());
|
||||||
|
/** @var \DOMElement $button */
|
||||||
|
foreach ($node->getIterator() as $button) {
|
||||||
|
$type = $button->getAttribute('data-type');
|
||||||
|
$this->assertContains($type, $expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIndexActionWithEntriesForTeamleadDoesNotShowUserWithoutTeam()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
$begin = new \DateTime('first day of this month');
|
$begin = new \DateTime('first day of this month');
|
||||||
|
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||||
|
|
||||||
|
// these should be ignored, becuase teamlead and user do NOT share a team!
|
||||||
$fixture = new TimesheetFixtures();
|
$fixture = new TimesheetFixtures();
|
||||||
$fixture
|
$fixture
|
||||||
->setUser($this->getUserByRole($em, User::ROLE_USER))
|
->setUser($user)
|
||||||
->setAmount(20)
|
->setAmount(20)
|
||||||
->setStartDate($begin)
|
->setStartDate($begin)
|
||||||
;
|
;
|
||||||
@@ -52,9 +117,25 @@ class ExportControllerTest extends ControllerBaseTest
|
|||||||
$this->request($client, '/export/?preview=');
|
$this->request($client, '/export/?preview=');
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
// make sure all existing records are displayed
|
||||||
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
|
|
||||||
|
$teamlead = $this->getUserByRole($em, User::ROLE_TEAMLEAD);
|
||||||
|
|
||||||
|
$fixture = new TimesheetFixtures();
|
||||||
|
$fixture
|
||||||
|
->setUser($teamlead)
|
||||||
|
->setAmount(2)
|
||||||
|
->setStartDate($begin)
|
||||||
|
;
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$this->request($client, '/export/?preview=');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
// make sure all existing records are displayed
|
// make sure all existing records are displayed
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasDataTable($client);
|
||||||
$this->assertDataTableRowCount($client, 'datatable_export', 20);
|
$this->assertDataTableRowCount($client, 'datatable_export', 2);
|
||||||
|
|
||||||
// assert export type buttons are available
|
// assert export type buttons are available
|
||||||
$expected = ['csv', 'html', 'pdf', 'xlsx'];
|
$expected = ['csv', 'html', 'pdf', 'xlsx'];
|
||||||
@@ -102,7 +183,7 @@ class ExportControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
public function testExportAction()
|
public function testExportAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
/** @var EntityManager $em */
|
/** @var EntityManager $em */
|
||||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
public function testExportAction()
|
public function testExportAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|
||||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
$fixture = new TimesheetFixtures();
|
$fixture = new TimesheetFixtures();
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ class ConfigurationTest extends TestCase
|
|||||||
],
|
],
|
||||||
'invoice' => [
|
'invoice' => [
|
||||||
'documents' => [
|
'documents' => [
|
||||||
|
],
|
||||||
|
'defaults' => [
|
||||||
0 => 'var/invoices/',
|
0 => 'var/invoices/',
|
||||||
1 => 'templates/invoice/renderer/',
|
1 => 'templates/invoice/renderer/',
|
||||||
],
|
],
|
||||||
|
|||||||
39
tests/Event/InvoicePostRenderEventTest.php
Normal file
39
tests/Event/InvoicePostRenderEventTest.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Event;
|
||||||
|
|
||||||
|
use App\Entity\InvoiceDocument;
|
||||||
|
use App\Event\InvoicePostRenderEvent;
|
||||||
|
use App\Invoice\InvoiceModel;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
|
use App\Tests\Invoice\Renderer\DebugRenderer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Event\InvoicePostRenderEvent
|
||||||
|
*/
|
||||||
|
class InvoicePostRenderEventTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDefaultValues()
|
||||||
|
{
|
||||||
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
|
$document = new InvoiceDocument(new \SplFileInfo(__FILE__));
|
||||||
|
$renderer = new DebugRenderer();
|
||||||
|
$response = new Response();
|
||||||
|
|
||||||
|
$sut = new InvoicePostRenderEvent($model, $document, $renderer, $response);
|
||||||
|
|
||||||
|
self::assertSame($model, $sut->getModel());
|
||||||
|
self::assertSame($document, $sut->getDocument());
|
||||||
|
self::assertSame($renderer, $sut->getRenderer());
|
||||||
|
self::assertSame($response, $sut->getResponse());
|
||||||
|
}
|
||||||
|
}
|
||||||
36
tests/Event/InvoicePreRenderEventTest.php
Normal file
36
tests/Event/InvoicePreRenderEventTest.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Event;
|
||||||
|
|
||||||
|
use App\Entity\InvoiceDocument;
|
||||||
|
use App\Event\InvoicePreRenderEvent;
|
||||||
|
use App\Invoice\InvoiceModel;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
|
use App\Tests\Invoice\Renderer\DebugRenderer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Event\InvoicePreRenderEvent
|
||||||
|
*/
|
||||||
|
class InvoicePreRenderEventTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDefaultValues()
|
||||||
|
{
|
||||||
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
|
$document = new InvoiceDocument(new \SplFileInfo(__FILE__));
|
||||||
|
$renderer = new DebugRenderer();
|
||||||
|
|
||||||
|
$sut = new InvoicePreRenderEvent($model, $document, $renderer);
|
||||||
|
|
||||||
|
self::assertSame($model, $sut->getModel());
|
||||||
|
self::assertSame($document, $sut->getDocument());
|
||||||
|
self::assertSame($renderer, $sut->getRenderer());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,16 +75,16 @@ class DashboardSubscriberTest extends TestCase
|
|||||||
$authMock->method('isGranted')->willReturn($isAdmin);
|
$authMock->method('isGranted')->willReturn($isAdmin);
|
||||||
|
|
||||||
$userMock = $this->getMockBuilder(UserRepository::class)->disableOriginalConstructor()->getMock();
|
$userMock = $this->getMockBuilder(UserRepository::class)->disableOriginalConstructor()->getMock();
|
||||||
$userMock->method('countUser')->willReturn($userCount);
|
$userMock->method('countUsersForQuery')->willReturn($userCount);
|
||||||
|
|
||||||
$projectMock = $this->getMockBuilder(ProjectRepository::class)->disableOriginalConstructor()->getMock();
|
$projectMock = $this->getMockBuilder(ProjectRepository::class)->disableOriginalConstructor()->getMock();
|
||||||
$projectMock->method('countProject')->willReturn($projectCount);
|
$projectMock->method('countProjectsForQuery')->willReturn($projectCount);
|
||||||
|
|
||||||
$activityMock = $this->getMockBuilder(ActivityRepository::class)->disableOriginalConstructor()->getMock();
|
$activityMock = $this->getMockBuilder(ActivityRepository::class)->disableOriginalConstructor()->getMock();
|
||||||
$activityMock->method('countActivity')->willReturn($activityCount);
|
$activityMock->method('countActivitiesForQuery')->willReturn($activityCount);
|
||||||
|
|
||||||
$customerMock = $this->getMockBuilder(CustomerRepository::class)->disableOriginalConstructor()->getMock();
|
$customerMock = $this->getMockBuilder(CustomerRepository::class)->disableOriginalConstructor()->getMock();
|
||||||
$customerMock->method('countCustomer')->willReturn($customerCount);
|
$customerMock->method('countCustomersForQuery')->willReturn($customerCount);
|
||||||
|
|
||||||
return new DashboardSubscriber($authMock, $userMock, $activityMock, $projectMock, $customerMock);
|
return new DashboardSubscriber($authMock, $userMock, $activityMock, $projectMock, $customerMock);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use App\Entity\User;
|
|||||||
use App\Invoice\CalculatorInterface;
|
use App\Invoice\CalculatorInterface;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
abstract class AbstractCalculatorTest extends TestCase
|
abstract class AbstractCalculatorTest extends TestCase
|
||||||
@@ -28,7 +29,7 @@ abstract class AbstractCalculatorTest extends TestCase
|
|||||||
$template = new InvoiceTemplate();
|
$template = new InvoiceTemplate();
|
||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setQuery($query);
|
$model->setQuery($query);
|
||||||
@@ -80,7 +81,7 @@ abstract class AbstractCalculatorTest extends TestCase
|
|||||||
->setActivity($activity)
|
->setActivity($activity)
|
||||||
->setProject($project);
|
->setProject($project);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries([$timesheet]);
|
$model->setEntries([$timesheet]);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use App\Entity\User;
|
|||||||
use App\Invoice\Calculator\ActivityInvoiceCalculator;
|
use App\Invoice\Calculator\ActivityInvoiceCalculator;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\Invoice\Calculator\ActivityInvoiceCalculator
|
* @covers \App\Invoice\Calculator\ActivityInvoiceCalculator
|
||||||
@@ -105,7 +106,7 @@ class ActivityInvoiceCalculatorTest extends AbstractCalculatorTest
|
|||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
$query->setActivity($activity1);
|
$query->setActivity($activity1);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use App\Entity\User;
|
|||||||
use App\Invoice\Calculator\DateInvoiceCalculator;
|
use App\Invoice\Calculator\DateInvoiceCalculator;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +107,7 @@ class DateInvoiceCalculatorTest extends AbstractCalculatorTest
|
|||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
$query->setProject($project1);
|
$query->setProject($project1);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use App\Entity\Timesheet;
|
|||||||
use App\Invoice\Calculator\DefaultCalculator;
|
use App\Invoice\Calculator\DefaultCalculator;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\Invoice\Calculator\DefaultCalculator
|
* @covers \App\Invoice\Calculator\DefaultCalculator
|
||||||
@@ -57,7 +58,7 @@ class DefaultCalculatorTest extends AbstractCalculatorTest
|
|||||||
|
|
||||||
$entries = [$timesheet, $timesheet2, $timesheet3];
|
$entries = [$timesheet, $timesheet2, $timesheet3];
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use App\Entity\User;
|
|||||||
use App\Invoice\Calculator\ProjectInvoiceCalculator;
|
use App\Invoice\Calculator\ProjectInvoiceCalculator;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +107,7 @@ class ProjectInvoiceCalculatorTest extends AbstractCalculatorTest
|
|||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
$query->setProject($project1);
|
$query->setProject($project1);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use App\Invoice\Calculator\ShortInvoiceCalculator;
|
|||||||
use App\Invoice\InvoiceItem;
|
use App\Invoice\InvoiceItem;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\Invoice\Calculator\ShortInvoiceCalculator
|
* @covers \App\Invoice\Calculator\ShortInvoiceCalculator
|
||||||
@@ -86,7 +87,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
|
|||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
$query->setActivity($activity);
|
$query->setActivity($activity);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
@@ -167,7 +168,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
|
|||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
$query->setActivity($activity);
|
$query->setActivity($activity);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
@@ -246,7 +247,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
|
|||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
$query->setActivity($activity);
|
$query->setActivity($activity);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use App\Entity\User;
|
|||||||
use App\Invoice\Calculator\UserInvoiceCalculator;
|
use App\Invoice\Calculator\UserInvoiceCalculator;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\Invoice\Calculator\UserInvoiceCalculator
|
* @covers \App\Invoice\Calculator\UserInvoiceCalculator
|
||||||
@@ -104,7 +105,7 @@ class UserInvoiceCalculatorTest extends AbstractCalculatorTest
|
|||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
$query->setActivity($activity);
|
$query->setActivity($activity);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel(new DebugFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
|
|||||||
79
tests/Invoice/DebugFormatter.php
Normal file
79
tests/Invoice/DebugFormatter.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Invoice;
|
||||||
|
|
||||||
|
use App\Invoice\InvoiceFormatter;
|
||||||
|
|
||||||
|
class DebugFormatter implements InvoiceFormatter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDateTime(\DateTime $date)
|
||||||
|
{
|
||||||
|
return $date->format('d.m.Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedTime(\DateTime $date)
|
||||||
|
{
|
||||||
|
return $date->format('H:i');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $amount
|
||||||
|
* @param string|null $currency
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedMoney($amount, $currency)
|
||||||
|
{
|
||||||
|
if (null !== $currency) {
|
||||||
|
return $amount . ' ' . $currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedMonthName(\DateTime $date)
|
||||||
|
{
|
||||||
|
return $date->format('m');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $seconds
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDuration($seconds)
|
||||||
|
{
|
||||||
|
return $seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $seconds
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getFormattedDecimalDuration($seconds)
|
||||||
|
{
|
||||||
|
return $seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrencySymbol(string $currency): string
|
||||||
|
{
|
||||||
|
return $currency;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,5 +35,7 @@ class InvoiceItemTest extends TestCase
|
|||||||
self::assertNull($sut->getUser());
|
self::assertNull($sut->getUser());
|
||||||
self::assertNull($sut->getDescription());
|
self::assertNull($sut->getDescription());
|
||||||
self::assertEquals(0, $sut->getDuration());
|
self::assertEquals(0, $sut->getDuration());
|
||||||
|
self::assertNull($sut->getCategory());
|
||||||
|
self::assertNull($sut->getType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ class InvoiceModelTest extends TestCase
|
|||||||
{
|
{
|
||||||
public function testEmptyObject()
|
public function testEmptyObject()
|
||||||
{
|
{
|
||||||
$sut = new InvoiceModel();
|
$formatter = new DebugFormatter();
|
||||||
|
$sut = new InvoiceModel($formatter);
|
||||||
|
|
||||||
self::assertNull($sut->getQuery());
|
self::assertNull($sut->getQuery());
|
||||||
self::assertNull($sut->getCustomer());
|
self::assertNull($sut->getCustomer());
|
||||||
@@ -38,11 +39,13 @@ class InvoiceModelTest extends TestCase
|
|||||||
|
|
||||||
self::assertNull($sut->getTemplate());
|
self::assertNull($sut->getTemplate());
|
||||||
self::assertInstanceOf(\DateTime::class, $sut->getInvoiceDate());
|
self::assertInstanceOf(\DateTime::class, $sut->getInvoiceDate());
|
||||||
|
|
||||||
|
self::assertSame($formatter, $sut->getFormatter());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetter()
|
public function testSetter()
|
||||||
{
|
{
|
||||||
$sut = new InvoiceModel();
|
$sut = new InvoiceModel(new DebugFormatter());
|
||||||
|
|
||||||
$query = new InvoiceQuery();
|
$query = new InvoiceQuery();
|
||||||
self::assertInstanceOf(InvoiceModel::class, $sut->setQuery($query));
|
self::assertInstanceOf(InvoiceModel::class, $sut->setQuery($query));
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Tests\Invoice\NumberGenerator;
|
|||||||
|
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
||||||
|
use App\Tests\Invoice\DebugFormatter;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,7 +22,7 @@ class DateNumberGeneratorTest extends TestCase
|
|||||||
public function testGetInvoiceNumber()
|
public function testGetInvoiceNumber()
|
||||||
{
|
{
|
||||||
$sut = new DateNumberGenerator();
|
$sut = new DateNumberGenerator();
|
||||||
$sut->setModel(new InvoiceModel());
|
$sut->setModel(new InvoiceModel(new DebugFormatter()));
|
||||||
|
|
||||||
$this->assertEquals(date('ymd'), $sut->getInvoiceNumber());
|
$this->assertEquals(date('ymd'), $sut->getInvoiceNumber());
|
||||||
$this->assertEquals('default', $sut->getId());
|
$this->assertEquals('default', $sut->getId());
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ namespace App\Tests\Invoice\Renderer;
|
|||||||
|
|
||||||
use App\Entity\InvoiceDocument;
|
use App\Entity\InvoiceDocument;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Invoice\Renderer\RendererTrait;
|
use App\Invoice\Renderer\AbstractRenderer;
|
||||||
use App\Invoice\RendererInterface;
|
use App\Invoice\RendererInterface;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class DebugRenderer implements RendererInterface
|
class DebugRenderer extends AbstractRenderer implements RendererInterface
|
||||||
{
|
{
|
||||||
use RendererTrait;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
@@ -35,60 +33,6 @@ class DebugRenderer implements RendererInterface
|
|||||||
return 'array';
|
return 'array';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $date
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedDateTime(\DateTime $date)
|
|
||||||
{
|
|
||||||
return $date->format('d.m.Y');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $date
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedTime(\DateTime $date)
|
|
||||||
{
|
|
||||||
return $date->format('H:i');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed $amount
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedMoney($amount)
|
|
||||||
{
|
|
||||||
return $amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $date
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedMonthName(\DateTime $date)
|
|
||||||
{
|
|
||||||
return $date->format('m');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed $seconds
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedDuration($seconds)
|
|
||||||
{
|
|
||||||
return $seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed $seconds
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
protected function getFormattedDecimalDuration($seconds)
|
|
||||||
{
|
|
||||||
return $seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the given InvoiceDocument with the data from the InvoiceModel into a stupid array for testing only.
|
* Render the given InvoiceDocument with the data from the InvoiceModel into a stupid array for testing only.
|
||||||
*
|
*
|
||||||
@@ -99,12 +43,12 @@ class DebugRenderer implements RendererInterface
|
|||||||
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
public function render(InvoiceDocument $document, InvoiceModel $model): Response
|
||||||
{
|
{
|
||||||
$result = [
|
$result = [
|
||||||
'model' => $this->modelToReplacer($model),
|
'model' => $model->toArray(),
|
||||||
'entries' => [],
|
'entries' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($model->getCalculator()->getEntries() as $entry) {
|
foreach ($model->getCalculator()->getEntries() as $entry) {
|
||||||
$result['entries'][] = $this->timesheetToArray($entry);
|
$result['entries'][] = $model->itemToArray($entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(json_encode($result));
|
return new Response(json_encode($result));
|
||||||
|
|||||||
@@ -58,15 +58,19 @@ class DebugRendererTest extends TestCase
|
|||||||
'invoice.date',
|
'invoice.date',
|
||||||
'invoice.number',
|
'invoice.number',
|
||||||
'invoice.currency',
|
'invoice.currency',
|
||||||
|
'invoice.currency_symbol',
|
||||||
'invoice.vat',
|
'invoice.vat',
|
||||||
'invoice.tax',
|
'invoice.tax',
|
||||||
'invoice.tax_nc',
|
'invoice.tax_nc',
|
||||||
|
'invoice.tax_plain',
|
||||||
'invoice.total_time',
|
'invoice.total_time',
|
||||||
'invoice.duration_decimal',
|
'invoice.duration_decimal',
|
||||||
'invoice.total',
|
'invoice.total',
|
||||||
'invoice.total_nc',
|
'invoice.total_nc',
|
||||||
|
'invoice.total_plain',
|
||||||
'invoice.subtotal',
|
'invoice.subtotal',
|
||||||
'invoice.subtotal_nc',
|
'invoice.subtotal_nc',
|
||||||
|
'invoice.subtotal_plain',
|
||||||
'template.name',
|
'template.name',
|
||||||
'template.company',
|
'template.company',
|
||||||
'template.address',
|
'template.address',
|
||||||
@@ -93,14 +97,28 @@ class DebugRendererTest extends TestCase
|
|||||||
'customer.homepage',
|
'customer.homepage',
|
||||||
'customer.comment',
|
'customer.comment',
|
||||||
'customer.fixed_rate',
|
'customer.fixed_rate',
|
||||||
|
'customer.fixed_rate_nc',
|
||||||
|
'customer.fixed_rate_plain',
|
||||||
'customer.hourly_rate',
|
'customer.hourly_rate',
|
||||||
|
'customer.hourly_rate_nc',
|
||||||
|
'customer.hourly_rate_plain',
|
||||||
'customer.meta.foo-customer',
|
'customer.meta.foo-customer',
|
||||||
'activity.id',
|
'activity.id',
|
||||||
'activity.name',
|
'activity.name',
|
||||||
'activity.comment',
|
'activity.comment',
|
||||||
'activity.fixed_rate',
|
'activity.fixed_rate',
|
||||||
|
'activity.fixed_rate_nc',
|
||||||
|
'activity.fixed_rate_plain',
|
||||||
'activity.hourly_rate',
|
'activity.hourly_rate',
|
||||||
|
'activity.hourly_rate_nc',
|
||||||
|
'activity.hourly_rate_plain',
|
||||||
'activity.meta.foo-activity',
|
'activity.meta.foo-activity',
|
||||||
|
'user.alias',
|
||||||
|
'user.email',
|
||||||
|
'user.name',
|
||||||
|
'user.title',
|
||||||
|
'user.meta.hello',
|
||||||
|
'user.meta.kitty',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($hasProject) {
|
if ($hasProject) {
|
||||||
@@ -111,7 +129,11 @@ class DebugRendererTest extends TestCase
|
|||||||
'project.order_date',
|
'project.order_date',
|
||||||
'project.order_number',
|
'project.order_number',
|
||||||
'project.fixed_rate',
|
'project.fixed_rate',
|
||||||
|
'project.fixed_rate_nc',
|
||||||
|
'project.fixed_rate_plain',
|
||||||
'project.hourly_rate',
|
'project.hourly_rate',
|
||||||
|
'project.hourly_rate_nc',
|
||||||
|
'project.hourly_rate_plain',
|
||||||
'project.meta.foo-project',
|
'project.meta.foo-project',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -131,8 +153,10 @@ class DebugRendererTest extends TestCase
|
|||||||
'entry.amount',
|
'entry.amount',
|
||||||
'entry.rate',
|
'entry.rate',
|
||||||
'entry.rate_nc',
|
'entry.rate_nc',
|
||||||
|
'entry.rate_plain',
|
||||||
'entry.total',
|
'entry.total',
|
||||||
'entry.total_nc',
|
'entry.total_nc',
|
||||||
|
'entry.total_plain',
|
||||||
'entry.currency',
|
'entry.currency',
|
||||||
'entry.duration',
|
'entry.duration',
|
||||||
'entry.duration_decimal',
|
'entry.duration_decimal',
|
||||||
@@ -154,6 +178,8 @@ class DebugRendererTest extends TestCase
|
|||||||
'entry.customer',
|
'entry.customer',
|
||||||
'entry.project_id',
|
'entry.project_id',
|
||||||
'entry.customer_id',
|
'entry.customer_id',
|
||||||
|
'entry.category',
|
||||||
|
'entry.type',
|
||||||
];
|
];
|
||||||
|
|
||||||
$keys = array_merge($keys, $metaFields);
|
$keys = array_merge($keys, $metaFields);
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ use App\Entity\ProjectMeta;
|
|||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\TimesheetMeta;
|
use App\Entity\TimesheetMeta;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Entity\UserPreference;
|
||||||
use App\Invoice\Calculator\DefaultCalculator;
|
use App\Invoice\Calculator\DefaultCalculator;
|
||||||
|
use App\Invoice\DefaultInvoiceFormatter;
|
||||||
|
use App\Invoice\InvoiceFormatter;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
||||||
use App\Invoice\Renderer\AbstractRenderer;
|
use App\Invoice\Renderer\AbstractRenderer;
|
||||||
@@ -65,6 +68,11 @@ trait RendererTestTrait
|
|||||||
* @return AbstractRenderer
|
* @return AbstractRenderer
|
||||||
*/
|
*/
|
||||||
protected function getAbstractRenderer(string $classname)
|
protected function getAbstractRenderer(string $classname)
|
||||||
|
{
|
||||||
|
return new $classname();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormatter(): InvoiceFormatter
|
||||||
{
|
{
|
||||||
$requestStack = new RequestStack();
|
$requestStack = new RequestStack();
|
||||||
$languages = [
|
$languages = [
|
||||||
@@ -85,11 +93,19 @@ trait RendererTestTrait
|
|||||||
$dateExtension = new DateExtensions($localeSettings);
|
$dateExtension = new DateExtensions($localeSettings);
|
||||||
$extensions = new Extensions($localeSettings);
|
$extensions = new Extensions($localeSettings);
|
||||||
|
|
||||||
return new $classname($translator, $dateExtension, $extensions);
|
return new DefaultInvoiceFormatter($translator, $dateExtension, $extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getInvoiceModel(): InvoiceModel
|
protected function getInvoiceModel(): InvoiceModel
|
||||||
{
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setUsername('one-user');
|
||||||
|
$user->setTitle('user title');
|
||||||
|
$user->setAlias('genious alias');
|
||||||
|
$user->setEmail('fantastic@four');
|
||||||
|
$user->addPreference((new UserPreference())->setName('kitty')->setValue('kat'));
|
||||||
|
$user->addPreference((new UserPreference())->setName('hello')->setValue('world'));
|
||||||
|
|
||||||
$customer = new Customer();
|
$customer = new Customer();
|
||||||
$customer->setCurrency('EUR');
|
$customer->setCurrency('EUR');
|
||||||
$customer->setMetaField((new CustomerMeta())->setName('foo-customer')->setValue('bar-customer')->setIsVisible(true));
|
$customer->setMetaField((new CustomerMeta())->setName('foo-customer')->setValue('bar-customer')->setIsVisible(true));
|
||||||
@@ -185,11 +201,12 @@ trait RendererTestTrait
|
|||||||
$query->setEnd(new \DateTime());
|
$query->setEnd(new \DateTime());
|
||||||
$query->setProject($project);
|
$query->setProject($project);
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel($this->getFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
$model->setQuery($query);
|
$model->setQuery($query);
|
||||||
|
$model->setUser($user);
|
||||||
|
|
||||||
$calculator = new DefaultCalculator();
|
$calculator = new DefaultCalculator();
|
||||||
$calculator->setModel($model);
|
$calculator->setModel($model);
|
||||||
@@ -206,6 +223,14 @@ trait RendererTestTrait
|
|||||||
|
|
||||||
protected function getInvoiceModelOneEntry(): InvoiceModel
|
protected function getInvoiceModelOneEntry(): InvoiceModel
|
||||||
{
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setUsername('one-user');
|
||||||
|
$user->setTitle('user title');
|
||||||
|
$user->setAlias('genious alias');
|
||||||
|
$user->setEmail('fantastic@four');
|
||||||
|
$user->addPreference((new UserPreference())->setName('kitty')->setValue('kat'));
|
||||||
|
$user->addPreference((new UserPreference())->setName('hello')->setValue('world'));
|
||||||
|
|
||||||
$customer = new Customer();
|
$customer = new Customer();
|
||||||
$customer->setCurrency('USD');
|
$customer->setCurrency('USD');
|
||||||
$customer->setMetaField((new CustomerMeta())->setName('foo-customer')->setValue('bar-customer')->setIsVisible(true));
|
$customer->setMetaField((new CustomerMeta())->setName('foo-customer')->setValue('bar-customer')->setIsVisible(true));
|
||||||
@@ -248,11 +273,12 @@ trait RendererTestTrait
|
|||||||
$query->setBegin(new \DateTime());
|
$query->setBegin(new \DateTime());
|
||||||
$query->setEnd(new \DateTime());
|
$query->setEnd(new \DateTime());
|
||||||
|
|
||||||
$model = new InvoiceModel();
|
$model = new InvoiceModel($this->getFormatter());
|
||||||
$model->setCustomer($customer);
|
$model->setCustomer($customer);
|
||||||
$model->setTemplate($template);
|
$model->setTemplate($template);
|
||||||
$model->setEntries($entries);
|
$model->setEntries($entries);
|
||||||
$model->setQuery($query);
|
$model->setQuery($query);
|
||||||
|
$model->setUser($user);
|
||||||
|
|
||||||
$calculator = new DefaultCalculator();
|
$calculator = new DefaultCalculator();
|
||||||
$calculator->setModel($model);
|
$calculator->setModel($model);
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of the Kimai time-tracking app.
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Tests\Model;
|
|
||||||
|
|
||||||
use App\Entity\Customer;
|
|
||||||
use App\Entity\InvoiceTemplate;
|
|
||||||
use App\Entity\Timesheet;
|
|
||||||
use App\Invoice\Calculator\DefaultCalculator;
|
|
||||||
use App\Invoice\InvoiceModel;
|
|
||||||
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
|
||||||
use App\Repository\Query\InvoiceQuery;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @covers \App\Invoice\InvoiceModel
|
|
||||||
*/
|
|
||||||
class InvoiceModelTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testDefaultValues()
|
|
||||||
{
|
|
||||||
$sut = new InvoiceModel();
|
|
||||||
$this->assertNull($sut->getNumberGenerator());
|
|
||||||
$this->assertNull($sut->getCalculator());
|
|
||||||
$this->assertNull($sut->getTemplate());
|
|
||||||
$this->assertNull($sut->getCustomer());
|
|
||||||
$this->assertNull($sut->getQuery());
|
|
||||||
$this->assertNull($sut->getDueDate());
|
|
||||||
$this->assertEmpty($sut->getEntries());
|
|
||||||
$this->assertIsArray($sut->getEntries());
|
|
||||||
$this->assertInstanceOf(\DateTime::class, $sut->getInvoiceDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSetter()
|
|
||||||
{
|
|
||||||
$sut = new InvoiceModel();
|
|
||||||
|
|
||||||
$sut->setTemplate((new InvoiceTemplate())->setDueDays(10));
|
|
||||||
$sut->setCustomer(new Customer());
|
|
||||||
$sut->setQuery(new InvoiceQuery());
|
|
||||||
$sut->setEntries([new Timesheet()]);
|
|
||||||
$sut->setNumberGenerator(new DateNumberGenerator());
|
|
||||||
$sut->setCalculator(new DefaultCalculator());
|
|
||||||
|
|
||||||
$this->assertInstanceOf(DateNumberGenerator::class, $sut->getNumberGenerator());
|
|
||||||
$this->assertInstanceOf(DefaultCalculator::class, $sut->getCalculator());
|
|
||||||
$this->assertInstanceOf(InvoiceTemplate::class, $sut->getTemplate());
|
|
||||||
$this->assertInstanceOf(Customer::class, $sut->getCustomer());
|
|
||||||
$this->assertInstanceOf(InvoiceQuery::class, $sut->getQuery());
|
|
||||||
$this->assertInstanceOf(\DateTime::class, $sut->getDueDate());
|
|
||||||
$this->assertInstanceOf(Timesheet::class, $sut->getEntries()[0]);
|
|
||||||
$this->assertInstanceOf(\DateTime::class, $sut->getInvoiceDate());
|
|
||||||
|
|
||||||
$this->assertEquals(
|
|
||||||
(new \DateTime('+10 days'))->format('Y-m-d'),
|
|
||||||
$sut->getDueDate()->format('Y-m-d')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,6 +38,11 @@ class TimesheetValidatorTest extends ConstraintValidatorTestCase
|
|||||||
'rules' => [
|
'rules' => [
|
||||||
'allow_future_times' => false,
|
'allow_future_times' => false,
|
||||||
],
|
],
|
||||||
|
'rounding' => [
|
||||||
|
'default' => [
|
||||||
|
'begin' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
$service = (new TrackingModeServiceFactory($this))->create('default');
|
$service = (new TrackingModeServiceFactory($this))->create('default');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user